diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f12f1..f731c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 6–8× (#750) diff --git a/src/app/credExplorer/App.js b/src/app/credExplorer/App.js index 1420a7a..f75a275 100644 --- a/src/app/credExplorer/App.js +++ b/src/app/credExplorer/App.js @@ -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( this.setState({edgeEvaluator})} + adapters={defaultStaticAdapters()} /> {pagerankTable} diff --git a/src/app/credExplorer/WeightConfig.js b/src/app/credExplorer/WeightConfig.js index 0672863..4f2830b 100644 --- a/src/app/credExplorer/WeightConfig.js +++ b/src/app/credExplorer/WeightConfig.js @@ -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, - nodeWeights: $ReadOnlyArray, + pluginNameToWeights: Map, expanded: boolean, }; @@ -32,12 +33,7 @@ export class WeightConfig extends React.Component { 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 { justifyContent: "space-between", }} > - - this.setState({edgeWeights: ew}, () => this.fire()) - } - /> - - this.setState({nodeWeights: nw}, () => this.fire()) - } - /> + {this.pluginWeightConfigs()} )} ); } + 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 ( + + ); + }); + } + 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 { this.props.onChange(edgeEvaluator); } } - -class EdgeConfig extends React.Component<{ - edgeWeights: $ReadOnlyArray, - onChange: ($ReadOnlyArray) => 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 ( - - ); - } - ); - } - - render() { - return ( -
-

Edge weights

-

- Flow cred from {styledVariable("β")} to {styledVariable("α")} when: -

- {this._renderWeightControls()} -
- ); - } -} - -class NodeConfig extends React.Component<{ - nodeWeights: $ReadOnlyArray, - onChange: ($ReadOnlyArray) => 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 ( - - ); - } - ); - } - render() { - return ( -
-

Node weights

- {this._renderControls()} -
- ); - } -} diff --git a/src/app/credExplorer/weights/PluginWeightConfig.js b/src/app/credExplorer/weights/PluginWeightConfig.js new file mode 100644 index 0000000..f0fb691 --- /dev/null +++ b/src/app/credExplorer/weights/PluginWeightConfig.js @@ -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, + +edges: $ReadOnlyArray, +|}; + +export type Props = {| + +adapter: StaticPluginAdapter, + +onChange: (WeightedTypes) => void, +|}; +export type State = {|nodes: WeightedNodeType[], edges: WeightedEdgeType[]|}; + +export class PluginWeightConfig extends React.Component { + 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 ( + + ); + }); + } + + _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 ( + + ); + }); + } + + render() { + return ( +
+

{this.props.adapter.name()}

+

Node weights

+ {this._renderNodeWeightControls()} +

Edge weights

+

+ Flow cred from {styledVariable("β")} to {styledVariable("α")} when: +

+ {this._renderEdgeWeightControls()} +
+ ); + } +} diff --git a/src/app/credExplorer/weights/PluginWeightConfig.test.js b/src/app/credExplorer/weights/PluginWeightConfig.test.js new file mode 100644 index 0000000..d6609dc --- /dev/null +++ b/src/app/credExplorer/weights/PluginWeightConfig.test.js @@ -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( + + ); + 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); + }); + }); +});