Skip to content

Commit

Permalink
Rewrite the UI with modern Javascript (React / ES2015)
Browse files Browse the repository at this point in the history
This rewrite is done in order to modernize the frontend of the tool's
HTML output. It uses all the cool things like webpack, React, Babel,
immutable and many other JS libs to nicely encapsulate and give structure
to the UI.

This also eases some parts of the UI development: simply running
`npm run webpack-watch` after building the data files automatically
recompiles the .jsx to .js, leaning itself to a pretty fast development
experience.

This does not require a node server to display the HTML output.

This huge commit also fixes some bugs, like not showing the right
amount of approvals per user, and improves the UI for the overview page
by moving the team graph to its own widget at the bottom of the page,
instead of overlaying it on top of the table.
  • Loading branch information
holmari committed Sep 20, 2016
1 parent 05104f0 commit 793f5bb
Show file tree
Hide file tree
Showing 93 changed files with 4,941 additions and 2,954 deletions.
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ local.properties
# Gradle: autogenerated resource listing
resList.js

# Babel generates these files on build step
GerritStats/src/main/resources/res/*.js
# JSON data files, generated when building HTML
GerritStats/src/main/frontend/data

# Default output dir
out/
Expand All @@ -19,7 +19,6 @@ out/

# Node
GerritStats/node_modules
GerritStats/src/main/resources/res/node_modules

# OS X
.DS_Store
Expand Down
2 changes: 1 addition & 1 deletion GerritStats/.babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"presets": ["es2015"]
"presets": ["es2015", "react"]
}
50 changes: 2 additions & 48 deletions GerritStats/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,51 +27,5 @@ jar {
// Include dependencies in JAR file (see http://stackoverflow.com/a/3450409/639421).
from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }

task cleanupNpmDependenciesAndGeneratedJS(type: Delete) {
delete 'src/main/resources/res/node_modules'
delete 'src/main/resources/res/*.js'
}

task copyNpmDependenciesToResources(type: Copy) {
from('node_modules') {
exclude 'npm/**'
}
into 'src/main/resources/res/node_modules'
}

task generateResourceFileListing() << {
assert sourceSets.main.resources.srcDirs.size() == 1;

java.io.StringWriter filenameListWriter = new StringWriter();

File baseDir = sourceSets.main.resources.srcDirs.iterator().next();
String basePathName = baseDir.absolutePath;

Stack<File> fileStack = new Stack<>();
fileStack.add(baseDir);
while (!fileStack.isEmpty()) {
File file = fileStack.pop();
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files.findIndexOf { f -> f.name =~ /^.*\.ignore$/ } == -1) {
fileStack.addAll(files);
}
} else if (file.isFile() && !file.isHidden()) {
String relPath = file.absolutePath.substring(basePathName.length() + 1);
filenameListWriter.write(relPath + '\n');
}
}

java.io.FileWriter fileWriter = new FileWriter(basePathName + "/resList.js");
fileWriter.write(filenameListWriter.toString());
fileWriter.close();
}

cleanupNpmDependenciesAndGeneratedJS.dependsOn npmInstall
// npm_run_* is a magic command whose suffix takes the npm script name.
npm_run_babel.dependsOn npmInstall
copyNpmDependenciesToResources.dependsOn npm_run_babel
copyNpmDependenciesToResources.dependsOn cleanupNpmDependenciesAndGeneratedJS
generateResourceFileListing.dependsOn copyNpmDependenciesToResources
processResources.dependsOn generateResourceFileListing
}
processResources.dependsOn npmInstall
}
35 changes: 27 additions & 8 deletions GerritStats/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,32 @@
"main": "index.html",
"author": "Lasse Holmstedt",
"license": "MIT",
"private": true,
"dependencies": {
"bootstrap": "^3.3.6",
"bootstrap": "^3.3.7",
"classnames": "^2.2.5",
"css-element-queries": "^0.3.2",
"d3": "<4.x.x",
"d3-svg-legend": "^1.12.0",
"jquery": "<3.x.x",
"immutable": "^3.8.1",
"moment": "^2.14.1",
"node-sass": "^3.10.0",
"numeral": "^1.5.3",
"tablesorter": "^2.26.6"
"react": "^15.3.1",
"react-bootstrap": "^0.30.3",
"react-dom": "^15.3.1",
"react-router": "^2.7.0",
"reactable": "^0.14.0"
},
"scripts": {
"clean": "../gradlew clean",
"build": "../gradlew build",
"test": "../gradlew test",
"babel": "babel src/main/js --out-dir src/main/resources/res/"
"cleanJava": "../gradlew clean",
"buildJava": "../gradlew build",
"testJava": "../gradlew test",
"//": "If called inpendently, invoke like this with args: npm run generateData -- -f project.json",
"generateData": "rm -rf src/main/frontend/data && java -Xmx4096m -Xms256m -jar build/libs/GerritStats.jar -o src/main/frontend/data",
"webpack": "webpack --display-error-details --colors --progress --config webpack.config.js",
"webpack-watch": "webpack --display-error-details --colors --progress --watch",
"prewebpack": "cp src/main/frontend/index.html ../out-html/"
},
"repository": {
"type": "git",
Expand All @@ -29,6 +40,14 @@
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-loader": "^6.2.5",
"babel-preset-es2015": "^6.14.0"
"babel-preset-es2015": "^6.14.0",
"babel-preset-react": "^6.11.1",
"css-loader": "^0.25.0",
"file-loader": "^0.9.0",
"react-hot-loader": "^3.0.0-beta.3",
"sass-loader": "^4.0.2",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.2"
}
}
18 changes: 18 additions & 0 deletions GerritStats/src/main/frontend/common/ClearFloat.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

export default class ClearFloat extends React.Component {
constructor(props) {
super(props);
}

render() {
const clearStyle = {
clear: 'both'
};
return (
<div style={clearStyle}></div>
);
}
}

ClearFloat.displayName = 'ClearFloat';
79 changes: 79 additions & 0 deletions GerritStats/src/main/frontend/common/GerritVersionAlerts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';

import {Alert} from 'react-bootstrap';

// show this alert just once for the whole app; it'll come back on refresh.
var alertShown = false;

export default class GerritVersionAlerts extends React.Component {

constructor(props) {
super(props);
this.state = {
alertVisible: !this.isGerritVersionAtLeast(2, 9) && !alertShown
};
}

isGerritVersionAtLeast(major, minor) {
var gerritVersion = this.props.datasetOverview['gerritVersion'] || {};
return gerritVersion['major'] >= major
&& gerritVersion['minor'] >= minor;
}

isGerritVersionUnknown() {
var gerritVersion = this.props.datasetOverview['gerritVersion'] || {};
return gerritVersion['major'] === -1
&& gerritVersion['minor'] === -1
&& gerritVersion['patch'] === -1;
}

getPrintableGerritVersion() {
var gerritVersion = this.props.datasetOverview['gerritVersion'] || {};
return gerritVersion['major'] + '.'
+ gerritVersion['minor'] + '.'
+ gerritVersion['patch'];
}

handleAlertDismiss(event) {
alertShown = true;
this.setState({
alertVisible: false
});
}

render() {
if (!this.state.alertVisible) {
return null;
}

if (this.isGerritVersionUnknown()) {
return (
<Alert bsStyle="warning" onDismiss={this.handleAlertDismiss.bind(this)}>
<strong>Unknown Gerrit version warning</strong>:
Some or all of this data has been generated with data from an unknown Gerrit version.
Some data is not included in the sources.
Rerun GerritDownloader and GerritStats to get rid of this message.
</Alert>
);
} else if (!this.isGerritVersionAtLeast(2, 9)) {
const gerritVersion = this.getPrintableGerritVersion();
return (
<Alert bsStyle="warning" onDismiss={this.handleAlertDismiss.bind(this)}>
<strong>Old Gerrit version warning</strong>:
Some or all of this data has been generated with data from an old Gerrit version ({gerritVersion}).
Versions prior to 2.9 do not provide enough information on who was added as a reviewer,
so much of the data will be incorrect.
Update Gerrit to get better statistics!
</Alert>
);
} else {
return null;
}
}
}

GerritVersionAlerts.displayName = 'GerritVersionAlerts';

GerritVersionAlerts.defaultProps = {
datasetOverview: {}
};
19 changes: 19 additions & 0 deletions GerritStats/src/main/frontend/common/HorizontalCenterDiv.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import './HorizontalCenterDiv.scss'

import React from 'react';

export default class HorizontalCenterDiv extends React.Component {
constructor(props) {
super(props);
}

render() {
return (
<div className='horizontalCenterDiv'>
{this.props.children}
</div>
);
}
}

HorizontalCenterDiv.displayName = 'HorizontalCenterDiv';
4 changes: 4 additions & 0 deletions GerritStats/src/main/frontend/common/HorizontalCenterDiv.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.horizontalCenterDiv {
margin-left: 0px;
text-align: center;
}
44 changes: 44 additions & 0 deletions GerritStats/src/main/frontend/common/PageFooter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import './PageFooter.scss'

import ClearFloat from './ClearFloat';

import React from 'react';
import moment from 'moment';

export default class PageFooter extends React.Component {
constructor(props) {
super(props);
}

renderTimestamp(timestamp) {
if (!timestamp) {
return '\u2013';
} else {
return moment(timestamp).format('YYYY-MM-DD hh:mm:ss')
}
}

render() {
return (
<footer>
<div className="footerContent">
<div className="footerLeft">
<a href="https://github.com/holmari/gerritstats">Github</a>
</div>
<div className="footerRight">
Generated on {this.renderTimestamp(this.props.datasetOverview['generatedDate'])}
</div>
<ClearFloat />
</div>
</footer>
);
}
}

PageFooter.displayName = 'PageFooter';

PageFooter.defaultProps = {
datasetOverview: {
generatedDate: 0
}
};
22 changes: 22 additions & 0 deletions GerritStats/src/main/frontend/common/PageFooter.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
footer {
display: block;
clear: both;
padding-top: 1em;
}

.footerContent {
background-color: #303138;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 14px;
padding-right: 14px;
color: #f4f4f4;
}

.footerLeft {
float: left;
}

.footerRight {
float: right;
}
49 changes: 49 additions & 0 deletions GerritStats/src/main/frontend/common/Panel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import './Panel.scss';

import classnames from 'classnames';
import {List} from 'immutable';
import React from 'react';

export default class Panel extends React.Component {
constructor(props) {
super(props);
}

getClassNames() {
return classnames(
'panel',
{'thirdWidth': this.props.size == 'third'},
{'twoThirdsWidth': this.props.size == 'twoThirds'},
{'halfWidth': this.props.size == 'half'},
{'fullWidth': this.props.size == 'full'},
{'flexWidth': this.props.size == 'flex'},
{'fourthWidth': this.props.size == 'fourth'},
{'threeFourthsWidth': this.props.size == 'threeFourths'},
);
}

render() {
const className = this.getClassNames();
return (
<div className={className}>
<h2>{this.props.title}</h2>
{this.props.children}
</div>
);
}
}

Panel.displayName = 'Panel';

Panel.defaultProps = {
size: 'normal'
};

Panel.propTypes = {
title: React.PropTypes.string.isRequired,
size: React.PropTypes.oneOf([
'normal', 'third', 'twoThirds',
'half', 'full', 'flex',
'fourth', 'threeFourths'
]).isRequired,
};
Loading

0 comments on commit 793f5bb

Please sign in to comment.