Move WeightConfig into PagerankTable (#798)
Previously, the WeightConfig (and the button that expanded it) were in the credExplorer App. This was a little weird, as there's no reason to play with the weights before you have some Pagerank results to investigate; additionally, it risked confusing new users with a concept that was not yet applicable. Also, the implementation was wonky: the WeightConfig had responsibility for expanding/hiding itself, which gave poor ability to position the button and the WeightConfig separately. Finally, the codepath was untested (vestiges of #604). This commit fixes all three issues: - The WeightConfig and button have moved into PagerankTable - The WeightConfig is now a stateless component, and the parent takes responsibility for deciding when to mount it - Logic for showing/hiding the WeightConfig is now tested.
This commit is contained in:
parent
b632bd6188
commit
bf35bbbbda
|
@ -9,7 +9,6 @@ import BrowserLocalStore from "../browserLocalStore";
|
||||||
|
|
||||||
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
|
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
|
||||||
import {PagerankTable} from "./pagerankTable/Table";
|
import {PagerankTable} from "./pagerankTable/Table";
|
||||||
import {WeightConfig} from "./weights/WeightConfig";
|
|
||||||
import {
|
import {
|
||||||
type WeightedTypes,
|
type WeightedTypes,
|
||||||
defaultWeightsForAdapterSet,
|
defaultWeightsForAdapterSet,
|
||||||
|
@ -86,6 +85,10 @@ export function createApp(
|
||||||
<PagerankTable
|
<PagerankTable
|
||||||
defaultNodeFilter={GithubPrefix.user}
|
defaultNodeFilter={GithubPrefix.user}
|
||||||
adapters={adapters}
|
adapters={adapters}
|
||||||
|
weightedTypes={this.state.weightedTypes}
|
||||||
|
onWeightedTypesChange={(weightedTypes) =>
|
||||||
|
this.setState({weightedTypes})
|
||||||
|
}
|
||||||
pnd={pnd}
|
pnd={pnd}
|
||||||
maxEntriesPerList={100}
|
maxEntriesPerList={100}
|
||||||
/>
|
/>
|
||||||
|
@ -130,11 +133,6 @@ export function createApp(
|
||||||
>
|
>
|
||||||
Analyze cred
|
Analyze cred
|
||||||
</button>
|
</button>
|
||||||
<WeightConfig
|
|
||||||
onChange={(weightedTypes) => this.setState({weightedTypes})}
|
|
||||||
weightedTypes={this.state.weightedTypes}
|
|
||||||
adapters={this.props.adapters}
|
|
||||||
/>
|
|
||||||
<LoadingIndicator appState={this.state.appState} />
|
<LoadingIndicator appState={this.state.appState} />
|
||||||
{pagerankTable}
|
{pagerankTable}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {defaultWeightsForAdapter} from "./weights/weights";
|
||||||
|
|
||||||
import RepositorySelect from "./RepositorySelect";
|
import RepositorySelect from "./RepositorySelect";
|
||||||
import {PagerankTable} from "./pagerankTable/Table";
|
import {PagerankTable} from "./pagerankTable/Table";
|
||||||
import {WeightConfig} from "./weights/WeightConfig";
|
|
||||||
import {createApp, LoadingIndicator} from "./App";
|
import {createApp, LoadingIndicator} from "./App";
|
||||||
import {uninitializedState} from "./state";
|
import {uninitializedState} from "./state";
|
||||||
import {_Prefix as GithubPrefix} from "../../plugins/github/nodes";
|
import {_Prefix as GithubPrefix} from "../../plugins/github/nodes";
|
||||||
|
@ -126,18 +125,6 @@ describe("app/credExplorer/App", () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function testWeightConfig(stateFn) {
|
|
||||||
it("creates a working WeightConfig", () => {
|
|
||||||
const {el, setState} = example();
|
|
||||||
setState(stateFn());
|
|
||||||
const wc = el.find(WeightConfig);
|
|
||||||
const wt = defaultWeightsForAdapter(new FactorioStaticAdapter());
|
|
||||||
wc.props().onChange(wt);
|
|
||||||
expect(el.state().weightedTypes).toBe(wt);
|
|
||||||
expect(wc.props().adapters).toBe(el.instance().props.adapters);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function testAnalyzeCredButton(stateFn, {disabled}) {
|
function testAnalyzeCredButton(stateFn, {disabled}) {
|
||||||
const adjective = disabled ? "disabled" : "working";
|
const adjective = disabled ? "disabled" : "working";
|
||||||
it(`has a ${adjective} analyze cred button`, () => {
|
it(`has a ${adjective} analyze cred button`, () => {
|
||||||
|
@ -178,8 +165,16 @@ describe("app/credExplorer/App", () => {
|
||||||
}
|
}
|
||||||
const adapters = state.graphWithAdapters.adapters;
|
const adapters = state.graphWithAdapters.adapters;
|
||||||
const pnd = state.pagerankNodeDecomposition;
|
const pnd = state.pagerankNodeDecomposition;
|
||||||
|
const weightedTypes = el.instance().state.weightedTypes;
|
||||||
expect(prt.props().adapters).toBe(adapters);
|
expect(prt.props().adapters).toBe(adapters);
|
||||||
expect(prt.props().pnd).toBe(pnd);
|
expect(prt.props().pnd).toBe(pnd);
|
||||||
|
expect(prt.props().weightedTypes).toBe(weightedTypes);
|
||||||
|
const prtWeightedTypesChange = prt.props().onWeightedTypesChange;
|
||||||
|
const newTypes = defaultWeightsForAdapter(
|
||||||
|
new FactorioStaticAdapter()
|
||||||
|
);
|
||||||
|
prtWeightedTypesChange(newTypes);
|
||||||
|
expect(el.instance().state.weightedTypes).toBe(newTypes);
|
||||||
} else {
|
} else {
|
||||||
expect(prt).toHaveLength(0);
|
expect(prt).toHaveLength(0);
|
||||||
}
|
}
|
||||||
|
@ -203,7 +198,6 @@ describe("app/credExplorer/App", () => {
|
||||||
{analyzeCredDisabled, hasPagerankTable}
|
{analyzeCredDisabled, hasPagerankTable}
|
||||||
) {
|
) {
|
||||||
describe(suiteName, () => {
|
describe(suiteName, () => {
|
||||||
testWeightConfig(stateFn);
|
|
||||||
testRepositorySelect(stateFn);
|
testRepositorySelect(stateFn);
|
||||||
testAnalyzeCredButton(stateFn, {disabled: analyzeCredDisabled});
|
testAnalyzeCredButton(stateFn, {disabled: analyzeCredDisabled});
|
||||||
testPagerankTable(stateFn, hasPagerankTable);
|
testPagerankTable(stateFn, hasPagerankTable);
|
||||||
|
|
|
@ -9,16 +9,23 @@ import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerank
|
||||||
import {DynamicAdapterSet} from "../../adapters/adapterSet";
|
import {DynamicAdapterSet} from "../../adapters/adapterSet";
|
||||||
import type {DynamicPluginAdapter} from "../../adapters/pluginAdapter";
|
import type {DynamicPluginAdapter} from "../../adapters/pluginAdapter";
|
||||||
import {FALLBACK_NAME} from "../../adapters/fallbackAdapter";
|
import {FALLBACK_NAME} from "../../adapters/fallbackAdapter";
|
||||||
|
import {type WeightedTypes} from "../weights/weights";
|
||||||
|
import {WeightConfig} from "../weights/WeightConfig";
|
||||||
|
|
||||||
import {NodeRowList} from "./Node";
|
import {NodeRowList} from "./Node";
|
||||||
|
|
||||||
type PagerankTableProps = {|
|
type PagerankTableProps = {|
|
||||||
+pnd: PagerankNodeDecomposition,
|
+pnd: PagerankNodeDecomposition,
|
||||||
+adapters: DynamicAdapterSet,
|
+adapters: DynamicAdapterSet,
|
||||||
|
+weightedTypes: WeightedTypes,
|
||||||
|
+onWeightedTypesChange: (WeightedTypes) => void,
|
||||||
+maxEntriesPerList: number,
|
+maxEntriesPerList: number,
|
||||||
+defaultNodeFilter: ?NodeAddressT,
|
+defaultNodeFilter: ?NodeAddressT,
|
||||||
|};
|
|};
|
||||||
type PagerankTableState = {|topLevelFilter: NodeAddressT|};
|
type PagerankTableState = {|
|
||||||
|
topLevelFilter: NodeAddressT,
|
||||||
|
showWeightConfig: boolean,
|
||||||
|
|};
|
||||||
export class PagerankTable extends React.PureComponent<
|
export class PagerankTable extends React.PureComponent<
|
||||||
PagerankTableProps,
|
PagerankTableProps,
|
||||||
PagerankTableState
|
PagerankTableState
|
||||||
|
@ -38,13 +45,42 @@ export class PagerankTable extends React.PureComponent<
|
||||||
props.defaultNodeFilter,
|
props.defaultNodeFilter,
|
||||||
NodeAddress.empty
|
NodeAddress.empty
|
||||||
);
|
);
|
||||||
this.state = {topLevelFilter};
|
this.state = {topLevelFilter, showWeightConfig: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderConfigurationRow() {
|
||||||
|
const {showWeightConfig} = this.state;
|
||||||
|
return (
|
||||||
|
<div style={{display: "flex"}}>
|
||||||
|
{this.renderFilterSelect()}
|
||||||
|
<span style={{flexGrow: 1}} />
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
this.setState(({showWeightConfig}) => ({
|
||||||
|
showWeightConfig: !showWeightConfig,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showWeightConfig
|
||||||
|
? "Hide weight configuration"
|
||||||
|
: "Show weight configuration"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {showWeightConfig} = this.state;
|
||||||
return (
|
return (
|
||||||
<div style={{marginTop: 10}}>
|
<div style={{marginTop: 10}}>
|
||||||
{this.renderFilterSelect()}
|
{this.renderConfigurationRow()}
|
||||||
|
{showWeightConfig && (
|
||||||
|
<WeightConfig
|
||||||
|
adapters={this.props.adapters.static()}
|
||||||
|
weightedTypes={this.props.weightedTypes}
|
||||||
|
onChange={(wt) => this.props.onWeightedTypesChange(wt)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{this.renderTable()}
|
{this.renderTable()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,42 +7,87 @@ import {NodeAddress, type NodeAddressT} from "../../../core/graph";
|
||||||
|
|
||||||
import {PagerankTable} from "./Table";
|
import {PagerankTable} from "./Table";
|
||||||
import {example, COLUMNS} from "./sharedTestUtils";
|
import {example, COLUMNS} from "./sharedTestUtils";
|
||||||
|
import {NodeRowList} from "./Node";
|
||||||
|
import {WeightConfig} from "../weights/WeightConfig";
|
||||||
|
import {defaultWeightsForAdapter} from "../weights/weights";
|
||||||
|
import {FactorioStaticAdapter} from "../../adapters/demoAdapters";
|
||||||
|
|
||||||
require("../../testUtil").configureEnzyme();
|
require("../../testUtil").configureEnzyme();
|
||||||
describe("app/credExplorer/pagerankTable/Table", () => {
|
describe("app/credExplorer/pagerankTable/Table", () => {
|
||||||
describe("PagerankTable", () => {
|
describe("PagerankTable", () => {
|
||||||
it("renders thead column order properly", async () => {
|
async function setup(defaultNodeFilter?: NodeAddressT) {
|
||||||
const {pnd, adapters} = await example();
|
const {pnd, adapters, weightedTypes} = await example();
|
||||||
|
const onWeightedTypesChange = jest.fn();
|
||||||
|
const maxEntriesPerList = 321;
|
||||||
const element = shallow(
|
const element = shallow(
|
||||||
<PagerankTable
|
<PagerankTable
|
||||||
defaultNodeFilter={null}
|
defaultNodeFilter={defaultNodeFilter}
|
||||||
|
weightedTypes={weightedTypes}
|
||||||
|
onWeightedTypesChange={onWeightedTypesChange}
|
||||||
pnd={pnd}
|
pnd={pnd}
|
||||||
adapters={adapters}
|
adapters={adapters}
|
||||||
maxEntriesPerList={1}
|
maxEntriesPerList={maxEntriesPerList}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
return {pnd, adapters, element, maxEntriesPerList, onWeightedTypesChange};
|
||||||
|
}
|
||||||
|
it("renders thead column order properly", async () => {
|
||||||
|
const {element} = await setup();
|
||||||
const th = element.find("thead th");
|
const th = element.find("thead th");
|
||||||
const columnNames = th.map((t) => t.text());
|
const columnNames = th.map((t) => t.text());
|
||||||
expect(columnNames).toEqual(COLUMNS());
|
expect(columnNames).toEqual(COLUMNS());
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("has a filter select", () => {
|
describe("has a WeightConfig", () => {
|
||||||
async function setup(defaultNodeFilter?: NodeAddressT) {
|
function findButton(element) {
|
||||||
const {pnd, adapters} = await example();
|
const button = element
|
||||||
const element = shallow(
|
.findWhere(
|
||||||
<PagerankTable
|
(x) =>
|
||||||
defaultNodeFilter={defaultNodeFilter}
|
x.text() === "Show weight configuration" ||
|
||||||
pnd={pnd}
|
x.text() === "Hide weight configuration"
|
||||||
adapters={adapters}
|
)
|
||||||
maxEntriesPerList={1}
|
.find("button");
|
||||||
/>
|
expect(button).toHaveLength(1);
|
||||||
);
|
return button;
|
||||||
const label = element.find("label");
|
|
||||||
const options = label.find("option");
|
|
||||||
return {pnd, adapters, element, label, options};
|
|
||||||
}
|
}
|
||||||
|
it("which is not present by default", async () => {
|
||||||
|
const {element} = await setup();
|
||||||
|
expect(element.find(WeightConfig)).toHaveLength(0);
|
||||||
|
const button = findButton(element);
|
||||||
|
expect(button.text()).toEqual("Show weight configuration");
|
||||||
|
});
|
||||||
|
it("which is present when the WeightConfig button is pushed", async () => {
|
||||||
|
const {element, onWeightedTypesChange} = await setup();
|
||||||
|
let button = findButton(element);
|
||||||
|
expect(button.text()).toEqual("Show weight configuration");
|
||||||
|
button.simulate("click");
|
||||||
|
button = findButton(element);
|
||||||
|
expect(button.text()).toEqual("Hide weight configuration");
|
||||||
|
expect(button).toHaveLength(1); // Its text changed
|
||||||
|
const wc = element.find(WeightConfig);
|
||||||
|
expect(wc).toHaveLength(1);
|
||||||
|
expect(wc.props().weightedTypes).toBe(
|
||||||
|
element.instance().props.weightedTypes
|
||||||
|
);
|
||||||
|
const wt = defaultWeightsForAdapter(new FactorioStaticAdapter());
|
||||||
|
wc.props().onChange(wt);
|
||||||
|
expect(onWeightedTypesChange).toHaveBeenCalledWith(wt);
|
||||||
|
expect(onWeightedTypesChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
it("which is hidden when the WeightConfig button is pushed twice", async () => {
|
||||||
|
const {element} = await setup();
|
||||||
|
findButton(element).simulate("click");
|
||||||
|
findButton(element).simulate("click");
|
||||||
|
let button = findButton(element);
|
||||||
|
expect(button.text()).toEqual("Show weight configuration");
|
||||||
|
expect(element.find(WeightConfig)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("has a filter select", () => {
|
||||||
it("with expected label text", async () => {
|
it("with expected label text", async () => {
|
||||||
const {label} = await setup();
|
const {element} = await setup();
|
||||||
|
const label = element.find("label");
|
||||||
const filterText = label
|
const filterText = label
|
||||||
.find("span")
|
.find("span")
|
||||||
.first()
|
.first()
|
||||||
|
@ -50,7 +95,9 @@ describe("app/credExplorer/pagerankTable/Table", () => {
|
||||||
expect(filterText).toMatchSnapshot();
|
expect(filterText).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("with expected option groups", async () => {
|
it("with expected option groups", async () => {
|
||||||
const {options} = await setup();
|
const {element} = await setup();
|
||||||
|
const label = element.find("label");
|
||||||
|
const options = label.find("option");
|
||||||
const optionsJSON = options.map((o) => ({
|
const optionsJSON = options.map((o) => ({
|
||||||
valueString: NodeAddress.toString(o.prop("value")),
|
valueString: NodeAddress.toString(o.prop("value")),
|
||||||
style: o.prop("style"),
|
style: o.prop("style"),
|
||||||
|
@ -59,7 +106,9 @@ describe("app/credExplorer/pagerankTable/Table", () => {
|
||||||
expect(optionsJSON).toMatchSnapshot();
|
expect(optionsJSON).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("with the ability to filter nodes passed to NodeRowList", async () => {
|
it("with the ability to filter nodes passed to NodeRowList", async () => {
|
||||||
const {element, options} = await setup();
|
const {element} = await setup();
|
||||||
|
const label = element.find("label");
|
||||||
|
const options = label.find("option");
|
||||||
const option = options.at(2);
|
const option = options.at(2);
|
||||||
|
|
||||||
const value = option.prop("value");
|
const value = option.prop("value");
|
||||||
|
@ -93,29 +142,17 @@ describe("app/credExplorer/pagerankTable/Table", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("creates a NodeRowList", () => {
|
describe("creates a NodeRowList", () => {
|
||||||
async function setup() {
|
|
||||||
const {adapters, pnd} = await example();
|
|
||||||
const maxEntriesPerList = 1;
|
|
||||||
const element = shallow(
|
|
||||||
<PagerankTable
|
|
||||||
defaultNodeFilter={null}
|
|
||||||
pnd={pnd}
|
|
||||||
adapters={adapters}
|
|
||||||
maxEntriesPerList={maxEntriesPerList}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const nrl = element.find("NodeRowList");
|
|
||||||
return {adapters, pnd, element, nrl, maxEntriesPerList};
|
|
||||||
}
|
|
||||||
it("with the correct SharedProps", async () => {
|
it("with the correct SharedProps", async () => {
|
||||||
const {nrl, adapters, pnd, maxEntriesPerList} = await setup();
|
const {element, adapters, pnd, maxEntriesPerList} = await setup();
|
||||||
|
const nrl = element.find(NodeRowList);
|
||||||
const expectedSharedProps = {adapters, pnd, maxEntriesPerList};
|
const expectedSharedProps = {adapters, pnd, maxEntriesPerList};
|
||||||
expect(nrl.prop("sharedProps")).toEqual(expectedSharedProps);
|
expect(nrl.props().sharedProps).toEqual(expectedSharedProps);
|
||||||
});
|
});
|
||||||
it("including all nodes by default", async () => {
|
it("including all nodes by default", async () => {
|
||||||
const {nrl, pnd} = await setup();
|
const {element, pnd} = await setup();
|
||||||
|
const nrl = element.find(NodeRowList);
|
||||||
const expectedNodes = Array.from(pnd.keys());
|
const expectedNodes = Array.from(pnd.keys());
|
||||||
expect(nrl.prop("nodes")).toEqual(expectedNodes);
|
expect(nrl.props().nodes).toEqual(expectedNodes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,16 +2,18 @@
|
||||||
|
|
||||||
import {dynamicAdapterSet} from "../../adapters/demoAdapters";
|
import {dynamicAdapterSet} from "../../adapters/demoAdapters";
|
||||||
import {pagerank} from "../../../core/attribution/pagerank";
|
import {pagerank} from "../../../core/attribution/pagerank";
|
||||||
|
import {defaultWeightsForAdapterSet} from "../weights/weights";
|
||||||
|
|
||||||
export const COLUMNS = () => ["Description", "", "Cred"];
|
export const COLUMNS = () => ["Description", "", "Cred"];
|
||||||
|
|
||||||
export async function example() {
|
export async function example() {
|
||||||
const adapters = await dynamicAdapterSet();
|
const adapters = await dynamicAdapterSet();
|
||||||
|
const weightedTypes = defaultWeightsForAdapterSet(adapters.static());
|
||||||
const graph = adapters.graph();
|
const graph = adapters.graph();
|
||||||
const pnd = await pagerank(graph, (_unused_Edge) => ({
|
const pnd = await pagerank(graph, (_unused_Edge) => ({
|
||||||
toWeight: 1,
|
toWeight: 1,
|
||||||
froWeight: 1,
|
froWeight: 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {adapters, pnd};
|
return {adapters, pnd, weightedTypes};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,30 +15,14 @@ type Props = {|
|
||||||
+onChange: (WeightedTypes) => void,
|
+onChange: (WeightedTypes) => void,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
type State = {|
|
export class WeightConfig extends React.Component<Props> {
|
||||||
expanded: boolean,
|
|
||||||
|};
|
|
||||||
|
|
||||||
export class WeightConfig extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props): void {
|
constructor(props: Props): void {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
|
||||||
expanded: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {expanded} = this.state;
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
this.setState(({expanded}) => ({expanded: !expanded}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{expanded ? "Hide weight configuration" : "Show weight configuration"}
|
|
||||||
</button>
|
|
||||||
{expanded && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -48,7 +32,6 @@ export class WeightConfig extends React.Component<Props, State> {
|
||||||
>
|
>
|
||||||
{this._renderPluginWeightConfigs()}
|
{this._renderPluginWeightConfigs()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,6 @@ describe("app/credExplorer/weights/WeightConfig", () => {
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
// TODO(@decentralion): WeightConfig stops expanding itself
|
|
||||||
el.setState({expanded: true});
|
|
||||||
return {el, adapters, types, onChange};
|
return {el, adapters, types, onChange};
|
||||||
}
|
}
|
||||||
it("creates a PluginWeightConfig for every non-fallback adapter", () => {
|
it("creates a PluginWeightConfig for every non-fallback adapter", () => {
|
||||||
|
|
Loading…
Reference in New Issue