diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-06-13 11:15:52 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-04 14:15:34 +0200 |
commit | 47b553e761f7e061cdce150003123f1f5de724be (patch) | |
tree | df209129f10f5f448acaf32ffbc119e0f80050f3 | |
parent | ab94fb19012b9686efeef543909d2b555fcf8c42 (diff) | |
download | sonarqube-47b553e761f7e061cdce150003123f1f5de724be.tar.gz sonarqube-47b553e761f7e061cdce150003123f1f5de724be.zip |
SONAR-9401 Remove usage of redux on the project activity page
* Create a query helper library and use it in issues page and project activity page
56 files changed, 770 insertions, 1718 deletions
diff --git a/server/sonar-web/src/main/js/api/projectActivity.js b/server/sonar-web/src/main/js/api/projectActivity.js index 131256b0696..8ffe600e8df 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.js +++ b/server/sonar-web/src/main/js/api/projectActivity.js @@ -30,30 +30,15 @@ type GetProjectActivityResponse = { }; type GetProjectActivityOptions = { + project: string, category?: ?string, - pageIndex?: ?number, - pageSize?: ?number + p?: ?number, + ps?: ?number }; export const getProjectActivity = ( - project: string, - options?: GetProjectActivityOptions -): Promise<GetProjectActivityResponse> => { - const data: Object = { project }; - if (options) { - if (options.category) { - data.category = options.category; - } - if (options.pageIndex) { - data.p = options.pageIndex; - } - if (options.pageSize) { - data.ps = options.pageSize; - } - } - - return getJSON('/api/project_analyses/search', data); -}; + data: GetProjectActivityOptions +): Promise<GetProjectActivityResponse> => getJSON('/api/project_analyses/search', data); type CreateEventResponse = { analysis: string, diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js index af8e4bde73c..c653d64e7b8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js @@ -88,7 +88,8 @@ export default class MeasureHistory extends React.PureComponent { return Promise.resolve([]); } - return getProjectActivity(this.props.component.key, { + return getProjectActivity({ + project: this.props.component.key, category: 'VERSION' }).then(({ analyses }) => { const events = analyses.map(analysis => { diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index d827a0d1f1f..e003d9ec0ea 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -58,15 +58,19 @@ import EmptySearch from '../../../components/common/EmptySearch'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; import type { Issue } from '../../../components/issue/types'; +import type { RawQuery } from '../../../helpers/query'; import '../styles.css'; export type Props = { component?: Component, currentUser: CurrentUser, - fetchIssues: ({}) => Promise<*>, - location: { pathname: string, query: { [string]: string } }, + fetchIssues: (query: RawQuery) => Promise<*>, + location: { pathname: string, query: RawQuery }, onRequestFail: Error => void, - router: { push: ({}) => void, replace: ({}) => void } + router: { + push: ({ pathname: string, query?: RawQuery }) => void, + replace: ({ pathname: string, query?: RawQuery }) => void + } }; export type State = { diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js index 3b2ce078438..0ce4f8bb598 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js @@ -29,8 +29,7 @@ import { getOrganizations } from '../../../api/organizations'; import { receiveOrganizations } from '../../../store/organizations/duck'; import { searchIssues } from '../../../api/issues'; import { parseIssueFromResponse } from '../../../helpers/issues'; - -type Query = { [string]: string }; +import type { RawQuery } from '../../../helpers/query'; const mapStateToProps = (state, ownProps) => ({ component: ownProps.location.query.id @@ -51,7 +50,7 @@ const fetchIssueOrganizations = issues => dispatch => { ); }; -const fetchIssues = (query: Query) => dispatch => +const fetchIssues = (query: RawQuery) => dispatch => searchIssues({ ...query, additionalFields: '_all' }) .then(response => { const parsedIssues = response.issues.map(issue => diff --git a/server/sonar-web/src/main/js/apps/issues/redirects.js b/server/sonar-web/src/main/js/apps/issues/redirects.js index 3efc1af64be..a66bb570a7b 100644 --- a/server/sonar-web/src/main/js/apps/issues/redirects.js +++ b/server/sonar-web/src/main/js/apps/issues/redirects.js @@ -19,7 +19,7 @@ */ // @flow import { parseQuery, areMyIssuesSelected, serializeQuery } from './utils'; -import type { RawQuery } from './utils'; +import type { RawQuery } from '../../helpers/query'; const parseHash = (hash: string): RawQuery => { const query: RawQuery = {}; diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js index d33defd8135..72451971f7a 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.js +++ b/server/sonar-web/src/main/js/apps/issues/utils.js @@ -18,11 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import { isNil, omitBy } from 'lodash'; import { searchMembers } from '../../api/organizations'; import { searchUsers } from '../../api/users'; - -export type RawQuery = { [string]: string }; +import { + queriesEqual, + cleanQuery, + parseAsBoolean, + parseAsFacetMode, + parseAsArray, + parseAsString, + serializeString, + serializeStringArray +} from '../../helpers/query'; +import type { RawQuery } from '../../helpers/query'; export type Query = {| assigned: boolean, @@ -56,112 +64,70 @@ export type Paging = { total: number }; -const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean => - (value === 'false' ? false : value === 'true' ? true : defaultValue); - -const parseAsString = (value: ?string): string => value || ''; - -const parseAsStringArray = (value: ?string): Array<string> => (value ? value.split(',') : []); - -const parseAsFacetMode = (facetMode: string) => - (facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count'); - // allow sorting by CREATION_DATE only const parseAsSort = (sort: string): string => (sort === 'CREATION_DATE' ? 'CREATION_DATE' : ''); export const parseQuery = (query: RawQuery): Query => ({ assigned: parseAsBoolean(query.assigned), - assignees: parseAsStringArray(query.assignees), - authors: parseAsStringArray(query.authors), + assignees: parseAsArray(query.assignees, parseAsString), + authors: parseAsArray(query.authors, parseAsString), createdAfter: parseAsString(query.createdAfter), createdAt: parseAsString(query.createdAt), createdBefore: parseAsString(query.createdBefore), createdInLast: parseAsString(query.createdInLast), - directories: parseAsStringArray(query.directories), + directories: parseAsArray(query.directories, parseAsString), facetMode: parseAsFacetMode(query.facetMode), - files: parseAsStringArray(query.fileUuids), - issues: parseAsStringArray(query.issues), - languages: parseAsStringArray(query.languages), - modules: parseAsStringArray(query.moduleUuids), - projects: parseAsStringArray(query.projectUuids), + files: parseAsArray(query.fileUuids, parseAsString), + issues: parseAsArray(query.issues, parseAsString), + languages: parseAsArray(query.languages, parseAsString), + modules: parseAsArray(query.moduleUuids, parseAsString), + projects: parseAsArray(query.projectUuids, parseAsString), resolved: parseAsBoolean(query.resolved), - resolutions: parseAsStringArray(query.resolutions), - rules: parseAsStringArray(query.rules), + resolutions: parseAsArray(query.resolutions, parseAsString), + rules: parseAsArray(query.rules, parseAsString), sort: parseAsSort(query.s), - severities: parseAsStringArray(query.severities), + severities: parseAsArray(query.severities, parseAsString), sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false), - statuses: parseAsStringArray(query.statuses), - tags: parseAsStringArray(query.tags), - types: parseAsStringArray(query.types) + statuses: parseAsArray(query.statuses, parseAsString), + tags: parseAsArray(query.tags, parseAsString), + types: parseAsArray(query.types, parseAsString) }); export const getOpen = (query: RawQuery) => query.open; export const areMyIssuesSelected = (query: RawQuery): boolean => query.myIssues === 'true'; -const serializeString = (value: string): ?string => value || undefined; - -const serializeValue = (value: Array<string>): ?string => (value.length ? value.join() : undefined); - export const serializeQuery = (query: Query): RawQuery => { const filter = { assigned: query.assigned ? undefined : 'false', - assignees: serializeValue(query.assignees), - authors: serializeValue(query.authors), + assignees: serializeStringArray(query.assignees), + authors: serializeStringArray(query.authors), createdAfter: serializeString(query.createdAfter), createdAt: serializeString(query.createdAt), createdBefore: serializeString(query.createdBefore), createdInLast: serializeString(query.createdInLast), - directories: serializeValue(query.directories), + directories: serializeStringArray(query.directories), facetMode: query.facetMode === 'effort' ? serializeString(query.facetMode) : undefined, - fileUuids: serializeValue(query.files), - issues: serializeValue(query.issues), - languages: serializeValue(query.languages), - moduleUuids: serializeValue(query.modules), - projectUuids: serializeValue(query.projects), + fileUuids: serializeStringArray(query.files), + issues: serializeStringArray(query.issues), + languages: serializeStringArray(query.languages), + moduleUuids: serializeStringArray(query.modules), + projectUuids: serializeStringArray(query.projects), resolved: query.resolved ? undefined : 'false', - resolutions: serializeValue(query.resolutions), + resolutions: serializeStringArray(query.resolutions), s: serializeString(query.sort), - severities: serializeValue(query.severities), + severities: serializeStringArray(query.severities), sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined, - statuses: serializeValue(query.statuses), - rules: serializeValue(query.rules), - tags: serializeValue(query.tags), - types: serializeValue(query.types) + statuses: serializeStringArray(query.statuses), + rules: serializeStringArray(query.rules), + tags: serializeStringArray(query.tags), + types: serializeStringArray(query.types) }; - return omitBy(filter, isNil); -}; - -const areArraysEqual = (a: Array<string>, b: Array<string>) => { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - return true; + return cleanQuery(filter); }; -export const areQueriesEqual = (a: RawQuery, b: RawQuery) => { - const parsedA: Query = parseQuery(a); - const parsedB: Query = parseQuery(b); - - const keysA = Object.keys(parsedA); - const keysB = Object.keys(parsedB); - - if (keysA.length !== keysB.length) { - return false; - } - - return keysA.every( - key => - (Array.isArray(parsedA[key]) && Array.isArray(parsedB[key]) - ? areArraysEqual(parsedA[key], parsedB[key]) - : parsedA[key] === parsedB[key]) - ); -}; +export const areQueriesEqual = (a: RawQuery, b: RawQuery) => + queriesEqual(parseQuery(a), parseQuery(b)); type RawFacet = { property: string, diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js index cd4ef7c68e4..6500ae188be 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js @@ -20,26 +20,27 @@ // @flow import React from 'react'; import { Link } from 'react-router'; -import { connect } from 'react-redux'; import Analysis from './Analysis'; +import throwGlobalError from '../../../app/utils/throwGlobalError'; +import { getProjectActivity } from '../../../api/projectActivity'; import { translate } from '../../../helpers/l10n'; -import { fetchRecentProjectActivity } from '../actions'; -import { getProjectActivity } from '../../../store/rootReducer'; -import { getAnalyses } from '../../../store/projectActivity/duck'; +import type { Analysis as AnalysisType } from '../../projectActivity/types'; type Props = { - analyses?: Array<*>, - project: string, - fetchRecentProjectActivity: (project: string) => Promise<*> + project: string }; -class AnalysesList extends React.PureComponent { +type State = { + analyses: Array<AnalysisType>, + loading: boolean +}; + +const PAGE_SIZE = 5; + +export default class AnalysesList extends React.PureComponent { mounted: boolean; props: Props; - - state = { - loading: true - }; + state: State = { analyses: [], loading: true }; componentDidMount() { this.mounted = true; @@ -58,14 +59,16 @@ class AnalysesList extends React.PureComponent { fetchData() { this.setState({ loading: true }); - this.props.fetchRecentProjectActivity(this.props.project).then(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); + getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }) + .then(({ analyses }) => { + if (this.mounted) { + this.setState({ analyses, loading: false }); + } + }) + .catch(throwGlobalError); } - renderList(analyses) { + renderList(analyses: Array<AnalysisType>) { if (!analyses.length) { return ( <p className="spacer-top note"> @@ -82,10 +85,9 @@ class AnalysesList extends React.PureComponent { } render() { - const { analyses } = this.props; - const { loading } = this.state; + const { analyses, loading } = this.state; - if (loading || !analyses) { + if (loading) { return null; } @@ -106,11 +108,3 @@ class AnalysesList extends React.PureComponent { ); } } - -const mapStateToProps = (state, ownProps: Props) => ({ - analyses: getAnalyses(getProjectActivity(state), ownProps.project) -}); - -const mapDispatchToProps = { fetchRecentProjectActivity }; - -export default connect(mapStateToProps, mapDispatchToProps)(AnalysesList); diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js index 582b64b64a4..959338bb2df 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js @@ -22,7 +22,7 @@ import Events from '../../projectActivity/components/Events'; import FormattedDate from '../../../components/ui/FormattedDate'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { translate } from '../../../helpers/l10n'; -import type { Analysis as AnalysisType } from '../../../store/projectActivity/duck'; +import type { Analysis as AnalysisType } from '../../projectActivity/types'; export default function Analysis(props: { analysis: AnalysisType }) { const { analysis } = props; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/actions-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/actions-test.js.snap new file mode 100644 index 00000000000..2e1d7d96c09 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/actions-test.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addCustomEvent should correctly add a custom event 1`] = ` +Object { + "date": "2016-10-27T12:21:15+0200", + "events": Array [ + Object { + "category": "Custom", + "key": "Enew", + "name": "Foo", + }, + ], + "key": "A2", +} +`; + +exports[`changeEvent should correctly update an event 1`] = ` +Object { + "date": "2016-10-27T16:33:50+0200", + "events": Array [ + Object { + "category": "VERSION", + "key": "E1", + "name": "changed", + }, + ], + "key": "A1", +} +`; + +exports[`deleteAnalysis should correctly delete an analyses 1`] = ` +Array [ + Object { + "date": "2016-10-27T12:21:15+0200", + "events": Array [], + "key": "A2", + }, + Object { + "date": "2016-10-26T12:17:29+0200", + "events": Array [ + Object { + "category": "OTHER", + "key": "E2", + "name": "foo", + }, + Object { + "category": "OTHER", + "key": "E3", + "name": "foo", + }, + ], + "key": "A3", + }, +] +`; + +exports[`deleteEvent should correctly remove an event 1`] = ` +Object { + "date": "2016-10-27T16:33:50+0200", + "events": Array [], + "key": "A1", +} +`; + +exports[`deleteEvent should correctly remove an event 2`] = ` +Object { + "date": "2016-10-27T12:21:15+0200", + "events": Array [], + "key": "A2", +} +`; + +exports[`deleteEvent should correctly remove an event 3`] = ` +Object { + "date": "2016-10-26T12:17:29+0200", + "events": Array [ + Object { + "category": "OTHER", + "key": "E3", + "name": "foo", + }, + ], + "key": "A3", +} +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js new file mode 100644 index 00000000000..896a172c9a4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 actions from '../actions'; + +const ANALYSES = [ + { + key: 'A1', + date: '2016-10-27T16:33:50+0200', + events: [ + { + key: 'E1', + category: 'VERSION', + name: '6.5-SNAPSHOT' + } + ] + }, + { + key: 'A2', + date: '2016-10-27T12:21:15+0200', + events: [] + }, + { + key: 'A3', + date: '2016-10-26T12:17:29+0200', + events: [ + { + key: 'E2', + category: 'OTHER', + name: 'foo' + }, + { + key: 'E3', + category: 'OTHER', + name: 'foo' + } + ] + } +]; + +const newEvent = { + key: 'Enew', + name: 'Foo', + category: 'Custom' +}; + +it('should never throw when there is no analyses', () => { + expect(actions.addCustomEvent('A1', newEvent)({})).toBeUndefined(); + expect(actions.deleteEvent('A1', newEvent)({})).toBeUndefined(); + expect(actions.changeEvent('A1', newEvent)({})).toBeUndefined(); + expect(actions.deleteAnalysis('Anew')({})).toBeUndefined(); +}); + +describe('addCustomEvent', () => { + it('should correctly add a custom event', () => { + expect( + actions.addCustomEvent('A2', newEvent)({ analyses: ANALYSES }).analyses[1] + ).toMatchSnapshot(); + expect( + actions.addCustomEvent('A1', newEvent)({ analyses: ANALYSES }).analyses[0].events + ).toContain(newEvent); + }); +}); + +describe('deleteEvent', () => { + it('should correctly remove an event', () => { + expect(actions.deleteEvent('A1', 'E1')({ analyses: ANALYSES }).analyses[0]).toMatchSnapshot(); + expect(actions.deleteEvent('A2', 'E1')({ analyses: ANALYSES }).analyses[1]).toMatchSnapshot(); + expect(actions.deleteEvent('A3', 'E2')({ analyses: ANALYSES }).analyses[2]).toMatchSnapshot(); + }); +}); + +describe('changeEvent', () => { + it('should correctly update an event', () => { + expect( + actions.changeEvent('A1', { key: 'E1', name: 'changed' })({ analyses: ANALYSES }).analyses[0] + ).toMatchSnapshot(); + expect( + actions.changeEvent('A2', { key: 'E2' })({ analyses: ANALYSES }).analyses[1].events + ).toHaveLength(0); + }); +}); + +describe('deleteAnalysis', () => { + it('should correctly delete an analyses', () => { + expect(actions.deleteAnalysis('A1')({ analyses: ANALYSES }).analyses).toMatchSnapshot(); + expect(actions.deleteAnalysis('A5')({ analyses: ANALYSES }).analyses).toHaveLength(3); + expect(actions.deleteAnalysis('A2')({ analyses: ANALYSES }).analyses).toHaveLength(2); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/actions.js b/server/sonar-web/src/main/js/apps/projectActivity/actions.js index 60a2dbcb480..e4e36ff0f85 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/actions.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/actions.js @@ -18,81 +18,44 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import * as api from '../../api/projectActivity'; -import { - receiveProjectActivity, - addEvent, - deleteEvent as deleteEventAction, - changeEvent as changeEventAction, - deleteAnalysis as deleteAnalysisAction, - getPaging -} from '../../store/projectActivity/duck'; -import { onFail } from '../../store/rootActions'; -import { getProjectActivity } from '../../store/rootReducer'; - -const rejectOnFail = (dispatch: Function) => (error: Object) => { - onFail(dispatch)(error); - return Promise.reject(); -}; - -export const fetchProjectActivity = (project: string, filter: ?string) => ( - dispatch: Function -): void => { - api - .getProjectActivity(project, { category: filter }) - .then( - ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)), - onFail(dispatch) - ); -}; - -export const fetchMoreProjectActivity = (project: string, filter: ?string) => ( - dispatch: Function, - getState: Function -): void => { - const projectActivity = getProjectActivity(getState()); - const { pageIndex } = getPaging(projectActivity, project); - - api - .getProjectActivity(project, { category: filter, pageIndex: pageIndex + 1 }) - .then( - ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)), - onFail(dispatch) - ); -}; - -export const addCustomEvent = (analysis: string, name: string, category?: string) => ( - dispatch: Function -): Promise<*> => { - return api - .createEvent(analysis, name, category) - .then(({ analysis, ...event }) => dispatch(addEvent(analysis, event)), rejectOnFail(dispatch)); -}; - -export const deleteEvent = (analysis: string, event: string) => ( - dispatch: Function -): Promise<*> => { - return api - .deleteEvent(event) - .then(() => dispatch(deleteEventAction(analysis, event)), rejectOnFail(dispatch)); -}; - -export const addVersion = (analysis: string, version: string) => ( - dispatch: Function -): Promise<*> => { - return dispatch(addCustomEvent(analysis, version, 'VERSION')); -}; - -export const changeEvent = (event: string, name: string) => (dispatch: Function): Promise<*> => { - return api - .changeEvent(event, name) - .then(() => dispatch(changeEventAction(event, { name })), rejectOnFail(dispatch)); -}; - -export const deleteAnalysis = (project: string, analysis: string) => ( - dispatch: Function -): Promise<*> => { - return api - .deleteAnalysis(analysis) - .then(() => dispatch(deleteAnalysisAction(project, analysis)), rejectOnFail(dispatch)); -}; +import type { Event } from './types'; +import type { State } from './components/ProjectActivityApp'; + +export const addCustomEvent = (analysis: string, event: Event) => (state: State) => ({ + analyses: state.analyses.map(item => { + if (item.key !== analysis) { + return item; + } + return { ...item, events: [...item.events, event] }; + }) +}); + +export const deleteEvent = (analysis: string, event: string) => (state: State) => ({ + analyses: state.analyses.map(item => { + if (item.key !== analysis) { + return item; + } + return { + ...item, + events: item.events.filter(eventItem => eventItem.key !== event) + }; + }) +}); + +export const changeEvent = (analysis: string, event: Event) => (state: State) => ({ + analyses: state.analyses.map(item => { + if (item.key !== analysis) { + return item; + } + return { + ...item, + events: item.events.map( + eventItem => (eventItem.key === event.key ? { ...eventItem, ...event } : eventItem) + ) + }; + }) +}); + +export const deleteAnalysis = (analysis: string) => (state: State) => ({ + analyses: state.analyses.filter(item => item.key !== analysis) +}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js index 6697449dc5d..2dc04fec94c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js @@ -20,17 +20,19 @@ // @flow import React from 'react'; import EventInner from './EventInner'; -import ChangeCustomEventForm from './forms/ChangeCustomEventForm'; -import RemoveCustomEventForm from './forms/RemoveCustomEventForm'; +import ChangeEventForm from './forms/ChangeEventForm'; +import RemoveEventForm from './forms/RemoveEventForm'; import DeleteIcon from './DeleteIcon'; import ChangeIcon from './ChangeIcon'; -import type { Event as EventType } from '../../../store/projectActivity/duck'; +import type { Event as EventType } from '../types'; type Props = { analysis: string, + canAdmin: boolean, + changeEvent: (event: string, name: string) => Promise<*>, + deleteEvent: (analysis: string, event: string) => Promise<*>, event: EventType, - isFirst: boolean, - canAdmin: boolean + isFirst: boolean }; type State = { @@ -41,7 +43,6 @@ type State = { export default class Event extends React.PureComponent { mounted: boolean; props: Props; - state: State = { changing: false, deleting: false @@ -77,9 +78,10 @@ export default class Event extends React.PureComponent { render() { const { event, canAdmin } = this.props; - const canChange = ['OTHER', 'VERSION'].includes(event.category); - const canDelete = - event.category === 'OTHER' || (event.category === 'VERSION' && !this.props.isFirst); + const isOther = event.category === 'OTHER'; + const isVersion = !isOther && event.category === 'VERSION'; + const canChange = isOther || isVersion; + const canDelete = isOther || (isVersion && !this.props.isFirst); const showActions = canAdmin && (canChange || canDelete); return ( @@ -99,13 +101,29 @@ export default class Event extends React.PureComponent { </div>} {this.state.changing && - <ChangeCustomEventForm event={this.props.event} onClose={this.stopChanging} />} + <ChangeEventForm + changeEventButtonText={ + 'project_activity.' + (isVersion ? 'change_version' : 'change_custom_event') + } + changeEvent={this.props.changeEvent} + event={this.props.event} + onClose={this.stopChanging} + />} {this.state.deleting && - <RemoveCustomEventForm + <RemoveEventForm analysis={this.props.analysis} + deleteEvent={this.props.deleteEvent} event={this.props.event} onClose={this.stopDeleting} + removeEventButtonText={ + 'project_activity.' + (isVersion ? 'remove_version' : 'remove_custom_event') + } + removeEventQuestion={ + 'project_activity.' + + (isVersion ? 'remove_version' : 'remove_custom_event') + + '.question' + } />} </div> ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js index 4135b322fda..62bc0e7e229 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; -import type { Event as EventType } from '../../../store/projectActivity/duck'; +import type { Event as EventType } from '../types'; import { translate } from '../../../helpers/l10n'; import './Event.css'; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js index 36a931422a5..27678c0a9ac 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js @@ -21,13 +21,15 @@ import React from 'react'; import { sortBy } from 'lodash'; import Event from './Event'; -import type { Event as EventType } from '../../../store/projectActivity/duck'; +import type { Event as EventType } from '../types'; type Props = { analysis: string, + canAdmin: boolean, + changeEvent: (event: string, name: string) => Promise<*>, + deleteEvent: (analysis: string, event: string) => Promise<*>, events: Array<EventType>, - isFirst: boolean, - canAdmin: boolean + isFirst: boolean }; export default function Events(props: Props) { @@ -43,11 +45,13 @@ export default function Events(props: Props) { <div className="project-activity-events"> {sortedEvents.map(event => ( <Event - key={event.key} analysis={props.analysis} + canAdmin={props.canAdmin} + changeEvent={props.changeEvent} + deleteEvent={props.deleteEvent} event={event} isFirst={props.isFirst} - canAdmin={props.canAdmin} + key={event.key} /> ))} </div> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js index 6e95edb3b25..96ab8cb14ad 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js @@ -19,27 +19,24 @@ */ // @flow import React from 'react'; -import { connect } from 'react-redux'; import { groupBy } from 'lodash'; import moment from 'moment'; import ProjectActivityAnalysis from './ProjectActivityAnalysis'; import FormattedDate from '../../../components/ui/FormattedDate'; -import { getProjectActivity } from '../../../store/rootReducer'; -import { getAnalyses } from '../../../store/projectActivity/duck'; import { translate } from '../../../helpers/l10n'; -import type { Analysis } from '../../../store/projectActivity/duck'; +import type { Analysis } from '../types'; type Props = { - project: string, - analyses?: Array<Analysis>, - canAdmin: boolean + addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>, + addVersion: (analysis: string, version: string) => Promise<*>, + analyses: Array<Analysis>, + canAdmin: boolean, + changeEvent: (event: string, name: string) => Promise<*>, + deleteAnalysis: (analysis: string) => Promise<*>, + deleteEvent: (analysis: string, event: string) => Promise<*> }; -function ProjectActivityAnalysesList(props: Props) { - if (!props.analyses) { - return null; - } - +export default function ProjectActivityAnalysesList(props: Props) { if (props.analyses.length === 0) { return <div className="note">{translate('no_results')}</div>; } @@ -64,11 +61,15 @@ function ProjectActivityAnalysesList(props: Props) { {byDay[day] != null && byDay[day].map(analysis => ( <ProjectActivityAnalysis - key={analysis.key} + addCustomEvent={props.addCustomEvent} + addVersion={props.addVersion} analysis={analysis} - isFirst={analysis === firstAnalysis} - project={props.project} canAdmin={props.canAdmin} + changeEvent={props.changeEvent} + deleteAnalysis={props.deleteAnalysis} + deleteEvent={props.deleteEvent} + isFirst={analysis === firstAnalysis} + key={analysis.key} /> ))} </ul> @@ -78,9 +79,3 @@ function ProjectActivityAnalysesList(props: Props) { </div> ); } - -const mapStateToProps = (state, ownProps) => ({ - analyses: getAnalyses(getProjectActivity(state), ownProps.project) -}); - -export default connect(mapStateToProps)(ProjectActivityAnalysesList); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js index 0774d09abb7..01f933beadc 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -20,17 +20,20 @@ // @flow import React from 'react'; import Events from './Events'; -import AddVersionForm from './forms/AddVersionForm'; -import AddCustomEventForm from './forms/AddCustomEventForm'; +import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; import FormattedDate from '../../../components/ui/FormattedDate'; -import type { Analysis } from '../../../store/projectActivity/duck'; import { translate } from '../../../helpers/l10n'; +import type { Analysis } from '../types'; type Props = { + addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>, + addVersion: (analysis: string, version: string) => Promise<*>, analysis: Analysis, + changeEvent: (event: string, name: string) => Promise<*>, + deleteAnalysis: (analysis: string) => Promise<*>, + deleteEvent: (analysis: string, event: string) => Promise<*>, isFirst: boolean, - project: string, canAdmin: boolean }; @@ -51,17 +54,25 @@ export default function ProjectActivityAnalysis(props: Props) { <ul className="dropdown-menu dropdown-menu-right"> {version == null && <li> - <AddVersionForm analysis={props.analysis} /> + <AddEventForm + addEvent={props.addVersion} + analysis={props.analysis} + addEventButtonText="project_activity.add_version" + /> </li>} <li> - <AddCustomEventForm analysis={props.analysis} /> + <AddEventForm + addEvent={props.addCustomEvent} + analysis={props.analysis} + addEventButtonText="project_activity.add_custom_event" + /> </li> </ul> </div> {!isFirst && <div className="display-inline-block little-spacer-left"> - <RemoveAnalysisForm analysis={props.analysis} project={props.project} /> + <RemoveAnalysisForm analysis={props.analysis} deleteAnalysis={props.deleteAnalysis} /> </div>} </div>} @@ -72,9 +83,11 @@ export default function ProjectActivityAnalysis(props: Props) { {events.length > 0 && <Events analysis={props.analysis.key} + canAdmin={canAdmin} + changeEvent={props.changeEvent} + deleteEvent={props.deleteEvent} events={events} isFirst={props.isFirst} - canAdmin={canAdmin} />} </li> ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index fd9ac67fc94..7289766ece9 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -20,54 +20,151 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; -import { connect } from 'react-redux'; import ProjectActivityPageHeader from './ProjectActivityPageHeader'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; import ProjectActivityPageFooter from './ProjectActivityPageFooter'; -import { fetchProjectActivity } from '../actions'; -import { getComponent } from '../../../store/rootReducer'; +import throwGlobalError from '../../../app/utils/throwGlobalError'; +import * as api from '../../../api/projectActivity'; +import * as actions from '../actions'; +import { parseQuery, serializeQuery, serializeUrlQuery } from '../utils'; import { translate } from '../../../helpers/l10n'; import './projectActivity.css'; +import type { Analysis, Query, Paging } from '../types'; +import type { RawQuery } from '../../../helpers/query'; type Props = { - location: { query: { id: string } }, - fetchProjectActivity: (project: string, filter: ?string) => void, - project: { configuration?: { showHistory: boolean } } + location: { pathname: string, query: RawQuery }, + project: { configuration?: { showHistory: boolean }, key: string }, + router: { push: ({ pathname: string, query?: RawQuery }) => void } }; -type State = { - filter: ?string +export type State = { + analyses: Array<Analysis>, + loading: boolean, + paging?: Paging, + query: Query }; -class ProjectActivityApp extends React.PureComponent { +export default class ProjectActivityApp extends React.PureComponent { + mounted: boolean; props: Props; + state: State; - state: State = { - filter: null - }; + constructor(props: Props) { + super(props); + this.state = { analyses: [], loading: true, query: parseQuery(props.location.query) }; + } componentDidMount() { - const html = document.querySelector('html'); - if (html) { - html.classList.add('dashboard-page'); + this.mounted = true; + this.handleQueryChange(); + const elem = document.querySelector('html'); + elem && elem.classList.add('dashboard-page'); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.location.query !== this.props.location.query) { + this.handleQueryChange(); } - this.props.fetchProjectActivity(this.props.location.query.id); } componentWillUnmount() { - const html = document.querySelector('html'); - if (html) { - html.classList.remove('dashboard-page'); + this.mounted = false; + const elem = document.querySelector('html'); + elem && elem.classList.remove('dashboard-page'); + } + + fetchActivity = ( + query: Query, + additional?: {} + ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => { + const parameters = { + ...serializeQuery(query), + ...additional + }; + return api.getProjectActivity(parameters).catch(throwGlobalError); + }; + + fetchMoreActivity = () => { + const { paging, query } = this.state; + if (!paging) { + return; } + + this.setState({ loading: true }); + this.fetchActivity(query, { p: paging.pageIndex + 1 }).then(({ analyses, paging }) => { + if (this.mounted) { + this.setState((state: State) => ({ + analyses: state.analyses ? state.analyses.concat(analyses) : analyses, + loading: false, + paging + })); + } + }); + }; + + addCustomEvent = (analysis: string, name: string, category?: string): Promise<*> => + api + .createEvent(analysis, name, category) + .then( + ({ analysis, ...event }) => + this.mounted && this.setState(actions.addCustomEvent(analysis, event)) + ) + .catch(throwGlobalError); + + addVersion = (analysis: string, version: string): Promise<*> => + this.addCustomEvent(analysis, version, 'VERSION'); + + deleteEvent = (analysis: string, event: string): Promise<*> => + api + .deleteEvent(event) + .then(() => this.mounted && this.setState(actions.deleteEvent(analysis, event))) + .catch(throwGlobalError); + + changeEvent = (event: string, name: string): Promise<*> => + api + .changeEvent(event, name) + .then( + ({ analysis, ...event }) => + this.mounted && this.setState(actions.changeEvent(analysis, event)) + ) + .catch(throwGlobalError); + + deleteAnalysis = (analysis: string): Promise<*> => + api + .deleteAnalysis(analysis) + .then(() => this.mounted && this.setState(actions.deleteAnalysis(analysis))) + .catch(throwGlobalError); + + handleQueryChange() { + const query = parseQuery(this.props.location.query); + this.setState({ loading: true, query }); + this.fetchActivity(query).then(({ analyses, paging }) => { + if (this.mounted) { + this.setState({ + analyses, + loading: false, + paging + }); + } + }); } - handleFilter = (filter: ?string) => { - this.setState({ filter }); - this.props.fetchProjectActivity(this.props.location.query.id, filter); + updateQuery = (newQuery: Query) => { + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...serializeUrlQuery({ + ...this.state.query, + ...newQuery + }), + id: this.props.project.key + } + }); }; render() { - const project = this.props.location.query.id; + const { query } = this.state; const { configuration } = this.props.project; const canAdmin = configuration ? configuration.showHistory : false; @@ -75,24 +172,24 @@ class ProjectActivityApp extends React.PureComponent { <div id="project-activity" className="page page-limited"> <Helmet title={translate('project_activity.page')} /> - <ProjectActivityPageHeader - project={project} - filter={this.state.filter} - changeFilter={this.handleFilter} - /> + <ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} /> - <ProjectActivityAnalysesList project={project} canAdmin={canAdmin} /> + <ProjectActivityAnalysesList + addCustomEvent={this.addCustomEvent} + addVersion={this.addVersion} + analyses={this.state.analyses} + canAdmin={canAdmin} + changeEvent={this.changeEvent} + deleteAnalysis={this.deleteAnalysis} + deleteEvent={this.deleteEvent} + /> - <ProjectActivityPageFooter project={project} /> + <ProjectActivityPageFooter + analyses={this.state.analyses} + fetchMoreActivity={this.fetchMoreActivity} + paging={this.state.paging} + /> </div> ); } } - -const mapStateToProps = (state, ownProps: Props) => ({ - project: getComponent(state, ownProps.location.query.id) -}); - -const mapDispatchToProps = { fetchProjectActivity }; - -export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityApp); diff --git a/server/sonar-web/src/main/js/apps/overview/actions.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 892cea5e498..9ed72083692 100644 --- a/server/sonar-web/src/main/js/apps/overview/actions.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -18,16 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import * as api from '../../api/projectActivity'; -import { receiveProjectActivity } from '../../store/projectActivity/duck'; -import { onFail } from '../../store/rootActions'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import ProjectActivityApp from './ProjectActivityApp'; +import { getComponent } from '../../../store/rootReducer'; -const PAGE_SIZE = 5; +const mapStateToProps = (state, ownProps) => ({ + project: getComponent(state, ownProps.location.query.id) +}); -export const fetchRecentProjectActivity = (project: string) => (dispatch: Function) => - api - .getProjectActivity(project, { pageSize: PAGE_SIZE }) - .then( - ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)), - onFail(dispatch) - ); +export default connect(mapStateToProps)(withRouter(ProjectActivityApp)); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js index 4bf4d21b3ac..77428e38105 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js @@ -19,43 +19,18 @@ */ // @flow import React from 'react'; -import { connect } from 'react-redux'; import ListFooter from '../../../components/controls/ListFooter'; -import { getProjectActivity } from '../../../store/rootReducer'; -import { getAnalyses, getPaging } from '../../../store/projectActivity/duck'; -import { fetchMoreProjectActivity } from '../actions'; -import type { Paging } from '../../../store/projectActivity/duck'; +import type { Paging } from '../types'; -class ProjectActivityPageFooter extends React.PureComponent { - props: { - analyses: Array<*>, - paging: ?Paging, - project: string, - fetchMoreProjectActivity: (project: string) => void - }; +type Props = { + analyses: Array<*>, + fetchMoreActivity: () => void, + paging?: Paging +}; - handleLoadMore = () => { - this.props.fetchMoreProjectActivity(this.props.project); - }; - - render() { - const { analyses, paging } = this.props; - - if (!paging || analyses.length === 0) { - return null; - } - - return ( - <ListFooter count={analyses.length} total={paging.total} loadMore={this.handleLoadMore} /> - ); +export default function ProjectActivityPageFooter({ analyses, fetchMoreActivity, paging }: Props) { + if (!paging || analyses.length === 0) { + return null; } + return <ListFooter count={analyses.length} total={paging.total} loadMore={fetchMoreActivity} />; } - -const mapStateToProps = (state, ownProps) => ({ - analyses: getAnalyses(getProjectActivity(state), ownProps.project), - paging: getPaging(getProjectActivity(state), ownProps.project) -}); - -const mapDispatchToProps = { fetchMoreProjectActivity }; - -export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityPageFooter); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js index 624559acd4c..8dd16cb5045 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js @@ -21,18 +21,20 @@ import React from 'react'; import Select from 'react-select'; import { translate } from '../../../helpers/l10n'; +import type { RawQuery } from '../../../helpers/query'; type Props = { - changeFilter: (filter: ?string) => void, - filter: ?string + updateQuery: RawQuery => void, + category?: string }; export default class ProjectActivityPageHeader extends React.PureComponent { props: Props; - handleChange = (option: null | { value: string }) => { - this.props.changeFilter(option && option.value); + handleCategoryChange = (option: ?{ value: string }) => { + this.props.updateQuery({ category: option ? option.value : '' }); }; + render() { const selectOptions = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'].map(category => ({ label: translate('event.category', category), @@ -47,9 +49,9 @@ export default class ProjectActivityPageHeader extends React.PureComponent { placeholder={translate('filter_verb') + '...'} clearable={true} searchable={false} - value={this.props.filter} + value={this.props.category} options={selectOptions} - onChange={this.handleChange} + onChange={this.handleCategoryChange} /> </div> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js deleted file mode 100644 index 9ff07425850..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import { addCustomEvent } from '../../actions'; -import AddEventForm from './AddEventForm'; -import type { Analysis } from '../../../../store/projectActivity/duck'; - -type Props = { - addEvent: (analysis: string, name: string, category?: string) => Promise<*>, - analysis: Analysis -}; - -function AddCustomEventForm(props: Props) { - return <AddEventForm {...props} addEventButtonText="project_activity.add_custom_event" />; -} - -const mapStateToProps = null; - -const mapDispatchToProps = { addEvent: addCustomEvent }; - -export default connect(mapStateToProps, mapDispatchToProps)(AddCustomEventForm); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js index 759a60313df..d83b4244d6a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js @@ -20,8 +20,8 @@ // @flow import React from 'react'; import Modal from 'react-modal'; -import type { Analysis } from '../../../../store/projectActivity/duck'; import { translate } from '../../../../helpers/l10n'; +import type { Analysis } from '../../types'; type Props = { addEvent: (analysis: string, name: string, category?: string) => Promise<*>, @@ -38,7 +38,6 @@ type State = { export default class AddEventForm extends React.PureComponent { mounted: boolean; props: Props; - state: State = { open: false, processing: false, @@ -131,7 +130,6 @@ export default class AddEventForm extends React.PureComponent { </div>} </footer> </form> - </Modal> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js deleted file mode 100644 index a415a6ec9e6..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import { addVersion } from '../../actions'; -import AddEventForm from './AddEventForm'; -import type { Analysis } from '../../../../store/projectActivity/duck'; - -type Props = { - addEvent: (analysis: string, version: string) => Promise<*>, - analysis: Analysis -}; - -function AddVersionForm(props: Props) { - return <AddEventForm {...props} addEventButtonText="project_activity.add_version" />; -} - -const mapStateToProps = null; - -const mapDispatchToProps = { addEvent: addVersion }; - -export default connect(mapStateToProps, mapDispatchToProps)(AddVersionForm); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js deleted file mode 100644 index 0cbb7d0d6f5..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import ChangeEventForm from './ChangeEventForm'; -import { changeEvent } from '../../actions'; -import type { Event } from '../../../../store/projectActivity/duck'; - -type Props = { - changeEvent: (event: string, name: string) => Promise<*>, - event: Event, - onClose: () => void -}; - -const ChangeCustomEventForm = (props: Props) => ( - <ChangeEventForm {...props} changeEventButtonText="project_activity.change_custom_event" /> -); - -const mapStateToProps = null; - -const mapDispatchToProps = { changeEvent }; - -export default connect(mapStateToProps, mapDispatchToProps)(ChangeCustomEventForm); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js index 240ad559c06..6a091dcae76 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js @@ -20,8 +20,8 @@ // @flow import React from 'react'; import Modal from 'react-modal'; -import type { Event } from '../../../../store/projectActivity/duck'; import { translate } from '../../../../helpers/l10n'; +import type { Event } from '../../types'; type Props = { changeEvent: (event: string, name: string) => Promise<*>, @@ -129,7 +129,6 @@ export default class ChangeEventForm extends React.PureComponent { </div>} </footer> </form> - </Modal> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js index ec3676d3ed8..9b0e215c686 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js @@ -19,16 +19,13 @@ */ // @flow import React from 'react'; -import { connect } from 'react-redux'; import Modal from 'react-modal'; -import type { Analysis } from '../../../../store/projectActivity/duck'; import { translate } from '../../../../helpers/l10n'; -import { deleteAnalysis } from '../../actions'; +import type { Analysis } from '../../types'; type Props = { analysis: Analysis, - deleteAnalysis: (project: string, analysis: string) => Promise<*>, - project: string + deleteAnalysis: (analysis: string) => Promise<*> }; type State = { @@ -36,10 +33,9 @@ type State = { processing: boolean }; -class RemoveAnalysisForm extends React.PureComponent { +export default class RemoveAnalysisForm extends React.PureComponent { mounted: boolean; props: Props; - state: State = { open: false, processing: false @@ -81,7 +77,7 @@ class RemoveAnalysisForm extends React.PureComponent { e.preventDefault(); this.setState({ processing: true }); this.props - .deleteAnalysis(this.props.project, this.props.analysis.key) + .deleteAnalysis(this.props.analysis.key) .then(this.stopProcessingAndClose, this.stopProcessing); }; @@ -128,9 +124,3 @@ class RemoveAnalysisForm extends React.PureComponent { ); } } - -const mapStateToProps = null; - -const mapDispatchToProps = { deleteAnalysis }; - -export default connect(mapStateToProps, mapDispatchToProps)(RemoveAnalysisForm); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js deleted file mode 100644 index 6223957e45a..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import RemoveEventForm from './RemoveEventForm'; -import { deleteEvent } from '../../actions'; -import type { Event } from '../../../../store/projectActivity/duck'; - -type Props = { - analysis: string, - event: Event, - deleteEvent: (analysis: string, event: string) => Promise<*>, - onClose: () => void -}; - -function RemoveCustomEventForm(props: Props) { - return ( - <RemoveEventForm - {...props} - removeEventButtonText="project_activity.remove_custom_event" - removeEventQuestion="project_activity.remove_custom_event.question" - /> - ); -} - -const mapStateToProps = null; - -const mapDispatchToProps = { deleteEvent }; - -export default connect(mapStateToProps, mapDispatchToProps)(RemoveCustomEventForm); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js index e17ed059e85..b23522db21c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js @@ -20,8 +20,8 @@ // @flow import React from 'react'; import Modal from 'react-modal'; -import type { Event } from '../../../../store/projectActivity/duck'; import { translate } from '../../../../helpers/l10n'; +import type { Event } from '../../types'; type Props = { analysis: string, @@ -39,7 +39,6 @@ type State = { export default class RemoveEventForm extends React.PureComponent { mounted: boolean; props: Props; - state: State = { processing: false }; @@ -108,7 +107,6 @@ export default class RemoveEventForm extends React.PureComponent { </div>} </footer> </form> - </Modal> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js deleted file mode 100644 index 3b0459170b8..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import RemoveEventForm from './RemoveEventForm'; -import { deleteEvent } from '../../actions'; -import type { Event } from '../../../../store/projectActivity/duck'; - -type Props = { - analysis: string, - event: Event, - deleteEvent: (analysis: string, event: string) => Promise<*>, - onClose: () => void -}; - -function RemoveVersionForm(props: Props) { - return ( - <RemoveEventForm - {...props} - removeEventButtonText="project_activity.remove_version" - removeEventQuestion="project_activity.remove_version.question" - /> - ); -} - -const mapStateToProps = null; - -const mapDispatchToProps = { deleteEvent }; - -export default connect(mapStateToProps, mapDispatchToProps)(RemoveVersionForm); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.js b/server/sonar-web/src/main/js/apps/projectActivity/routes.js index 3c5a57d10ec..e11d7fbe8b4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/routes.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/routes.js @@ -21,7 +21,7 @@ const routes = [ { getIndexRoute(_, callback) { require.ensure([], require => - callback(null, { component: require('./components/ProjectActivityApp').default }) + callback(null, { component: require('./components/ProjectActivityAppContainer').default }) ); } } diff --git a/server/sonar-web/src/main/js/store/projectActivity/paging.js b/server/sonar-web/src/main/js/apps/projectActivity/types.js index bf14686fa9c..b3d8211dfc8 100644 --- a/server/sonar-web/src/main/js/store/projectActivity/paging.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/types.js @@ -18,16 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import type { Paging, ReceiveProjectActivityAction } from './duck'; -export type State = { - [key: string]: Paging +export type Event = { + key: string, + name: string, + category: string, + description?: string }; -export default (state: State = {}, action: ReceiveProjectActivityAction): State => { - if (action.type === 'RECEIVE_PROJECT_ACTIVITY') { - return { ...state, [action.project]: action.paging }; - } +export type Analysis = { + key: string, + date: string, + events: Array<Event> +}; + +export type Paging = { + pageIndex: number, + pageSize: number, + total: number +}; - return state; +export type Query = { + project: string, + category: string }; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index 8eb2192cafc..be1646db137 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -18,24 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import React from 'react'; -import { connect } from 'react-redux'; -import ChangeEventForm from './ChangeEventForm'; -import { changeEvent } from '../../actions'; -import type { Event } from '../../../../store/projectActivity/duck'; +import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; +import type { Query } from './types'; +import type { RawQuery } from '../../helpers/query'; -type Props = { - changeEvent: (event: string, name: string) => Promise<*>, - event: Event, - onClose: () => void -}; +export const parseQuery = (urlQuery: RawQuery): Query => ({ + project: parseAsString(urlQuery['id']), + category: parseAsString(urlQuery['category']) +}); -function ChangeVersionForm(props: Props) { - return <ChangeEventForm {...props} changeEventButtonText="project_activity.change_version" />; -} +export const serializeQuery = (query: Query): Query => + cleanQuery({ + project: serializeString(query.project), + category: serializeString(query.category) + }); -const mapStateToProps = null; - -const mapDispatchToProps = { changeEvent }; - -export default connect(mapStateToProps, mapDispatchToProps)(ChangeVersionForm); +export const serializeUrlQuery = (query: Query): RawQuery => + cleanQuery({ + id: serializeString(query.project), + category: serializeString(query.category) + }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js index 746a6fee39e..13078f320a5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js @@ -28,11 +28,12 @@ import VisualizationsContainer from '../visualizations/VisualizationsContainer'; import { parseUrlQuery } from '../store/utils'; import { translate } from '../../../helpers/l10n'; import * as utils from '../utils'; +import type { RawQuery } from '../../../helpers/query'; import '../styles.css'; type Props = {| isFavorite: boolean, - location: { pathname: string, query: { [string]: string } }, + location: { pathname: string, query: RawQuery }, fetchProjects: (query: string, isFavorite: boolean, organization?: {}) => Promise<*>, organization?: { key: string }, router: { @@ -43,7 +44,7 @@ type Props = {| |}; type State = { - query: { [string]: string } + query: RawQuery }; export default class AllProjects extends React.PureComponent { diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js index f3e39df8b4d..bab378af29e 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js @@ -25,12 +25,13 @@ import AllProjectsContainer from './AllProjectsContainer'; import { getCurrentUser } from '../../../store/rootReducer'; import { isFavoriteSet, isAllSet } from '../utils'; import { searchProjects } from '../../../api/components'; +import type { RawQuery } from '../../../helpers/query'; type Props = { currentUser: { isLoggedIn: boolean }, location: { query: {} }, router: { - replace: (location: { pathname?: string, query?: { [string]: string } }) => void + replace: (location: { pathname?: string, query?: RawQuery }) => void } }; diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js index 45ed4b6e8a8..77cd3f97cc8 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js @@ -22,13 +22,14 @@ import React from 'react'; import { IndexLink, Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; import { saveAll, saveFavorite } from '../utils'; +import type { RawQuery } from '../../../helpers/query'; type Props = { user: { isLoggedIn?: boolean }, organization?: { key: string }, - query: { [string]: string } + query: RawQuery }; export default class FavoriteFilter extends React.PureComponent { diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js index dc1aa2de123..356690901e6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js @@ -25,6 +25,7 @@ import Tooltip from '../../../components/controls/Tooltip'; import PerspectiveSelect from './PerspectiveSelect'; import ProjectsSortingSelect from './ProjectsSortingSelect'; import { translate } from '../../../helpers/l10n'; +import type { RawQuery } from '../../../helpers/query'; type Props = {| currentUser?: { isLoggedIn: boolean }, @@ -33,7 +34,7 @@ type Props = {| organization?: { key: string }, projects: Array<*>, projectsAppState: { loading: boolean, total?: number }, - query: { [string]: string }, + query: RawQuery, onSortChange: (sort: string, desc: boolean) => void, selectedSort: string, view: string, diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js index 9771b00f497..65fa894d22c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -37,11 +37,12 @@ import SecurityFilter from '../filters/SecurityFilter'; import SizeFilter from '../filters/SizeFilter'; import TagsFilterContainer from '../filters/TagsFilterContainer'; import { translate } from '../../../helpers/l10n'; +import type { RawQuery } from '../../../helpers/query'; type Props = { isFavorite: boolean, organization?: { key: string }, - query: { [string]: string }, + query: RawQuery, view: string, visualization: string }; diff --git a/server/sonar-web/src/main/js/apps/projects/store/utils.js b/server/sonar-web/src/main/js/apps/projects/store/utils.js index 4c383fe8d0c..190facb928c 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/utils.js +++ b/server/sonar-web/src/main/js/apps/projects/store/utils.js @@ -34,6 +34,7 @@ const getAsLevel = value => { return null; }; +// TODO Maybe use parseAsString form helpers/query const getAsString = value => { if (!value) { return null; @@ -41,6 +42,7 @@ const getAsString = value => { return value; }; +// TODO Maybe move it to helpers/query const getAsArray = (values, elementGetter) => { if (!values) { return null; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/query-test.js.snap b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/query-test.js.snap new file mode 100644 index 00000000000..55b274979e3 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/query-test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cleanQuery should remove undefined and null query items 1`] = ` +Object { + "a": "b", + "d": "", + "e": 0, +} +`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js new file mode 100644 index 00000000000..5a60b4a52c3 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 query from '../query'; + +describe('queriesEqual', () => { + it('should correctly test equality of two queries', () => { + expect(query.queriesEqual({ a: 'test', b: 'test' }, { a: 'test', b: 'test' })).toBeTruthy(); + expect(query.queriesEqual({ a: [1, 2], b: 'test' }, { a: [1, 2], b: 'test' })).toBeTruthy(); + expect(query.queriesEqual({ a: 'a' }, { a: 'test', b: 'test' })).toBeFalsy(); + expect(query.queriesEqual({ a: [1, 2], b: 'test' }, { a: [1], b: 'test' })).toBeFalsy(); + }); +}); + +describe('cleanQuery', () => { + it('should remove undefined and null query items', () => { + expect(query.cleanQuery({ a: 'b', b: undefined, c: null, d: '', e: 0 })).toMatchSnapshot(); + }); +}); + +describe('parseAsBoolean', () => { + it('should parse booleans correctly', () => { + expect(query.parseAsBoolean('false')).toBeFalsy(); + expect(query.parseAsBoolean('true')).toBeTruthy(); + }); + + it('should return a default value', () => { + expect(query.parseAsBoolean(1)).toBeTruthy(); + expect(query.parseAsBoolean('foo')).toBeTruthy(); + }); +}); + +describe('parseAsFacetMode', () => { + it('should facets modes correctly', () => { + expect(query.parseAsFacetMode('debt')).toBe('effort'); + expect(query.parseAsFacetMode('effort')).toBe('effort'); + expect(query.parseAsFacetMode('count')).toBe('count'); + expect(query.parseAsFacetMode('random')).toBe('count'); + }); +}); + +describe('parseAsString', () => { + it('should parse strings correctly', () => { + expect(query.parseAsString('random')).toBe('random'); + expect(query.parseAsString('')).toBe(''); + expect(query.parseAsString(null)).toBe(''); + }); +}); + +describe('parseAsArray', () => { + it('should parse string arrays correctly', () => { + expect(query.parseAsArray('1,2,3', query.parseAsString)).toEqual(['1', '2', '3']); + }); +}); + +describe('serializeString', () => { + it('should serialize string correctly', () => { + expect(query.serializeString('foo')).toBe('foo'); + expect(query.serializeString('')).toBeUndefined(); + }); +}); + +describe('serializeStringArray', () => { + it('should serialize array of string correctly', () => { + expect(query.serializeStringArray(['1', '2', '3'])).toBe('1,2,3'); + expect(query.serializeStringArray([])).toBeUndefined(); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/query.js b/server/sonar-web/src/main/js/helpers/query.js new file mode 100644 index 00000000000..6e7746d4b08 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/query.js @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { isNil, omitBy } from 'lodash'; + +export type RawQuery = { [string]: string }; + +const arraysEqual = <T>(a: Array<T>, b: Array<T>) => { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +}; + +export const queriesEqual = <T>(a: T, b: T): boolean => { + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) { + return false; + } + + return keysA.every( + key => + (Array.isArray(a[key]) && Array.isArray(b[key]) + ? arraysEqual(a[key], b[key]) + : a[key] === b[key]) + ); +}; + +export const cleanQuery = (query: { [string]: ?string }): RawQuery => omitBy(query, isNil); + +export const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean => + (value === 'false' ? false : value === 'true' ? true : defaultValue); + +export const parseAsFacetMode = (facetMode: string) => + (facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count'); + +export const parseAsString = (value: ?string): string => value || ''; + +export const parseAsArray = <T>(value: ?string, itemParser: string => T): Array<T> => + (value ? value.split(',').map(itemParser) : []); + +export const serializeString = (value: string): ?string => value || undefined; + +export const serializeStringArray = (value: ?Array<string>): ?string => + (value && value.length ? value.join() : undefined); diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap deleted file mode 100644 index 48c18d0c6e0..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducer 1`] = `Object {}`; - -exports[`reducer 2`] = ` -Object { - "AVgAgC1Vdo07z3PUnnkt": Object { - "date": "2016-10-26T12:17:29+0200", - "events": Array [ - "AVkWNYNYr4pSN7TrXcjY", - ], - "key": "AVgAgC1Vdo07z3PUnnkt", - }, - "AVgFqeOSKpGuA48ADATE": Object { - "date": "2016-10-27T12:21:15+0200", - "events": Array [], - "key": "AVgFqeOSKpGuA48ADATE", - }, - "AVgGkRvCrrTJiPpCD-rG": Object { - "date": "2016-10-27T16:33:50+0200", - "events": Array [ - "AVjUDBiSiXOcXjpycvde", - ], - "key": "AVgGkRvCrrTJiPpCD-rG", - }, -} -`; - -exports[`reducer 3`] = ` -Object { - "AVgAgC1Vdo07z3PUnnkt": Object { - "date": "2016-10-26T12:17:29+0200", - "events": Array [ - "AVkWNYNYr4pSN7TrXcjY", - ], - "key": "AVgAgC1Vdo07z3PUnnkt", - }, - "AVgFqeOSKpGuA48ADATE": Object { - "date": "2016-10-27T12:21:15+0200", - "events": Array [], - "key": "AVgFqeOSKpGuA48ADATE", - }, - "AVgGkRvCrrTJiPpCD-rG": Object { - "date": "2016-10-27T16:33:50+0200", - "events": Array [ - "AVjUDBiSiXOcXjpycvde", - "AVkWcQ8Hr4pSN7TrXcjZ", - ], - "key": "AVgGkRvCrrTJiPpCD-rG", - }, -} -`; - -exports[`reducer 4`] = ` -Object { - "AVgAgC1Vdo07z3PUnnkt": Object { - "date": "2016-10-26T12:17:29+0200", - "events": Array [ - "AVkWNYNYr4pSN7TrXcjY", - ], - "key": "AVgAgC1Vdo07z3PUnnkt", - }, - "AVgFqeOSKpGuA48ADATE": Object { - "date": "2016-10-27T12:21:15+0200", - "events": Array [], - "key": "AVgFqeOSKpGuA48ADATE", - }, - "AVgGkRvCrrTJiPpCD-rG": Object { - "date": "2016-10-27T16:33:50+0200", - "events": Array [ - "AVjUDBiSiXOcXjpycvde", - ], - "key": "AVgGkRvCrrTJiPpCD-rG", - }, -} -`; - -exports[`reducer 5`] = ` -Object { - "AVgAgC1Vdo07z3PUnnkt": Object { - "date": "2016-10-26T12:17:29+0200", - "events": Array [ - "AVkWNYNYr4pSN7TrXcjY", - ], - "key": "AVgAgC1Vdo07z3PUnnkt", - }, - "AVgGkRvCrrTJiPpCD-rG": Object { - "date": "2016-10-27T16:33:50+0200", - "events": Array [ - "AVjUDBiSiXOcXjpycvde", - ], - "key": "AVgGkRvCrrTJiPpCD-rG", - }, -} -`; diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap deleted file mode 100644 index 335d2124946..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducer 1`] = `Object {}`; - -exports[`reducer 2`] = ` -Object { - "project-foo": Array [ - "AVgFqeOSKpGuA48ADATE", - "AVgAgC1Vdo07z3PUnnkt", - ], -} -`; - -exports[`reducer 3`] = ` -Object { - "project-foo": Array [ - "AVgFqeOSKpGuA48ADATE", - "AVgAgC1Vdo07z3PUnnkt", - "AVgFqeOSKpGuA48ADATX", - ], -} -`; - -exports[`reducer 4`] = ` -Object { - "project-bar": Array [ - "AVgGkRvCrrTJiPpCD-rG", - ], - "project-foo": Array [ - "AVgFqeOSKpGuA48ADATE", - "AVgAgC1Vdo07z3PUnnkt", - "AVgFqeOSKpGuA48ADATX", - ], -} -`; - -exports[`reducer 5`] = ` -Object { - "project-bar": Array [ - "AVgGkRvCrrTJiPpCD-rG", - ], - "project-foo": Array [ - "AVgAgC1Vdo07z3PUnnkt", - "AVgFqeOSKpGuA48ADATX", - ], -} -`; - -exports[`reducer 6`] = ` -Object { - "project-bar": Array [], - "project-foo": Array [ - "AVgAgC1Vdo07z3PUnnkt", - "AVgFqeOSKpGuA48ADATX", - ], -} -`; diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap deleted file mode 100644 index 8dcb0729436..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`actions addEvent 1`] = ` -Object { - "analysis": "foo", - "event": Object { - "key": "bar", - }, - "type": "ADD_PROJECT_ACTIVITY_EVENT", -} -`; - -exports[`actions changeEvent 1`] = ` -Object { - "changes": Object { - "name": "bar", - }, - "event": "foo", - "type": "CHANGE_PROJECT_ACTIVITY_EVENT", -} -`; - -exports[`actions deleteAnalysis 1`] = ` -Object { - "analysis": "bar", - "project": "foo", - "type": "DELETE_PROJECT_ACTIVITY_ANALYSIS", -} -`; - -exports[`actions deleteEvent 1`] = ` -Object { - "analysis": "foo", - "event": "bar", - "type": "DELETE_PROJECT_ACTIVITY_EVENT", -} -`; - -exports[`selectors getAnalyses 1`] = ` -Array [ - Object { - "date": "2016-10-27T16:33:50+0200", - "events": Array [ - Object { - "category": "VERSION", - "key": "AVjUDBiSiXOcXjpycvde", - "name": "2.18-SNAPSHOT", - }, - ], - "key": "AVgGkRvCrrTJiPpCD-rG", - }, - Object { - "date": "2016-10-27T12:21:15+0200", - "events": Array [], - "key": "AVgFqeOSKpGuA48ADATE", - }, - Object { - "date": "2016-10-26T12:17:29+0200", - "events": Array [ - Object { - "category": "OTHER", - "key": "AVkWNYNYr4pSN7TrXcjY", - "name": "foo", - }, - ], - "key": "AVgAgC1Vdo07z3PUnnkt", - }, -] -`; - -exports[`selectors getPaging 1`] = ` -Object { - "pageIndex": 1, - "pageSize": 100, - "total": 3, -} -`; diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap deleted file mode 100644 index 284a895a928..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducer 1`] = `Object {}`; - -exports[`reducer 2`] = ` -Object { - "AVjUDBiSiXOcXjpycvde": Object { - "category": "VERSION", - "key": "AVjUDBiSiXOcXjpycvde", - "name": "2.18-SNAPSHOT", - }, - "AVkWNYNYr4pSN7TrXcjY": Object { - "category": "OTHER", - "key": "AVkWNYNYr4pSN7TrXcjY", - "name": "foo", - }, -} -`; - -exports[`reducer 3`] = ` -Object { - "AVjUDBiSiXOcXjpycvde": Object { - "category": "VERSION", - "key": "AVjUDBiSiXOcXjpycvde", - "name": "2.18-SNAPSHOT", - }, - "AVkWNYNYr4pSN7TrXcjY": Object { - "category": "OTHER", - "key": "AVkWNYNYr4pSN7TrXcjY", - "name": "foo", - }, - "AVkWcQ8Hr4pSN7TrXcjZ": Object { - "category": "OTHER", - "key": "AVkWcQ8Hr4pSN7TrXcjZ", - "name": "custom", - }, -} -`; - -exports[`reducer 4`] = ` -Object { - "AVjUDBiSiXOcXjpycvde": Object { - "category": "VERSION", - "key": "AVjUDBiSiXOcXjpycvde", - "name": "2.18-SNAPSHOT", - }, - "AVkWNYNYr4pSN7TrXcjY": Object { - "category": "OTHER", - "key": "AVkWNYNYr4pSN7TrXcjY", - "name": "foo", - }, - "AVkWcQ8Hr4pSN7TrXcjZ": Object { - "category": "OTHER", - "key": "AVkWcQ8Hr4pSN7TrXcjZ", - "name": "new name", - }, -} -`; - -exports[`reducer 5`] = ` -Object { - "AVjUDBiSiXOcXjpycvde": Object { - "category": "VERSION", - "key": "AVjUDBiSiXOcXjpycvde", - "name": "2.18-SNAPSHOT", - }, - "AVkWNYNYr4pSN7TrXcjY": Object { - "category": "OTHER", - "key": "AVkWNYNYr4pSN7TrXcjY", - "name": "foo", - }, -} -`; diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap deleted file mode 100644 index 8698272003a..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducer 1`] = `Object {}`; - -exports[`reducer 2`] = ` -Object { - "project-foo": Object { - "pageIndex": 1, - "pageSize": 100, - "total": 3, - }, -} -`; - -exports[`reducer 3`] = ` -Object { - "project-foo": Object { - "pageIndex": 2, - "pageSize": 30, - "total": 5, - }, -} -`; diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js deleted file mode 100644 index b2571ab2c90..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 { configureTestStore } from '../../utils/configureStore'; -import analyses, { getAnalysis } from '../analyses'; -import { receiveProjectActivity, addEvent, deleteEvent, deleteAnalysis } from '../duck'; - -const PROJECT = 'project-foo'; - -const ANALYSES = [ - { - key: 'AVgGkRvCrrTJiPpCD-rG', - date: '2016-10-27T16:33:50+0200', - events: [ - { - key: 'AVjUDBiSiXOcXjpycvde', - category: 'VERSION', - name: '2.18-SNAPSHOT' - } - ] - }, - { - key: 'AVgFqeOSKpGuA48ADATE', - date: '2016-10-27T12:21:15+0200', - events: [] - }, - { - key: 'AVgAgC1Vdo07z3PUnnkt', - date: '2016-10-26T12:17:29+0200', - events: [ - { - key: 'AVkWNYNYr4pSN7TrXcjY', - category: 'OTHER', - name: 'foo' - } - ] - } -]; - -const PAGING = { - total: 3, - pageIndex: 1, - pageSize: 100 -}; - -const NEW_EVENT = { - key: 'AVkWcQ8Hr4pSN7TrXcjZ', - category: 'OTHER', - name: 'custom' -}; - -it('reducer', () => { - const store = configureTestStore(analyses); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(deleteAnalysis(PROJECT, ANALYSES[1].key)); - expect(store.getState()).toMatchSnapshot(); -}); - -it('selector `getAnalysis`', () => { - const analysis = ANALYSES[0]; - const store = configureTestStore(analyses, { [analysis.key]: analysis }); - expect(getAnalysis(store.getState(), analysis.key)).toBe(analysis); - expect(getAnalysis(store.getState(), 'random')).toBeFalsy(); -}); diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js deleted file mode 100644 index 8c4a00bf0e2..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 { configureTestStore } from '../../utils/configureStore'; -import analysesByProject from '../analysesByProject'; -import { receiveProjectActivity, deleteAnalysis } from '../duck'; - -const PROJECT_FOO = 'project-foo'; -const PROJECT_BAR = 'project-bar'; - -const ANALYSES_FOO = [ - { - key: 'AVgFqeOSKpGuA48ADATE', - date: '2016-10-27T12:21:15+0200', - events: [] - }, - { - key: 'AVgAgC1Vdo07z3PUnnkt', - date: '2016-10-26T12:17:29+0200', - events: [] - } -]; - -const ANALYSES_FOO_2 = [ - { - key: 'AVgFqeOSKpGuA48ADATX', - date: '2016-10-27T12:21:15+0200', - events: [] - } -]; - -const ANALYSES_BAR = [ - { - key: 'AVgGkRvCrrTJiPpCD-rG', - date: '2016-10-27T16:33:50+0200', - events: [] - } -]; - -const PAGING = { - total: 3, - pageIndex: 1, - pageSize: 100 -}; - -it('reducer', () => { - const store = configureTestStore(analysesByProject); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO, PAGING)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO_2, { pageIndex: 2 })); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT_BAR, ANALYSES_BAR, PAGING)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(deleteAnalysis(PROJECT_FOO, 'AVgFqeOSKpGuA48ADATE')); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(deleteAnalysis(PROJECT_BAR, 'AVgGkRvCrrTJiPpCD-rG')); - expect(store.getState()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js deleted file mode 100644 index d1a6ddfd4a9..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 { configureTestStore } from '../../utils/configureStore'; -import reducer, { - receiveProjectActivity, - getAnalyses, - getPaging, - addEvent, - changeEvent, - deleteEvent, - deleteAnalysis -} from '../duck'; - -const PROJECT = 'project-foo'; - -const ANALYSES = [ - { - key: 'AVgGkRvCrrTJiPpCD-rG', - date: '2016-10-27T16:33:50+0200', - events: [ - { - key: 'AVjUDBiSiXOcXjpycvde', - category: 'VERSION', - name: '2.18-SNAPSHOT' - } - ] - }, - { - key: 'AVgFqeOSKpGuA48ADATE', - date: '2016-10-27T12:21:15+0200', - events: [] - }, - { - key: 'AVgAgC1Vdo07z3PUnnkt', - date: '2016-10-26T12:17:29+0200', - events: [ - { - key: 'AVkWNYNYr4pSN7TrXcjY', - category: 'OTHER', - name: 'foo' - } - ] - } -]; - -const PAGING = { - total: 3, - pageIndex: 1, - pageSize: 100 -}; - -describe('actions', () => { - it('addEvent', () => { - expect(addEvent('foo', { key: 'bar' })).toMatchSnapshot(); - }); - - it('changeEvent', () => { - expect(changeEvent('foo', { name: 'bar' })).toMatchSnapshot(); - }); - - it('deleteEvent', () => { - expect(deleteEvent('foo', 'bar')).toMatchSnapshot(); - }); - - it('deleteAnalysis', () => { - expect(deleteAnalysis('foo', 'bar')).toMatchSnapshot(); - }); -}); - -describe('selectors', () => { - it('getAnalyses', () => { - const store = configureTestStore(reducer); - store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); - expect(getAnalyses(store.getState(), PROJECT)).toMatchSnapshot(); - }); - - it('getPaging', () => { - const store = configureTestStore(reducer); - store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); - expect(getPaging(store.getState(), PROJECT)).toMatchSnapshot(); - }); -}); diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js deleted file mode 100644 index 87b1589cb25..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 { configureTestStore } from '../../utils/configureStore'; -import events, { getEvent } from '../events'; -import { receiveProjectActivity, addEvent, changeEvent, deleteEvent } from '../duck'; - -const PROJECT = 'project-foo'; - -const ANALYSES = [ - { - key: 'AVgGkRvCrrTJiPpCD-rG', - date: '2016-10-27T16:33:50+0200', - events: [ - { - key: 'AVjUDBiSiXOcXjpycvde', - category: 'VERSION', - name: '2.18-SNAPSHOT' - } - ] - }, - { - key: 'AVgFqeOSKpGuA48ADATE', - date: '2016-10-27T12:21:15+0200', - events: [] - }, - { - key: 'AVgAgC1Vdo07z3PUnnkt', - date: '2016-10-26T12:17:29+0200', - events: [ - { - key: 'AVkWNYNYr4pSN7TrXcjY', - category: 'OTHER', - name: 'foo' - } - ] - } -]; - -const PAGING = { - total: 3, - pageIndex: 1, - pageSize: 100 -}; - -const NEW_EVENT = { - key: 'AVkWcQ8Hr4pSN7TrXcjZ', - category: 'OTHER', - name: 'custom' -}; - -it('reducer', () => { - const store = configureTestStore(events); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(changeEvent(NEW_EVENT.key, { name: 'new name' })); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key)); - expect(store.getState()).toMatchSnapshot(); -}); - -it('selector `getEvent`', () => { - const event = ANALYSES[0].events[0]; - const store = configureTestStore(events, { [event.key]: event }); - expect(getEvent(store.getState(), event.key)).toBe(event); - expect(getEvent(store.getState(), 'random')).toBeFalsy(); -}); diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js deleted file mode 100644 index c100f66ad4d..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 { configureTestStore } from '../../utils/configureStore'; -import paging from '../paging'; -import { receiveProjectActivity } from '../duck'; - -const PROJECT = 'project-foo'; - -const ANALYSES = []; - -const PAGING_1 = { - total: 3, - pageIndex: 1, - pageSize: 100 -}; - -const PAGING_2 = { - total: 5, - pageIndex: 2, - pageSize: 30 -}; - -it('reducer', () => { - const store = configureTestStore(paging); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_1)); - expect(store.getState()).toMatchSnapshot(); - - store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_2)); - expect(store.getState()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/store/projectActivity/analyses.js b/server/sonar-web/src/main/js/store/projectActivity/analyses.js deleted file mode 100644 index 1bed62ad651..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/analyses.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import { keyBy } from 'lodash'; -import type { - Action, - ReceiveProjectActivityAction, - AddEventAction, - DeleteEventAction, - DeleteAnalysisAction -} from './duck'; - -type Analysis = { - key: string, - date: string, - events: Array<string> -}; - -export type State = { - [key: string]: Analysis -}; - -const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => { - const analysesWithFlatEvents = action.analyses.map(analysis => ({ - ...analysis, - events: analysis.events.map(event => event.key) - })); - return { ...state, ...keyBy(analysesWithFlatEvents, 'key') }; -}; - -const addEvent = (state: State, action: AddEventAction): State => { - const analysis = state[action.analysis]; - const newAnalysis = { - ...analysis, - events: [...analysis.events, action.event.key] - }; - return { ...state, [action.analysis]: newAnalysis }; -}; - -const deleteEvent = (state: State, action: DeleteEventAction): State => { - const analysis = state[action.analysis]; - const newAnalysis = { - ...analysis, - events: analysis.events.filter(event => event !== action.event) - }; - return { ...state, [action.analysis]: newAnalysis }; -}; - -const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => { - const newState = { ...state }; - delete newState[action.analysis]; - return newState; -}; - -export default (state: State = {}, action: Action): State => { - switch (action.type) { - case 'RECEIVE_PROJECT_ACTIVITY': - return receiveProjectActivity(state, action); - case 'ADD_PROJECT_ACTIVITY_EVENT': - return addEvent(state, action); - case 'DELETE_PROJECT_ACTIVITY_EVENT': - return deleteEvent(state, action); - case 'DELETE_PROJECT_ACTIVITY_ANALYSIS': - return deleteAnalysis(state, action); - default: - return state; - } -}; - -export const getAnalysis = (state: State, key: string): Analysis => state[key]; diff --git a/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js b/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js deleted file mode 100644 index 8601d2d0873..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import type { Action, ReceiveProjectActivityAction, DeleteAnalysisAction } from './duck'; - -export type State = { - [key: string]: Array<string> -}; - -const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => { - const analyses = state[action.project] || []; - const newAnalyses = action.analyses.map(analysis => analysis.key); - return { - ...state, - [action.project]: action.paging.pageIndex === 1 ? newAnalyses : [...analyses, ...newAnalyses] - }; -}; - -const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => { - const analyses = state[action.project]; - return { - ...state, - [action.project]: analyses.filter(key => key !== action.analysis) - }; -}; - -export default (state: State = {}, action: Action): State => { - switch (action.type) { - case 'RECEIVE_PROJECT_ACTIVITY': - return receiveProjectActivity(state, action); - case 'DELETE_PROJECT_ACTIVITY_ANALYSIS': - return deleteAnalysis(state, action); - default: - return state; - } -}; diff --git a/server/sonar-web/src/main/js/store/projectActivity/duck.js b/server/sonar-web/src/main/js/store/projectActivity/duck.js deleted file mode 100644 index 75e24c7e839..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/duck.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import { combineReducers } from 'redux'; -import analyses, * as fromAnalyses from './analyses'; -import type { State as AnalysesState } from './analyses'; -import analysesByProject from './analysesByProject'; -import type { State as AnalysesByProjectState } from './analysesByProject'; -import events, * as fromEvents from './events'; -import type { State as EventsState } from './events'; -import paging from './paging'; -import type { State as PagingState } from './paging'; - -export type Event = { - key: string, - name: string, - category: string, - description?: string -}; - -export type Analysis = { - key: string, - date: string, - events: Array<Event> -}; - -export type Paging = { - total: number, - pageIndex: number, - pageSize: number -}; - -export type ReceiveProjectActivityAction = { - type: 'RECEIVE_PROJECT_ACTIVITY', - project: string, - analyses: Array<Analysis>, - paging: Paging -}; - -export type AddEventAction = { - type: 'ADD_PROJECT_ACTIVITY_EVENT', - analysis: string, - event: Event -}; - -export type DeleteEventAction = { - type: 'DELETE_PROJECT_ACTIVITY_EVENT', - analysis: string, - event: string -}; - -export type ChangeEventAction = { - type: 'CHANGE_PROJECT_ACTIVITY_EVENT', - event: string, - changes: Object -}; - -export type DeleteAnalysisAction = { - type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS', - project: string, - analysis: string -}; - -export type Action = - | ReceiveProjectActivityAction - | AddEventAction - | DeleteEventAction - | ChangeEventAction - | DeleteAnalysisAction; - -export const receiveProjectActivity = ( - project: string, - analyses: Array<Analysis>, - paging: Paging -): ReceiveProjectActivityAction => ({ - type: 'RECEIVE_PROJECT_ACTIVITY', - project, - analyses, - paging -}); - -export const addEvent = (analysis: string, event: Event): AddEventAction => ({ - type: 'ADD_PROJECT_ACTIVITY_EVENT', - analysis, - event -}); - -export const deleteEvent = (analysis: string, event: string): DeleteEventAction => ({ - type: 'DELETE_PROJECT_ACTIVITY_EVENT', - analysis, - event -}); - -export const changeEvent = (event: string, changes: Object): ChangeEventAction => ({ - type: 'CHANGE_PROJECT_ACTIVITY_EVENT', - event, - changes -}); - -export const deleteAnalysis = (project: string, analysis: string): DeleteAnalysisAction => ({ - type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS', - project, - analysis -}); - -type State = { - analyses: AnalysesState, - analysesByProject: AnalysesByProjectState, - events: EventsState, - filter: string, - paging: PagingState -}; - -export default combineReducers({ analyses, analysesByProject, events, paging }); - -const getEvent = (state: State, key: string): Event => fromEvents.getEvent(state.events, key); - -const getAnalysis = (state: State, key: string) => { - const analysis = fromAnalyses.getAnalysis(state.analyses, key); - const events: Array<Event> = analysis.events.map(key => getEvent(state, key)); - return { ...analysis, events }; -}; - -export const getAnalyses = (state: State, project: string) => - state.analysesByProject[project] && - state.analysesByProject[project].map(key => getAnalysis(state, key)); -export const getPaging = (state: State, project: string) => state.paging[project]; diff --git a/server/sonar-web/src/main/js/store/projectActivity/events.js b/server/sonar-web/src/main/js/store/projectActivity/events.js deleted file mode 100644 index d4024b2e068..00000000000 --- a/server/sonar-web/src/main/js/store/projectActivity/events.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import { keyBy } from 'lodash'; -import type { - Action, - ReceiveProjectActivityAction, - AddEventAction, - DeleteEventAction, - ChangeEventAction -} from './duck'; - -export type State = { - [key: string]: { - key: string, - name: string, - category: string, - description?: string - } -}; - -const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => { - const events = {}; - action.analyses.forEach(analysis => { - Object.assign(events, keyBy(analysis.events, 'key')); - }); - return { ...state, ...events }; -}; - -const addEvent = (state: State, action: AddEventAction): State => { - return { ...state, [action.event.key]: action.event }; -}; - -const deleteEvent = (state: State, action: DeleteEventAction): State => { - const newState = { ...state }; - delete newState[action.event]; - return newState; -}; - -const changeEvent = (state: State, action: ChangeEventAction): State => { - const newEvent = { ...state[action.event], ...action.changes }; - return { ...state, [action.event]: newEvent }; -}; - -export default (state: State = {}, action: Action): State => { - switch (action.type) { - case 'RECEIVE_PROJECT_ACTIVITY': - return receiveProjectActivity(state, action); - case 'ADD_PROJECT_ACTIVITY_EVENT': - return addEvent(state, action); - case 'DELETE_PROJECT_ACTIVITY_EVENT': - return deleteEvent(state, action); - case 'CHANGE_PROJECT_ACTIVITY_EVENT': - return changeEvent(state, action); - default: - return state; - } -}; - -export const getEvent = (state: State, key: string) => state[key]; diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 5404ef78f6a..a0f81547c53 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -28,7 +28,6 @@ import notifications, * as fromNotifications from './notifications/duck'; import organizations, * as fromOrganizations from './organizations/duck'; import organizationsMembers, * as fromOrganizationsMembers from './organizationsMembers/reducer'; import globalMessages, * as fromGlobalMessages from './globalMessages/duck'; -import projectActivity from './projectActivity/duck'; import measuresApp, * as fromMeasuresApp from '../apps/component-measures/store/rootReducer'; import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer'; import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer'; @@ -46,7 +45,6 @@ export default combineReducers({ notifications, organizations, organizationsMembers, - projectActivity, users, // apps @@ -122,8 +120,6 @@ export const getOrganizationMembersLogins = (state, organization) => export const getOrganizationMembersState = (state, organization) => fromOrganizationsMembers.getOrganizationMembersState(state.organizationsMembers, organization); -export const getProjectActivity = state => state.projectActivity; - export const getProjects = state => fromProjectsApp.getProjects(state.projectsApp); export const getProjectsAppState = state => fromProjectsApp.getState(state.projectsApp); |