Integrate PageRank to the v3 cred explorer! (#468)

This integrates the PageRank table from #466 into the v3 cred explorer
app, bringing the v3 frontend to better-than-parity with v1!

Test plan:
Some unit tests were included, and running `yarn start` and inspecting
the App reveals that it is working correctly. Loading a PageRank result
and then changing the repository no longer triggers a crash :).

Paired with @wchargin
This commit is contained in:
Dandelion Mané 2018-06-30 15:04:40 -07:00 committed by GitHub
parent c10ce0060b
commit d627475119
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 174 additions and 28 deletions

View File

@ -7,11 +7,21 @@ import LocalStore from "./LocalStore";
import {createPluginAdapter as createGithubAdapter} from "../../plugins/github/pluginAdapter";
import {createPluginAdapter as createGitAdapter} from "../../plugins/git/pluginAdapter";
import {Graph} from "../../core/graph";
import {pagerank, type PagerankResult} from "../../core/attribution/pagerank";
import {PagerankTable} from "./PagerankTable";
import type {PluginAdapter} from "../pluginAdapter";
type GraphWithMetadata = {|
+graph: ?Graph,
+pagerankResult: ?PagerankResult,
adapters: ?$ReadOnlyArray<PluginAdapter>,
|};
type Props = {};
type State = {
repoOwner: string,
repoName: string,
data: GraphWithMetadata,
};
const REPO_OWNER_KEY = "repoOwner";
@ -23,6 +33,7 @@ export default class App extends React.Component<Props, State> {
this.state = {
repoOwner: "",
repoName: "",
data: {graph: null, pagerankResult: null, adapters: null},
};
}
@ -34,12 +45,12 @@ export default class App extends React.Component<Props, State> {
}
render() {
const {graph, adapters, pagerankResult} = this.state.data;
return (
<div>
<header className={css(styles.header)}>
<h1>Cred Explorer</h1>
</header>
<p>Welcome to the SourceCred Explorer!</p>
<div>
<label>
Repository owner:
@ -68,6 +79,42 @@ export default class App extends React.Component<Props, State> {
</label>
<br />
<button onClick={() => this.loadData()}>Load data</button>
<button
disabled={graph == null}
onClick={() => {
setTimeout(() => {
if (graph != null) {
const edgeWeight = (_unused_Edge) => ({
toWeight: 1,
froWeight: 1,
});
const pagerankResult = pagerank(graph, edgeWeight, {
verbose: true,
});
const data = {graph, pagerankResult, adapters};
// In case a new graph was loaded while waiting for PageRank
if (this.state.data.graph === graph) {
this.setState({data});
}
}
}, 0);
}}
>
Run basic PageRank
</button>
{graph ? (
<p>
Graph loaded: {Array.from(graph.nodes()).length} nodes,{" "}
{Array.from(graph.edges()).length} edges.
</p>
) : (
<p>Graph not loaded.</p>
)}
<PagerankTable
graph={graph}
pagerankResult={pagerankResult}
adapters={adapters}
/>
</div>
</div>
);
@ -85,37 +132,23 @@ export default class App extends React.Component<Props, State> {
return;
}
const githubGraphPromise = createGithubAdapter(repoOwner, repoName).then(
(githubAdapter) => {
const graph = githubAdapter.graph();
const nodeCount = Array.from(graph.nodes()).length;
const edgeCount = Array.from(graph.edges()).length;
console.log(
`GitHub: Loaded graph: ${nodeCount} nodes, ${edgeCount} edges.`
);
return graph;
const githubPromise = createGithubAdapter(repoOwner, repoName).then(
(adapter) => {
const graph = adapter.graph();
return {graph, adapter};
}
);
const gitGraphPromise = createGitAdapter(repoOwner, repoName).then(
(gitAdapter) => {
const graph = gitAdapter.graph();
const nodeCount = Array.from(graph.nodes()).length;
const edgeCount = Array.from(graph.edges()).length;
console.log(
`Git: Loaded graph: ${nodeCount} nodes, ${edgeCount} edges.`
);
return graph;
}
);
const gitPromise = createGitAdapter(repoOwner, repoName).then((adapter) => {
const graph = adapter.graph();
return {graph, adapter};
});
Promise.all([gitGraphPromise, githubGraphPromise]).then((graphs) => {
const graph = Graph.merge(graphs);
const nodeCount = Array.from(graph.nodes()).length;
const edgeCount = Array.from(graph.edges()).length;
console.log(
`Combined: Loaded graph: ${nodeCount} nodes, ${edgeCount} edges.`
);
Promise.all([gitPromise, githubPromise]).then((graphsAndAdapters) => {
const graph = Graph.merge(graphsAndAdapters.map((x) => x.graph));
const adapters = graphsAndAdapters.map((x) => x.adapter);
const data = {graph, adapters, pagerankResult: null};
this.setState({data});
});
}
}

View File

@ -0,0 +1,113 @@
// @flow
import React from "react";
import {shallow} from "enzyme";
import {pagerank} from "../../core/attribution/pagerank";
import App from "./App";
import {Graph, NodeAddress, EdgeAddress} from "../../core/graph";
require("../testUtil").configureEnzyme();
require("../testUtil").configureAphrodite();
function example() {
const graph = new Graph();
const nodes = {
fooAlpha: NodeAddress.fromParts(["foo", "a", "1"]),
fooBeta: NodeAddress.fromParts(["foo", "b", "2"]),
bar1: NodeAddress.fromParts(["bar", "a", "1"]),
bar2: NodeAddress.fromParts(["bar", "2"]),
xox: NodeAddress.fromParts(["xox"]),
empty: NodeAddress.empty,
};
Object.values(nodes).forEach((n) => graph.addNode((n: any)));
function addEdge(parts, src, dst) {
const edge = {address: EdgeAddress.fromParts(parts), src, dst};
graph.addEdge(edge);
}
addEdge(["a"], nodes.fooAlpha, nodes.fooBeta);
addEdge(["b"], nodes.fooAlpha, nodes.bar1);
addEdge(["c"], nodes.fooAlpha, nodes.xox);
addEdge(["d"], nodes.bar1, nodes.bar1);
addEdge(["e"], nodes.bar1, nodes.xox);
addEdge(["e'"], nodes.bar1, nodes.xox);
const adapters = [
{
name: () => "foo",
graph: () => {
throw new Error("unused");
},
renderer: () => ({
nodeDescription: (x) => `foo: ${NodeAddress.toString(x)}`,
}),
nodePrefix: () => NodeAddress.fromParts(["foo"]),
nodeTypes: () => [
{name: "alpha", prefix: NodeAddress.fromParts(["foo", "a"])},
{name: "beta", prefix: NodeAddress.fromParts(["foo", "b"])},
],
},
{
name: () => "bar",
graph: () => {
throw new Error("unused");
},
renderer: () => ({
nodeDescription: (x) => `bar: ${NodeAddress.toString(x)}`,
}),
nodePrefix: () => NodeAddress.fromParts(["bar"]),
nodeTypes: () => [
{name: "alpha", prefix: NodeAddress.fromParts(["bar", "a"])},
],
},
{
name: () => "xox",
graph: () => {
throw new Error("unused");
},
renderer: () => ({
nodeDescription: (_unused_arg) => `xox node!`,
}),
nodePrefix: () => NodeAddress.fromParts(["xox"]),
nodeTypes: () => [],
},
{
name: () => "unused",
graph: () => {
throw new Error("unused");
},
renderer: () => {
throw new Error("Impossible!");
},
nodePrefix: () => NodeAddress.fromParts(["unused"]),
nodeTypes: () => [],
},
];
const pagerankResult = pagerank(graph, (_unused_Edge) => ({
toWeight: 1,
froWeight: 1,
}));
return {adapters, nodes, graph, pagerankResult};
}
describe("app/credExplorer/App", () => {
it("renders with clean state", () => {
shallow(<App />);
});
it("renders with graph and adapters set", () => {
const app = shallow(<App />);
const {graph, adapters} = example();
const data = {graph, adapters, pagerankResult: null};
app.setState({data});
});
it("renders with graph and adapters and pagerankResult", () => {
const app = shallow(<App />);
const {graph, adapters, pagerankResult} = example();
const data = {graph, adapters, pagerankResult};
app.setState({data});
});
});