diff --git a/binocular-frontend/src/visualizations/code-review-metrics/chart/chart.tsx b/binocular-frontend/src/visualizations/code-review-metrics/chart/chart.tsx index ab3efb880..035d23634 100644 --- a/binocular-frontend/src/visualizations/code-review-metrics/chart/chart.tsx +++ b/binocular-frontend/src/visualizations/code-review-metrics/chart/chart.tsx @@ -1,14 +1,17 @@ 'use-strict'; import React from 'react'; import BubbleChart, { Bubble } from '../../../components/BubbleChart'; -import { MergeRequest } from '../../../types/dbTypes'; +import { Author, Comment, MergeRequest, ReviewThread } from '../../../types/dbTypes'; import LegendCompact from '../../../components/LegendCompact'; import _ from 'lodash'; import styles from '../styles.module.scss'; import { connect } from 'react-redux'; +import { incrementCollectionForSelectedAuthors } from '../../merge-request-ownership/chart/utils'; interface Props { mergeRequests: any[]; + allAuthors: any; + selectedAuthors: any; codeReviewMetricsState: any; } @@ -73,12 +76,119 @@ class ChartComponent extends React.Component { const mergeRequests = props.mergeRequests; const metricsData: Bubble[] = []; + const usersData = new Map(); + const filesData = new Map(); + + const configState = props.codeReviewMetricsState.config; + if (configState.grouping === 'user') { + switch (configState.category) { + case 'comment': + this.getCommentOwnershipCountByUser(mergeRequests, usersData, props); + break; + case 'review': + this.getReviewOwnershipCountByUser(mergeRequests, usersData, props); + break; + default: + break; + } + this.extractUsersData(metricsData, usersData); + } else if (configState.grouping === 'file') { + this.getReviewThreadOwnershipCountByFile(mergeRequests, filesData, props); + this.extractFilesData(metricsData, filesData); + } - console.log(props.codeReviewMetricsState); + return { metricsData }; + } - _.each(mergeRequests, (mergeRequest: MergeRequest) => {}); + extractUsersData(metricsData: Bubble[], usersData: Map): void { + usersData.forEach((entry) => { + const [count, color] = entry; + const bubble: Bubble = { + x: 0, + y: 0, + color: color, + size: count, + }; + metricsData.push(bubble); + }); + } - return { metricsData }; + extractFilesData(metricsData: Bubble[], filesData: Map): void { + filesData.forEach((count) => { + const bubble: Bubble = { + x: 0, + y: 0, + color: 'red', + size: count, + }; + metricsData.push(bubble); + }); + } + + /** + * returns the amount of review threads owned per user + * @param mergeRequests all mergerequests in the project + * @param authorMap map that stores the results (key: user login, value: count) + */ + getReviewOwnershipCountByUser(mergeRequests: MergeRequest[], authorMap: Map, props): void { + const authors: Author[] = []; + _.each(mergeRequests, (mergeRequest: MergeRequest) => { + mergeRequest.reviewThreads.forEach((reviewThread: ReviewThread) => { + const ownership = reviewThread.comments[0]; + if (!ownership || !ownership.author) return; + authors.push(ownership.author); + }); + }); + incrementCollectionForSelectedAuthors(authors, props.allAuthors, props.selectedAuthors, authorMap); + } + + /** + * returns the amount of comments owned per user + * @param mergeRequests all mergerequests in the project + * @param authorMap map that stores the results (key: user login, value: count) + */ + getCommentOwnershipCountByUser(mergeRequests: MergeRequest[], authorMap: Map, props): void { + const authors: Author[] = []; + _.each(mergeRequests, (mergeRequest: MergeRequest) => { + // process comments directly inside the merge request + mergeRequest.comments.forEach((comment: Comment) => { + if (!comment.author) return; + authors.push(comment.author); + }); + // process comments of a review thread inside the merge request + mergeRequest.reviewThreads.forEach((reviewThread: ReviewThread) => { + reviewThread.comments.forEach((comment: Comment) => { + if (!comment.author) return; + authors.push(comment.author); + }); + }); + }); + incrementCollectionForSelectedAuthors(authors, props.allAuthors, props.selectedAuthors, authorMap); + } + + /** + * returns the amount of review threads for any file + * @param mergeRequests all mergerequests in the project + * @param fileMap map that stores the results (key: path, value: count) + */ + getReviewThreadOwnershipCountByFile(mergeRequests: MergeRequest[], fileMap: Map, props) { + _.each(mergeRequests, (mergeRequest: MergeRequest) => { + mergeRequest.reviewThreads.forEach((reviewThread: ReviewThread) => { + if (props.codeReviewMetricsState.config.path.has(reviewThread.path)) { + this.handleMapIncrementation(reviewThread.path, fileMap); + } + }); + }); + } + + /** + * increments the count on a map + * @param key key of the element to be incremented + * @param map the map to be incremented on + */ + handleMapIncrementation(key: string, map: Map): void { + const count = map.get(key) || 0; + map.set(key, count + 1); } } diff --git a/binocular-frontend/src/visualizations/code-review-metrics/chart/index.tsx b/binocular-frontend/src/visualizations/code-review-metrics/chart/index.tsx index 7919d0e45..c79d04111 100644 --- a/binocular-frontend/src/visualizations/code-review-metrics/chart/index.tsx +++ b/binocular-frontend/src/visualizations/code-review-metrics/chart/index.tsx @@ -6,8 +6,11 @@ import Chart from './chart'; const mapStateToProps = (state) => { const codeReviewMetricsState = state.visualizations.codeReviewMetrics.state; + const universalSettings = state.universalSettings; return { mergeRequests: codeReviewMetricsState.data.data.mergeRequests, + selectedAuthors: universalSettings.selectedAuthorsGlobal, + allAuthors: universalSettings.allAuthors, }; }; diff --git a/binocular-frontend/src/visualizations/code-review-metrics/config.tsx b/binocular-frontend/src/visualizations/code-review-metrics/config.tsx index 36ae06463..2b58d379a 100644 --- a/binocular-frontend/src/visualizations/code-review-metrics/config.tsx +++ b/binocular-frontend/src/visualizations/code-review-metrics/config.tsx @@ -1,18 +1,75 @@ 'use-strict'; import React from 'react'; -import TabCombo from '../../components/TabCombo'; import * as styles from './styles.module.scss'; import { connect } from 'react-redux'; -import { setGroup, setMergeRequests } from './sagas'; +import TabCombo from '../../components/TabCombo'; +import { File, setCategory, setFile, setGrouping, setPath } from './sagas'; +import FileBrowser from '../legacy/code-hotspots/components/fileBrowser/fileBrowser'; interface Props { codeReviewMetricsState: any; + setGrouping: (group: any) => void; + setCategory: (category: any) => void; + setPath: (path: string) => void; + setFile: (file: string) => void; + fileBrowserProps: FileBrowserProps; +} + +interface FileBrowserProps { + files: File[]; } class ConfigComponent extends React.Component { + onClickGrouping(group) { + console.log(this.props); + this.props.setGrouping(group); + } + + onClickCategory(category) { + this.props.setCategory(category); + } + render() { - return
Config
; + return ( +
+
+

Grouping

+
+ this.onClickGrouping(value)} + /> +
+
+ {this.props.codeReviewMetricsState.config.grouping === 'user' && ( +
+

Category

+ this.onClickCategory(value)} + /> +
+ )} + {this.props.codeReviewMetricsState.config.grouping === 'file' && ( +
+ +
+ )} +
+ ); } } @@ -21,7 +78,12 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = (dispatch /*, ownProps*/) => { - return {}; + return { + setGrouping: (group) => dispatch(setGrouping(group)), + setCategory: (category) => dispatch(setCategory(category)), + onSetPath: (path) => dispatch(setPath(path)), + onSetFile: (file) => dispatch(setFile(file)), + }; }; export default connect(mapStateToProps, mapDispatchToProps)(ConfigComponent); diff --git a/binocular-frontend/src/visualizations/code-review-metrics/index.ts b/binocular-frontend/src/visualizations/code-review-metrics/index.ts index d8a05d414..f55115b63 100644 --- a/binocular-frontend/src/visualizations/code-review-metrics/index.ts +++ b/binocular-frontend/src/visualizations/code-review-metrics/index.ts @@ -13,5 +13,11 @@ export default { ChartComponent, ConfigComponent, HelpComponent, - usesUniversalSettings: false, + usesUniversalSettings: true, + universalSettingsConfig: { + hideExcludeCommitSettings: true, + hideMergeCommitSettings: true, + hideSprintSettings: true, + hideGranularitySettings: true, + }, }; diff --git a/binocular-frontend/src/visualizations/code-review-metrics/reducers/config.ts b/binocular-frontend/src/visualizations/code-review-metrics/reducers/config.ts index c55df7f22..838840daf 100644 --- a/binocular-frontend/src/visualizations/code-review-metrics/reducers/config.ts +++ b/binocular-frontend/src/visualizations/code-review-metrics/reducers/config.ts @@ -2,9 +2,32 @@ import { handleActions } from 'redux-actions'; import _ from 'lodash'; + +const highlights = new Set(); + +const setPath = (state, action) => { + const highlight = new Set(state.path); + if (highlight.has(action.payload)) { + highlight.delete(action.payload); + } else { + highlight.add(action.payload); + } + return _.assign({}, state, { path: highlight }); +}; + +const initHighlights = (state, action) => { + const highlight = new Set(action.payload); + return _.assign({}, state, { path: highlight }); +}; + export default handleActions( { SET_ACTIVE_VISUALIZATIONS: (state, action) => _.assign({}, state, { visualizations: action.payload }), + SET_GROUPING: (state, action) => _.assign({}, state, { grouping: action.payload }), + SET_CATEGORY: (state, action) => _.assign({}, state, { category: action.payload }), + SET_PATH: setPath, + SET_FILE: (state, action) => _.assign({}, state, { file: action.payload }), + INIT_HIGHLIGHTS: initHighlights, }, - { visualizations: [] }, + { visualizations: [], grouping: 'user', category: 'comment', path: highlights, file: '' }, ); diff --git a/binocular-frontend/src/visualizations/code-review-metrics/sagas/index.ts b/binocular-frontend/src/visualizations/code-review-metrics/sagas/index.ts index 65d87e3b1..366858d6b 100644 --- a/binocular-frontend/src/visualizations/code-review-metrics/sagas/index.ts +++ b/binocular-frontend/src/visualizations/code-review-metrics/sagas/index.ts @@ -3,12 +3,16 @@ import { createAction } from 'redux-actions'; import { fetchFactory, timestampedActionFactory } from '../../../sagas/utils'; import Database from '../../../database/database'; +import { put } from 'redux-saga/effects'; export const setActiveVisualizations = createAction('SET_ACTIVE_VISUALIZATIONS'); -export const setMergeRequests = createAction('SET_SHOW_MERGE_REQUESTS'); -export const setGroup = createAction('CRM_SET_GROUP'); export const refresh = createAction('REFRESH'); export const requestCodeReviewMetricsData = createAction('REQUEST_CODE_REVIEW_METRICS_DATA'); +export const setGrouping = createAction('SET_GROUPING'); +export const setCategory = createAction('SET_CATEGORY'); +export const setFile = createAction('SET_FILE'); +export const setPath = createAction('SET_PATH'); +export const initHighlights = createAction('INIT_HIGHLIGHTS'); export const receiveCodeReviewMetricsData = timestampedActionFactory('RECEIVE_CODE_REVIEW_METRICS_DATA'); export const receiveCodeReviewMetricsDataError = createAction('RECEIVE_CODE_REVIEW_METRICS_DATA_ERROR'); @@ -16,39 +20,47 @@ export default function* () { yield* fetchCodeReviewMetricsData(); } +export interface File { + key: string; + webUrl: string; +} + export const fetchCodeReviewMetricsData = fetchFactory( function* () { - const { firstMergeRequest, lastMergeRequest, firstComment, lastComment } = yield Database.getBounds(); + const { firstMergeRequest, lastMergeRequest } = yield Database.getBounds(); const firstMergeRequestTimestamp = Date.parse(firstMergeRequest.date); const lastMergeRequestTimestamp = Date.parse(lastMergeRequest.date); - const firstCommentTimestamp = Date.parse(firstComment.date); - const lastCommentTimestamp = Date.parse(lastComment.date); - return yield Promise.all([ + const results = yield Promise.all([ new Promise((resolve) => { - Database.getMergeRequestData( + const results = Database.getMergeRequestData( [firstMergeRequestTimestamp, lastMergeRequestTimestamp], [firstMergeRequestTimestamp, lastMergeRequestTimestamp], - ).then(resolve); - }), - new Promise((resolve) => { - Database.getCommentData([firstCommentTimestamp, lastCommentTimestamp], [firstCommentTimestamp, lastCommentTimestamp]).then(resolve); + ); + resolve(results); }), new Promise((resolve) => { - Database.getReviewThreadData().then(resolve); + const files: File[] = []; + Database.requestFileStructure().then((result) => { + const fs = result.files.data; + for (const f in fs) { + files.push({ key: fs[f].path, webUrl: fs[f].webUrl }); + } + resolve(files); + }); }), - ]).then((values) => { - const mergeRequests = values[0]; - const comments = values[1]; - const reviewThreads = values[2]; - console.log(reviewThreads); - return { - mergeRequests, - comments, - reviewThreads, - }; - }); + ]); + + const mergeRequests = results[0]; + const files = results[1]; + + yield put(initHighlights(files.map((file) => file.key))); + + return { + mergeRequests, + files, + }; }, requestCodeReviewMetricsData, receiveCodeReviewMetricsData, diff --git a/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.js b/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.js index b38a54c6e..c1e5f4a44 100644 --- a/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.js +++ b/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.js @@ -66,7 +66,12 @@ export default class FileBrowser extends React.PureComponent { {fileCount === 0 ?
Loading Files ...
: filteredFileCount === 0 ?
No Files found!
: null}
- +
); @@ -80,9 +85,15 @@ class FileStruct extends React.PureComponent {
{this.props.data.content.map((data, i) => { if (data.type === 'file') { + // only highlight if set is present + let shouldHighlight = false; + if (this.props.highlights) { + shouldHighlight = this.props.highlights.has(data.name); + } + const classes = shouldHighlight ? styles.BCHighlighted : i % 2 === 0 ? styles.BCEven : styles.BCOdd; return (
{ this.clickFile(data.url, data.path); diff --git a/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.module.scss b/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.module.scss index 7575d2e2f..a1ca32740 100644 --- a/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.module.scss +++ b/binocular-frontend/src/visualizations/legacy/code-hotspots/components/fileBrowser/fileBrowser.module.scss @@ -16,6 +16,14 @@ border-radius: 4px; } +.BCHighlighted { + background-color: #aedbfb; +} + +.BCHighlighted:hover { + background-color: #f0c6c6; +} + .ACOdd{ background-color: #4882e0; color: whitesmoke; @@ -94,3 +102,7 @@ .searchBoxHint:focus-within{ height: 4rem; } + +.highlight { + +} \ No newline at end of file