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:
Dandelion Mané 2018-09-06 18:58:09 -07:00 committed by GitHub
parent b632bd6188
commit bf35bbbbda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 90 deletions

View File

@ -9,7 +9,6 @@ import BrowserLocalStore from "../browserLocalStore";
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
import {PagerankTable} from "./pagerankTable/Table";
import {WeightConfig} from "./weights/WeightConfig";
import {
type WeightedTypes,
defaultWeightsForAdapterSet,
@ -86,6 +85,10 @@ export function createApp(
<PagerankTable
defaultNodeFilter={GithubPrefix.user}
adapters={adapters}
weightedTypes={this.state.weightedTypes}
onWeightedTypesChange={(weightedTypes) =>
this.setState({weightedTypes})
}
pnd={pnd}
maxEntriesPerList={100}
/>
@ -130,11 +133,6 @@ export function createApp(
>
Analyze cred
</button>
<WeightConfig
onChange={(weightedTypes) => this.setState({weightedTypes})}
weightedTypes={this.state.weightedTypes}
adapters={this.props.adapters}
/>
<LoadingIndicator appState={this.state.appState} />
{pagerankTable}
</div>

View File

@ -13,7 +13,6 @@ import {defaultWeightsForAdapter} from "./weights/weights";
import RepositorySelect from "./RepositorySelect";
import {PagerankTable} from "./pagerankTable/Table";
import {WeightConfig} from "./weights/WeightConfig";
import {createApp, LoadingIndicator} from "./App";
import {uninitializedState} from "./state";
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}) {
const adjective = disabled ? "disabled" : "working";
it(`has a ${adjective} analyze cred button`, () => {
@ -178,8 +165,16 @@ describe("app/credExplorer/App", () => {
}
const adapters = state.graphWithAdapters.adapters;
const pnd = state.pagerankNodeDecomposition;
const weightedTypes = el.instance().state.weightedTypes;
expect(prt.props().adapters).toBe(adapters);
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 {
expect(prt).toHaveLength(0);
}
@ -203,7 +198,6 @@ describe("app/credExplorer/App", () => {
{analyzeCredDisabled, hasPagerankTable}
) {
describe(suiteName, () => {
testWeightConfig(stateFn);
testRepositorySelect(stateFn);
testAnalyzeCredButton(stateFn, {disabled: analyzeCredDisabled});
testPagerankTable(stateFn, hasPagerankTable);

View File

@ -9,16 +9,23 @@ import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerank
import {DynamicAdapterSet} from "../../adapters/adapterSet";
import type {DynamicPluginAdapter} from "../../adapters/pluginAdapter";
import {FALLBACK_NAME} from "../../adapters/fallbackAdapter";
import {type WeightedTypes} from "../weights/weights";
import {WeightConfig} from "../weights/WeightConfig";
import {NodeRowList} from "./Node";
type PagerankTableProps = {|
+pnd: PagerankNodeDecomposition,
+adapters: DynamicAdapterSet,
+weightedTypes: WeightedTypes,
+onWeightedTypesChange: (WeightedTypes) => void,
+maxEntriesPerList: number,
+defaultNodeFilter: ?NodeAddressT,
|};
type PagerankTableState = {|topLevelFilter: NodeAddressT|};
type PagerankTableState = {|
topLevelFilter: NodeAddressT,
showWeightConfig: boolean,
|};
export class PagerankTable extends React.PureComponent<
PagerankTableProps,
PagerankTableState
@ -38,13 +45,42 @@ export class PagerankTable extends React.PureComponent<
props.defaultNodeFilter,
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() {
const {showWeightConfig} = this.state;
return (
<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()}
</div>
);

View File

@ -7,42 +7,87 @@ import {NodeAddress, type NodeAddressT} from "../../../core/graph";
import {PagerankTable} from "./Table";
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();
describe("app/credExplorer/pagerankTable/Table", () => {
describe("PagerankTable", () => {
it("renders thead column order properly", async () => {
const {pnd, adapters} = await example();
async function setup(defaultNodeFilter?: NodeAddressT) {
const {pnd, adapters, weightedTypes} = await example();
const onWeightedTypesChange = jest.fn();
const maxEntriesPerList = 321;
const element = shallow(
<PagerankTable
defaultNodeFilter={null}
defaultNodeFilter={defaultNodeFilter}
weightedTypes={weightedTypes}
onWeightedTypesChange={onWeightedTypesChange}
pnd={pnd}
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 columnNames = th.map((t) => t.text());
expect(columnNames).toEqual(COLUMNS());
});
describe("has a filter select", () => {
async function setup(defaultNodeFilter?: NodeAddressT) {
const {pnd, adapters} = await example();
const element = shallow(
<PagerankTable
defaultNodeFilter={defaultNodeFilter}
pnd={pnd}
adapters={adapters}
maxEntriesPerList={1}
/>
);
const label = element.find("label");
const options = label.find("option");
return {pnd, adapters, element, label, options};
describe("has a WeightConfig", () => {
function findButton(element) {
const button = element
.findWhere(
(x) =>
x.text() === "Show weight configuration" ||
x.text() === "Hide weight configuration"
)
.find("button");
expect(button).toHaveLength(1);
return button;
}
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 () => {
const {label} = await setup();
const {element} = await setup();
const label = element.find("label");
const filterText = label
.find("span")
.first()
@ -50,7 +95,9 @@ describe("app/credExplorer/pagerankTable/Table", () => {
expect(filterText).toMatchSnapshot();
});
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) => ({
valueString: NodeAddress.toString(o.prop("value")),
style: o.prop("style"),
@ -59,7 +106,9 @@ describe("app/credExplorer/pagerankTable/Table", () => {
expect(optionsJSON).toMatchSnapshot();
});
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 value = option.prop("value");
@ -93,29 +142,17 @@ describe("app/credExplorer/pagerankTable/Table", () => {
});
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 () => {
const {nrl, adapters, pnd, maxEntriesPerList} = await setup();
const {element, adapters, pnd, maxEntriesPerList} = await setup();
const nrl = element.find(NodeRowList);
const expectedSharedProps = {adapters, pnd, maxEntriesPerList};
expect(nrl.prop("sharedProps")).toEqual(expectedSharedProps);
expect(nrl.props().sharedProps).toEqual(expectedSharedProps);
});
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());
expect(nrl.prop("nodes")).toEqual(expectedNodes);
expect(nrl.props().nodes).toEqual(expectedNodes);
});
});
});

View File

@ -2,16 +2,18 @@
import {dynamicAdapterSet} from "../../adapters/demoAdapters";
import {pagerank} from "../../../core/attribution/pagerank";
import {defaultWeightsForAdapterSet} from "../weights/weights";
export const COLUMNS = () => ["Description", "", "Cred"];
export async function example() {
const adapters = await dynamicAdapterSet();
const weightedTypes = defaultWeightsForAdapterSet(adapters.static());
const graph = adapters.graph();
const pnd = await pagerank(graph, (_unused_Edge) => ({
toWeight: 1,
froWeight: 1,
}));
return {adapters, pnd};
return {adapters, pnd, weightedTypes};
}

View File

@ -15,40 +15,23 @@ type Props = {|
+onChange: (WeightedTypes) => void,
|};
type State = {|
expanded: boolean,
|};
export class WeightConfig extends React.Component<Props, State> {
export class WeightConfig extends React.Component<Props> {
constructor(props: Props): void {
super(props);
this.state = {
expanded: false,
};
}
render() {
const {expanded} = this.state;
return (
<React.Fragment>
<button
onClick={() => {
this.setState(({expanded}) => ({expanded: !expanded}));
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
}}
>
{expanded ? "Hide weight configuration" : "Show weight configuration"}
</button>
{expanded && (
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
}}
>
{this._renderPluginWeightConfigs()}
</div>
)}
{this._renderPluginWeightConfigs()}
</div>
</React.Fragment>
);
}

View File

@ -31,8 +31,6 @@ describe("app/credExplorer/weights/WeightConfig", () => {
onChange={onChange}
/>
);
// TODO(@decentralion): WeightConfig stops expanding itself
el.setState({expanded: true});
return {el, adapters, types, onChange};
}
it("creates a PluginWeightConfig for every non-fallback adapter", () => {