Strip down explorer app to a barebones React app (#88)

Summary:
We’re not deleting it because it works with the build system and has the
service worker stuff from create-react-app, but we’ll soon repurpose it.

Paired with @dandelionmane.

Test Plan:
The following still work:
  - `yarn test`
  - `yarn start`
  - `yarn build; (cd build; python -m SimpleHTTPServer)`

wchargin-branch: dismantle-explorer
This commit is contained in:
William Chargin 2018-03-19 15:09:11 -07:00 committed by Dandelion Mané
parent ca85fdf234
commit 8f8d9c4564
8 changed files with 5 additions and 414 deletions

View File

@ -1,51 +0,0 @@
.App {
display: grid;
grid-template-areas: "header header"
"fileexplorer userexplorer";
grid-template-rows: 50px 1fr;
grid-template-columns: 1fr 1fr;
grid-gap: 4px;
width: 100%;
}
html, body, #root, .App {
height: 100%;
font-family: Roboto, sans-serif;
}
.anchor-button:visited {
color: black;
}
.selected-path {
background-color: #ddd;
}
.App-intro {
font-size: large;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
.plugin-pane {
overflow: scroll;
background-color: white;
border-radius: 5px;
margin: 8px;
}
.file-explorer {
grid-area: fileexplorer;
}
.user-explorer {
grid-area: userexplorer;
}

View File

@ -1,31 +1,16 @@
// @flow // @flow
import React, {Component} from "react";
import "./App.css"; import React from "react";
import {GraphExplorer} from "./GraphExplorer";
type Props = {}; type Props = {};
type State = {}; type State = {};
class App extends Component<Props, State> { export default class App extends React.Component<Props, State> {
render() { render() {
return ( return (
<div className="App" style={{backgroundColor: "#eeeeee"}}> <div>
<header <h1>Hello, world!</h1>
style={{
backgroundColor: "#01579B",
color: "white",
gridArea: "header",
textAlign: "center",
boxShadow: "0px 2px 2px #aeaeae",
}}
>
<h1 style={{fontSize: "1.5em"}}>SourceCred Explorer</h1>
</header>
<GraphExplorer />
</div> </div>
); );
} }
} }
export default App;

View File

@ -1,95 +0,0 @@
// @flow
import React, {Component} from "react";
import {buildTree} from "./commitUtils";
import type {CommitData, FileTree} from "./commitUtils";
export class FileExplorer extends Component<{
selectedPath: string,
onSelectPath: (newPath: string) => void,
data: CommitData,
}> {
render() {
// within the FileExplorer, paths start with "./", outside they don't
// which is hacky and should be cleaned up
const fileNames = Object.keys(this.props.data.fileToCommits).sort();
const tree = buildTree(fileNames);
const selectPath = (path) => {
if (path.startsWith("./")) {
path = path.slice(2);
}
this.props.onSelectPath(path);
};
return (
<div className="file-explorer plugin-pane">
<h3 style={{textAlign: "center"}}>File Explorer</h3>
<div style={{fontFamily: "monospace"}}>
<FileEntry
alwaysExpand={true}
name=""
path="."
tree={tree}
onSelectPath={selectPath}
selectedPath={`./${this.props.selectedPath}`}
/>
</div>
</div>
);
}
}
class FileEntry extends Component<
{
name: string,
path: string,
alwaysExpand: boolean,
tree: FileTree,
selectedPath: string,
onSelectPath: (newPath: string) => void,
},
{
expanded: boolean,
}
> {
constructor() {
super();
this.state = {expanded: false};
}
render() {
const topLevels = Object.keys(this.props.tree);
const subEntries = topLevels.map((x) => (
<FileEntry
key={x}
name={x}
path={`${this.props.path}/${x}`}
alwaysExpand={false}
tree={this.props.tree[x]}
selectedPath={this.props.selectedPath}
onSelectPath={this.props.onSelectPath}
/>
));
const isFolder = topLevels.length > 0 && !this.props.alwaysExpand;
const toggleExpand = () => this.setState({expanded: !this.state.expanded});
const isSelected = this.props.path === this.props.selectedPath;
const selectTarget = isSelected ? "." : this.props.path;
const onClick = () => this.props.onSelectPath(selectTarget);
return (
<div
className={isSelected ? "selected-path" : ""}
style={{marginLeft: this.props.path === "." ? 0 : 25}}
>
<p>
{isFolder && (
<button style={{marginRight: 3}} onClick={toggleExpand}>
»
</button>
)}
<a href="javascript: void 0" onClick={onClick}>
{this.props.name}
</a>
</p>
{(this.state.expanded || this.props.alwaysExpand) && subEntries}
</div>
);
}
}

View File

@ -1,17 +0,0 @@
// @flow
// A frontend for visualizing Contribution Graphs
import React, {Component} from "react";
type Props = {};
type State = {};
export class GraphExplorer extends Component<Props, State> {
render() {
return (
<div>
<h1>Graph Explorer</h1>
</div>
);
}
}

View File

@ -1,49 +0,0 @@
// @flow
import React, {Component} from "react";
import {commitWeight, userWeightForPath} from "./commitUtils";
import type {CommitData, FileTree} from "./commitUtils";
export class UserExplorer extends Component<{
selectedPath: string,
selectedUser: ?string,
onSelectUser: (newUser: string) => void,
data: CommitData,
}> {
render() {
const weights = userWeightForPath(
this.props.selectedPath,
this.props.data,
commitWeight
);
const sortedUserWeightTuples = Object.keys(weights)
.map((k) => [k, weights[k]])
.sort((a, b) => b[1] - a[1]);
const entries = sortedUserWeightTuples.map((authorWeight) => {
const [author, weight] = authorWeight;
return <UserEntry userId={author} weight={weight} key={author} />;
});
return (
<div className="user-explorer plugin-pane">
<h3 style={{textAlign: "center"}}> User Explorer </h3>
<div style={{marginLeft: 8, marginRight: 8}}>{entries}</div>
</div>
);
}
}
/**
* Record the cred earned by the user in a given scope.
*/
class UserEntry extends Component<{
userId: string,
weight: number,
}> {
render() {
return (
<div className="user-entry">
<span> {this.props.userId} </span>
<span> {this.props.weight.toFixed(1)} </span>
</div>
);
}
}

View File

@ -1,107 +0,0 @@
// @flow
import PropTypes from "prop-types";
export type CommitData = {
// TODO improve variable names
fileToCommits: {[filename: string]: string[]},
commits: {[commithash: string]: Commit},
};
type Commit = {
author: string,
stats: {[filename: string]: FileStats},
};
type FileStats = {
lines: number,
added: number,
deleted: number,
};
export function commitWeight(commit: Commit, filepath: string): number {
// hack - GitPython encodes renames in the filepath. ignore for now.
if (filepath.indexOf("=>") !== -1) {
return 0;
}
return Math.sqrt(commit.stats[filepath].lines);
}
function allSelectedFiles(filepath: string, data: CommitData): string[] {
const fnames = Object.keys(data.fileToCommits);
return fnames.filter((x) => x.startsWith(filepath));
}
function* userWeights(
files: string[],
data: CommitData,
weightFn: WeightFn
): Iterable<[string, number]> {
for (const file of files) {
for (const commitHash of data.fileToCommits[file]) {
const commit = data.commits[commitHash];
let w;
if (commit.stats[file] == null) {
// hack - likely due to the GitPython file rename issue
w = 0;
} else {
w = weightFn(commit, file);
}
yield [commit.author, w];
}
}
}
/**
* A weight function determines how much commit contributes to the importance
* of a particular file. For instance, a weight function might be defined as,
* the number of lines changed in filepath in commit commit, or 1 if
* filepath was changed in commit, else 0.
*/
type WeightFn = (commit: Commit, filepath: string) => number;
export function userWeightForPath(
path: string,
data: CommitData,
weightFn: WeightFn
): {[string]: number} {
const userWeightMap = {};
const files = allSelectedFiles(path, data);
for (const [user, weight] of userWeights(files, data, weightFn)) {
if (userWeightMap[user] == null) {
userWeightMap[user] = 0;
}
userWeightMap[user] += weight;
}
return userWeightMap;
}
export type FileTree = {[string]: FileTree};
export function buildTree(fileNames: string[]): FileTree {
const sortedFileNames = fileNames.slice().sort();
return _buildTree(sortedFileNames);
}
function _buildTree(sortedFileNames: string[]): FileTree {
const topLevelBuckets: {[root: string]: string[]} = {};
for (const fileName of sortedFileNames) {
const topLevel = fileName.split("/")[0];
const remainder = fileName
.split("/")
.slice(1)
.join("/");
if (topLevelBuckets[topLevel] == null) {
topLevelBuckets[topLevel] = [];
}
if (remainder !== "") {
topLevelBuckets[topLevel].push(remainder);
}
}
const result = {};
for (const topLevel of Object.keys(topLevelBuckets)) {
const remainders = topLevelBuckets[topLevel];
result[topLevel] = _buildTree(remainders);
}
return result;
}

View File

@ -1,74 +0,0 @@
// @flow
import {userWeightForPath, buildTree} from "./commitUtils";
const exampleData = {
fileToCommits: {
"foo.txt": ["1"],
"bar.txt": ["1", "2"],
},
commits: {
"1": {
author: "dandelionmane",
stats: {
"foo.txt": {lines: 5, added: 3, deleted: 2},
"bar.txt": {lines: 100, added: 100, deleted: 0},
},
},
"2": {
author: "wchargin",
stats: {
"bar.txt": {lines: 100, added: 50, deleted: 50},
},
},
},
authors: ["dandelionmane", "wchargin"],
};
const emptyData = {fileToCommits: {}, commits: {}, authors: []};
function weightByNumFilesTouched(commit, filepath) {
return 1;
}
describe("userWeightForPath", () => {
it("works on empty data", () => {
const actual = userWeightForPath("", emptyData, weightByNumFilesTouched);
const expected = {};
expect(actual).toEqual(expected);
});
it("works in simple case", () => {
const actual = userWeightForPath("", exampleData, weightByNumFilesTouched);
const expected = {dandelionmane: 2, wchargin: 1};
expect(actual).toEqual(expected);
});
it("respects file paths", () => {
const actual = userWeightForPath(
"bar.txt",
exampleData,
weightByNumFilesTouched
);
const expected = {dandelionmane: 1, wchargin: 1};
expect(actual).toEqual(expected);
});
it("uses custom weight function", () => {
const myWeight = (commit, filepath) => commit.stats[filepath].lines;
const actual = userWeightForPath("", exampleData, myWeight);
const expected = {dandelionmane: 105, wchargin: 100};
expect(actual).toEqual(expected);
});
});
describe("buildTree", () => {
it("handles empty tree", () => {
expect(buildTree([])).toEqual({});
});
it("handles trees", () => {
const names = ["foo", "bar/zod", "bar/zoink"];
const expected = {foo: {}, bar: {zod: {}, zoink: {}}};
expect(buildTree(names)).toEqual(expected);
});
});

File diff suppressed because one or more lines are too long