Organize weights by plugin (#773)

This commit adds PluginWeightConfig, which is responsible for
adding all the weights for an individual plugin. The top-level
WeightConfig now creates multiple PluginWeightConfigs. It also takes
responsibility for hiding the FallbackPlugin.

Test plan: The PluginWeightConfig is tested (and fairly simple). The
top-level WeightConfig is not yet tested (#604), so I manually tested
that the weights in the app still function.
This commit is contained in:
Dandelion Mané 2018-09-05 11:57:20 -07:00 committed by GitHub
parent 846363465d
commit 2779770af5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 227 additions and 96 deletions

View File

@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
- Organize weight config by plugin (#773)
- Configure edge forward/backward weights separately (#749)
- Combine "load graph" and "run pagerank" into one button (#759)
- Store GitHub data compressed at rest, reducing space usage by 68× (#750)

View File

@ -9,6 +9,7 @@ import CheckedLocalStore from "../checkedLocalStore";
import BrowserLocalStore from "../browserLocalStore";
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
import {PagerankTable} from "./pagerankTable/Table";
import {WeightConfig} from "./WeightConfig";
import RepositorySelect from "./RepositorySelect";
@ -118,6 +119,7 @@ export function createApp(
</button>
<WeightConfig
onChange={(edgeEvaluator) => this.setState({edgeEvaluator})}
adapters={defaultStaticAdapters()}
/>
<LoadingIndicator appState={this.state.appState} />
{pagerankTable}

View File

@ -1,30 +1,31 @@
// @flow
import React from "react";
import sortBy from "lodash.sortby";
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import {byEdgeType, byNodeType} from "./edgeWeights";
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
import type {StaticAdapterSet} from "../adapters/adapterSet";
import {
type WeightedTypes,
PluginWeightConfig,
} from "./weights/PluginWeightConfig";
import {
NodeTypeConfig,
defaultWeightedNodeType,
type WeightedNodeType,
defaultWeightedNodeType,
} from "./weights/NodeTypeConfig";
import {
EdgeTypeConfig,
defaultWeightedEdgeType,
type WeightedEdgeType,
defaultWeightedEdgeType,
} from "./weights/EdgeTypeConfig";
import {styledVariable} from "./weights/EdgeTypeConfig";
import {FALLBACK_NAME} from "../adapters/fallbackAdapter";
type Props = {|
+adapters: StaticAdapterSet,
+onChange: (EdgeEvaluator) => void,
|};
type State = {
edgeWeights: $ReadOnlyArray<WeightedEdgeType>,
nodeWeights: $ReadOnlyArray<WeightedNodeType>,
pluginNameToWeights: Map<string, WeightedTypes>,
expanded: boolean,
};
@ -32,12 +33,7 @@ export class WeightConfig extends React.Component<Props, State> {
constructor(props: Props): void {
super(props);
this.state = {
edgeWeights: defaultStaticAdapters()
.edgeTypes()
.map(defaultWeightedEdgeType),
nodeWeights: defaultStaticAdapters()
.nodeTypes()
.map(defaultWeightedNodeType),
pluginNameToWeights: new Map(),
expanded: false,
};
}
@ -65,26 +61,49 @@ export class WeightConfig extends React.Component<Props, State> {
justifyContent: "space-between",
}}
>
<EdgeConfig
edgeWeights={this.state.edgeWeights}
onChange={(ew) =>
this.setState({edgeWeights: ew}, () => this.fire())
}
/>
<NodeConfig
nodeWeights={this.state.nodeWeights}
onChange={(nw) =>
this.setState({nodeWeights: nw}, () => this.fire())
}
/>
{this.pluginWeightConfigs()}
</div>
)}
</React.Fragment>
);
}
pluginWeightConfigs() {
return this.props.adapters
.adapters()
.filter((x) => x.name() !== FALLBACK_NAME)
.map((adapter) => {
const onChange = (weightedTypes) => {
this.state.pluginNameToWeights.set(adapter.name(), weightedTypes);
this.fire();
};
return (
<PluginWeightConfig
key={adapter.name()}
adapter={adapter}
onChange={onChange}
/>
);
});
}
fire() {
const {edgeWeights, nodeWeights} = this.state;
let nodeWeights: WeightedNodeType[] = [];
let edgeWeights: WeightedEdgeType[] = [];
for (const adapter of this.props.adapters.adapters()) {
const weights = this.state.pluginNameToWeights.get(adapter.name());
const newNodeWeights =
weights == null
? adapter.nodeTypes().map(defaultWeightedNodeType)
: weights.nodes;
const newEdgeWeights =
weights == null
? adapter.edgeTypes().map(defaultWeightedEdgeType)
: weights.edges;
nodeWeights = nodeWeights.concat(newNodeWeights);
edgeWeights = edgeWeights.concat(newEdgeWeights);
}
const edgePrefixes = edgeWeights.map(
({type, forwardWeight, backwardWeight}) => ({
prefix: type.prefix,
@ -100,71 +119,3 @@ export class WeightConfig extends React.Component<Props, State> {
this.props.onChange(edgeEvaluator);
}
}
class EdgeConfig extends React.Component<{
edgeWeights: $ReadOnlyArray<WeightedEdgeType>,
onChange: ($ReadOnlyArray<WeightedEdgeType>) => void,
}> {
_renderWeightControls() {
return sortBy(this.props.edgeWeights, ({type}) => type.prefix).map(
(weightedEdgeType) => {
const onChange = (value) => {
const edgeWeights = this.props.edgeWeights.filter(
(x) => x.type.prefix !== weightedEdgeType.type.prefix
);
edgeWeights.push(value);
this.props.onChange(edgeWeights);
};
return (
<EdgeTypeConfig
key={weightedEdgeType.type.prefix}
weightedType={weightedEdgeType}
onChange={onChange}
/>
);
}
);
}
render() {
return (
<div>
<h2>Edge weights</h2>
<p>
Flow cred from {styledVariable("β")} to {styledVariable("α")} when:
</p>
{this._renderWeightControls()}
</div>
);
}
}
class NodeConfig extends React.Component<{
nodeWeights: $ReadOnlyArray<WeightedNodeType>,
onChange: ($ReadOnlyArray<WeightedNodeType>) => void,
}> {
_renderControls() {
return sortBy(this.props.nodeWeights, ({type}) => type.prefix).map(
(weightedType) => {
const onChange = (newType) => {
const nodeWeights = this.props.nodeWeights.filter(
(x) => x.type.prefix !== weightedType.type.prefix
);
nodeWeights.push(newType);
this.props.onChange(nodeWeights);
};
return (
<NodeTypeConfig weightedType={weightedType} onChange={onChange} />
);
}
);
}
render() {
return (
<div>
<h2>Node weights</h2>
{this._renderControls()}
</div>
);
}
}

View File

@ -0,0 +1,99 @@
// @flow
import React from "react";
import deepEqual from "lodash.isequal";
import {
NodeTypeConfig,
defaultWeightedNodeType,
type WeightedNodeType,
} from "./NodeTypeConfig";
import {
EdgeTypeConfig,
defaultWeightedEdgeType,
type WeightedEdgeType,
} from "./EdgeTypeConfig";
import {StaticPluginAdapter} from "../../adapters/pluginAdapter";
import {styledVariable} from "./EdgeTypeConfig";
export type WeightedTypes = {|
+nodes: $ReadOnlyArray<WeightedNodeType>,
+edges: $ReadOnlyArray<WeightedEdgeType>,
|};
export type Props = {|
+adapter: StaticPluginAdapter,
+onChange: (WeightedTypes) => void,
|};
export type State = {|nodes: WeightedNodeType[], edges: WeightedEdgeType[]|};
export class PluginWeightConfig extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const nodes = this.props.adapter.nodeTypes().map(defaultWeightedNodeType);
const edges = this.props.adapter.edgeTypes().map(defaultWeightedEdgeType);
this.state = {nodes, edges};
}
fire() {
this.props.onChange({nodes: this.state.nodes, edges: this.state.edges});
}
componentDidMount() {
this.fire();
}
_renderNodeWeightControls() {
return this.state.nodes.map((wnt: WeightedNodeType) => {
const onChange = (newType: WeightedNodeType) => {
const index = this.state.nodes.findIndex((x) =>
deepEqual(x.type, wnt.type)
);
const newNodes = this.state.nodes.slice();
newNodes[index] = newType;
this.setState({nodes: newNodes}, () => this.fire());
};
return (
<NodeTypeConfig
key={wnt.type.prefix}
weightedType={wnt}
onChange={onChange}
/>
);
});
}
_renderEdgeWeightControls() {
return this.state.edges.map((wnt: WeightedEdgeType) => {
const onChange = (newType: WeightedEdgeType) => {
const index = this.state.edges.findIndex((x) =>
deepEqual(x.type, wnt.type)
);
const newEdges = this.state.edges.slice();
newEdges[index] = newType;
this.setState({edges: newEdges}, () => this.fire());
};
return (
<EdgeTypeConfig
key={wnt.type.prefix}
weightedType={wnt}
onChange={onChange}
/>
);
});
}
render() {
return (
<div>
<h3>{this.props.adapter.name()}</h3>
<h4 style={{marginBottom: "0.3em"}}>Node weights</h4>
{this._renderNodeWeightControls()}
<h4 style={{marginBottom: "0.3em"}}>Edge weights</h4>
<p style={{marginBottom: "0.6em", marginTop: "0.6em"}}>
Flow cred from {styledVariable("β")} to {styledVariable("α")} when:
</p>
{this._renderEdgeWeightControls()}
</div>
);
}
}

View File

@ -0,0 +1,78 @@
// @flow
import React from "react";
import {shallow} from "enzyme";
import {PluginWeightConfig} from "./PluginWeightConfig";
import {FactorioStaticAdapter} from "../../adapters/demoAdapters";
import {NodeTypeConfig, defaultWeightedNodeType} from "./NodeTypeConfig";
import {EdgeTypeConfig, defaultWeightedEdgeType} from "./EdgeTypeConfig";
require("../../testUtil").configureEnzyme();
describe("src/app/credExplorer/weights/PluginWeightConfig", () => {
describe("PluginWeightConfig", () => {
function example() {
const onChange = jest.fn();
const adapter = new FactorioStaticAdapter();
const el = shallow(
<PluginWeightConfig adapter={adapter} onChange={onChange} />
);
return {el, onChange, adapter};
}
it("fires plugin's default weights on mount", () => {
const {onChange, adapter} = example();
const expected = {
nodes: adapter.nodeTypes().map(defaultWeightedNodeType),
edges: adapter.edgeTypes().map(defaultWeightedEdgeType),
};
expect(onChange).toHaveBeenCalledWith(expected);
});
it("renders a NodeTypeConfig for each node type", () => {
const {el, adapter} = example();
const ntc = el.find(NodeTypeConfig);
const nodeTypes = adapter.nodeTypes();
for (let i = 0; i < nodeTypes.length; i++) {
const weightedType = defaultWeightedNodeType(nodeTypes[i]);
expect(ntc.at(i).props().weightedType).toEqual(weightedType);
}
});
it("renders a EdgeTypeConfig for each edge type", () => {
const {el, adapter} = example();
const ntc = el.find(EdgeTypeConfig);
const edgeTypes = adapter.edgeTypes();
for (let i = 0; i < edgeTypes.length; i++) {
const weightedType = defaultWeightedEdgeType(edgeTypes[i]);
expect(ntc.at(i).props().weightedType).toEqual(weightedType);
}
});
it("NodeTypeConfig onChange wired properly", () => {
const {el, adapter, onChange} = example();
const ntc = el.find(NodeTypeConfig).at(0);
const nodes = adapter.nodeTypes().map(defaultWeightedNodeType);
const newWeightedType = {...nodes[0], weight: 707};
const newNodes = [newWeightedType, ...nodes.slice(1)];
const expected = {
nodes: newNodes,
edges: adapter.edgeTypes().map(defaultWeightedEdgeType),
};
ntc.props().onChange(newWeightedType);
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange.mock.calls[1][0]).toEqual(expected);
});
it("EdgeTypeConfig onChange wired properly", () => {
const {el, adapter, onChange} = example();
const ntc = el.find(EdgeTypeConfig).at(0);
const edges = adapter.edgeTypes().map(defaultWeightedEdgeType);
const newWeightedType = {...edges[0], weight: 707};
const newEdges = [newWeightedType, ...edges.slice(1)];
const expected = {
nodes: adapter.nodeTypes().map(defaultWeightedNodeType),
edges: newEdges,
};
ntc.props().onChange(newWeightedType);
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange.mock.calls[1][0]).toEqual(expected);
});
});
});