Skip to content

Commit

Permalink
#233 added basic code review metrics functionality and adjusted fileB…
Browse files Browse the repository at this point in the history
…rowser
  • Loading branch information
maerzman committed Jul 25, 2024
1 parent aad500a commit d816d01
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down Expand Up @@ -73,12 +76,119 @@ class ChartComponent extends React.Component<Props, State> {

const mergeRequests = props.mergeRequests;
const metricsData: Bubble[] = [];
const usersData = new Map<string, [number, string]>();
const filesData = new Map<string, number>();

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<string, [number, string]>): 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<string, number>): 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<string, [number, string]>, 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<string, [number, string]>, 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<string, number>, 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<string, number>): void {
const count = map.get(key) || 0;
map.set(key, count + 1);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Props> {
onClickGrouping(group) {
console.log(this.props);
this.props.setGrouping(group);
}

onClickCategory(category) {
this.props.setCategory(category);
}

render() {
return <div className={styles.configContainer}>Config</div>;
return (
<div className={styles.configContainer}>
<div className="field">
<h2>Grouping</h2>
<div className="control">
<TabCombo
options={[
{ label: 'Users', icon: 'user', value: 'user' },
{ label: 'Files', icon: 'file', value: 'file' },
]}
value={this.props.codeReviewMetricsState.config.grouping}
onChange={(value) => this.onClickGrouping(value)}
/>
</div>
</div>
{this.props.codeReviewMetricsState.config.grouping === 'user' && (
<div className="field">
<h2>Category</h2>
<TabCombo
options={[
{ label: 'Comments', icon: '', value: 'comment' },
{ label: 'Reviews', icon: '', value: 'review' },
]}
value={this.props.codeReviewMetricsState.config.category}
onChange={(value) => this.onClickCategory(value)}
/>
</div>
)}
{this.props.codeReviewMetricsState.config.grouping === 'file' && (
<div className="field">
<FileBrowser
files={this.props.codeReviewMetricsState.data.data.files}
props={this.props}
highlights={this.props.codeReviewMetricsState.config.path}
/>
</div>
)}
</div>
);
}
}

Expand All @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@ export default {
ChartComponent,
ConfigComponent,
HelpComponent,
usesUniversalSettings: false,
usesUniversalSettings: true,
universalSettingsConfig: {
hideExcludeCommitSettings: true,
hideMergeCommitSettings: true,
hideSprintSettings: true,
hideGranularitySettings: true,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,32 @@

import { handleActions } from 'redux-actions';
import _ from 'lodash';

const highlights = new Set<string>();

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: '' },
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,64 @@
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');

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,
Expand Down
Loading

0 comments on commit d816d01

Please sign in to comment.