Browse Source

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
tags/6.5-M2
Grégoire Aubert 7 years ago
parent
commit
47b553e761
56 changed files with 770 additions and 1718 deletions
  1. 5
    20
      server/sonar-web/src/main/js/api/projectActivity.js
  2. 2
    1
      server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js
  3. 7
    3
      server/sonar-web/src/main/js/apps/issues/components/App.js
  4. 2
    3
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
  5. 1
    1
      server/sonar-web/src/main/js/apps/issues/redirects.js
  6. 42
    76
      server/sonar-web/src/main/js/apps/issues/utils.js
  7. 23
    29
      server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
  8. 1
    1
      server/sonar-web/src/main/js/apps/overview/events/Analysis.js
  9. 85
    0
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/actions-test.js.snap
  10. 106
    0
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js
  11. 41
    78
      server/sonar-web/src/main/js/apps/projectActivity/actions.js
  12. 29
    11
      server/sonar-web/src/main/js/apps/projectActivity/components/Event.js
  13. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js
  14. 9
    5
      server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
  15. 16
    21
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
  16. 21
    8
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
  17. 135
    38
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
  18. 8
    11
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
  19. 10
    35
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js
  20. 8
    6
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
  21. 0
    40
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js
  22. 1
    3
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
  23. 0
    40
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js
  24. 0
    41
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js
  25. 1
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js
  26. 4
    14
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
  27. 0
    48
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js
  28. 1
    3
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js
  29. 0
    48
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js
  30. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/routes.js
  31. 19
    8
      server/sonar-web/src/main/js/apps/projectActivity/types.js
  32. 17
    18
      server/sonar-web/src/main/js/apps/projectActivity/utils.js
  33. 3
    2
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
  34. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js
  35. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js
  36. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
  37. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
  38. 2
    0
      server/sonar-web/src/main/js/apps/projects/store/utils.js
  39. 9
    0
      server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/query-test.js.snap
  40. 84
    0
      server/sonar-web/src/main/js/helpers/__tests__/query-test.js
  41. 68
    0
      server/sonar-web/src/main/js/helpers/query.js
  42. 0
    95
      server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap
  43. 0
    57
      server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap
  44. 0
    77
      server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap
  45. 0
    73
      server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap
  46. 0
    23
      server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap
  47. 0
    90
      server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js
  48. 0
    80
      server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js
  49. 0
    99
      server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js
  50. 0
    90
      server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js
  51. 0
    49
      server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js
  52. 0
    87
      server/sonar-web/src/main/js/store/projectActivity/analyses.js
  53. 0
    53
      server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js
  54. 0
    144
      server/sonar-web/src/main/js/store/projectActivity/duck.js
  55. 0
    77
      server/sonar-web/src/main/js/store/projectActivity/events.js
  56. 0
    4
      server/sonar-web/src/main/js/store/rootReducer.js

+ 5
- 20
server/sonar-web/src/main/js/api/projectActivity.js View File

@@ -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,

+ 2
- 1
server/sonar-web/src/main/js/apps/component-measures/details/history/MeasureHistory.js View File

@@ -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 => {

+ 7
- 3
server/sonar-web/src/main/js/apps/issues/components/App.js View File

@@ -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 = {

+ 2
- 3
server/sonar-web/src/main/js/apps/issues/components/AppContainer.js View File

@@ -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 =>

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/redirects.js View File

@@ -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 = {};

+ 42
- 76
server/sonar-web/src/main/js/apps/issues/utils.js View File

@@ -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,

+ 23
- 29
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js View File

@@ -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);

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/events/Analysis.js View File

@@ -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;

+ 85
- 0
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/actions-test.js.snap View File

@@ -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",
}
`;

+ 106
- 0
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js View File

@@ -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);
});
});

+ 41
- 78
server/sonar-web/src/main/js/apps/projectActivity/actions.js View File

@@ -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)
});

+ 29
- 11
server/sonar-web/src/main/js/apps/projectActivity/components/Event.js View File

@@ -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>
);

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js View File

@@ -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';


+ 9
- 5
server/sonar-web/src/main/js/apps/projectActivity/components/Events.js View File

@@ -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>

+ 16
- 21
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js View File

@@ -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);

+ 21
- 8
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js View File

@@ -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>
);

+ 135
- 38
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js View File

@@ -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);

server/sonar-web/src/main/js/apps/overview/actions.js → server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js View File

@@ -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));

+ 10
- 35
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js View File

@@ -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);

+ 8
- 6
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js View File

@@ -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>


+ 0
- 40
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js View File

@@ -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);

+ 1
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js View File

@@ -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>
);
}

+ 0
- 40
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js View File

@@ -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);

+ 0
- 41
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js View File

@@ -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);

+ 1
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js View File

@@ -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>
);
}

+ 4
- 14
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js View File

@@ -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);

+ 0
- 48
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js View File

@@ -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);

+ 1
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js View File

@@ -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>
);
}

+ 0
- 48
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js View File

@@ -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);

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/routes.js View File

@@ -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 })
);
}
}

server/sonar-web/src/main/js/store/projectActivity/paging.js → server/sonar-web/src/main/js/apps/projectActivity/types.js View File

@@ -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
};

server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js → server/sonar-web/src/main/js/apps/projectActivity/utils.js View File

@@ -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)
});

+ 3
- 2
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js View File

@@ -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 {

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js View File

@@ -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
}
};


+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js View File

@@ -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 {

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/PageHeader.js View File

@@ -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,

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js View File

@@ -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
};

+ 2
- 0
server/sonar-web/src/main/js/apps/projects/store/utils.js View File

@@ -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;

+ 9
- 0
server/sonar-web/src/main/js/helpers/__tests__/__snapshots__/query-test.js.snap View File

@@ -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,
}
`;

+ 84
- 0
server/sonar-web/src/main/js/helpers/__tests__/query-test.js View File

@@ -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();
});
});

+ 68
- 0
server/sonar-web/src/main/js/helpers/query.js View File

@@ -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);

+ 0
- 95
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap View File

@@ -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",
},
}
`;

+ 0
- 57
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap View File

@@ -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",
],
}
`;

+ 0
- 77
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap View File

@@ -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,
}
`;

+ 0
- 73
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap View File

@@ -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",
},
}
`;

+ 0
- 23
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap View File

@@ -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,
},
}
`;

+ 0
- 90
server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js View File

@@ -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();
});

+ 0
- 80
server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js View File

@@ -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();
});

+ 0
- 99
server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js View File

@@ -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();
});
});

+ 0
- 90
server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js View File

@@ -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();
});

+ 0
- 49
server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js View File

@@ -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();
});

+ 0
- 87
server/sonar-web/src/main/js/store/projectActivity/analyses.js View File

@@ -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];

+ 0
- 53
server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js View File

@@ -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;
}
};

+ 0
- 144
server/sonar-web/src/main/js/store/projectActivity/duck.js View File

@@ -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];

+ 0
- 77
server/sonar-web/src/main/js/store/projectActivity/events.js View File

@@ -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];

+ 0
- 4
server/sonar-web/src/main/js/store/rootReducer.js View File

@@ -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);

Loading…
Cancel
Save