PagerankTable: Replace topLevelFilter with NodeType in props (#1103)
The motivation for this change is to make it easier to access the selected Node's `name` prop for #576, in which we plan to show a Card displaying summary stats for the selected node. With only the `topLevelFilter` available, it's trickier than it needs to be to find out a node type's `name`. Test Plan: * Yarn test passes. * Visual/Manual inspection of table doesn't surface any issues. * Updated `it("filter defaults to defaultNodeFilter if available")` to `it("selectedNodeType defaults to defaultNodeType if available")`. * Verified that the above new test is failable in several ways by mangling the tests to test for the wrong node type and mangling the code to set the wrong node type. * Since we factored out 'topLevelFilter' and 'defaultNodeFilter', running `git grep -i topLevelFilter` and `git grep -i defaultNodeFilter` turns up empty, just to make sure those terms aren't hanging around to confuse anybody in the future. * I don't think changing the `prop` parameter warrants any additional tests, as the current tests verify that the prop is passed in correctly. This was at @decentralion's suggestion, following the Contributing Guideline's Kent Beck quote of making the easy change to make the change we were originally after (#576) easier. 🙌
This commit is contained in:
parent
c353efff36
commit
42669cd160
|
@ -20,6 +20,7 @@ import {
|
||||||
initialState,
|
initialState,
|
||||||
} from "./state";
|
} from "./state";
|
||||||
import {StaticExplorerAdapterSet} from "./adapters/explorerAdapterSet";
|
import {StaticExplorerAdapterSet} from "./adapters/explorerAdapterSet";
|
||||||
|
import {userNodeType} from "../plugins/github/declaration";
|
||||||
|
|
||||||
const credOverviewUrl =
|
const credOverviewUrl =
|
||||||
"https://discuss.sourcecred.io/t/a-gentle-introduction-to-cred/20";
|
"https://discuss.sourcecred.io/t/a-gentle-introduction-to-cred/20";
|
||||||
|
@ -91,7 +92,7 @@ export function createApp(
|
||||||
const pnd = appState.pagerankNodeDecomposition;
|
const pnd = appState.pagerankNodeDecomposition;
|
||||||
pagerankTable = (
|
pagerankTable = (
|
||||||
<PagerankTable
|
<PagerankTable
|
||||||
defaultNodeFilter={GithubPrefix.user}
|
defaultNodeType={userNodeType}
|
||||||
adapters={adapters}
|
adapters={adapters}
|
||||||
weightedTypes={this.state.weightedTypes}
|
weightedTypes={this.state.weightedTypes}
|
||||||
onWeightedTypesChange={(weightedTypes) =>
|
onWeightedTypesChange={(weightedTypes) =>
|
||||||
|
|
|
@ -4,15 +4,16 @@ import React from "react";
|
||||||
import sortBy from "lodash.sortby";
|
import sortBy from "lodash.sortby";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
import {type NodeAddressT, NodeAddress} from "../../core/graph";
|
import {NodeAddress} from "../../core/graph";
|
||||||
import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition";
|
import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition";
|
||||||
import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
|
import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
|
||||||
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
|
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
|
||||||
import {FALLBACK_NAME} from "../../analysis/fallbackDeclaration";
|
import {FALLBACK_NAME} from "../../analysis/fallbackDeclaration";
|
||||||
import type {WeightedTypes} from "../../analysis/weights";
|
import type {WeightedTypes} from "../../analysis/weights";
|
||||||
import {WeightConfig} from "../weights/WeightConfig";
|
import {WeightConfig} from "../weights/WeightConfig";
|
||||||
|
|
||||||
import {NodeRowList} from "./Node";
|
import {NodeRowList} from "./Node";
|
||||||
|
import {type NodeType} from "../../analysis/types";
|
||||||
|
import {fallbackNodeType} from "../../analysis/fallbackDeclaration";
|
||||||
|
|
||||||
type PagerankTableProps = {|
|
type PagerankTableProps = {|
|
||||||
+pnd: PagerankNodeDecomposition,
|
+pnd: PagerankNodeDecomposition,
|
||||||
|
@ -20,10 +21,10 @@ type PagerankTableProps = {|
|
||||||
+weightedTypes: WeightedTypes,
|
+weightedTypes: WeightedTypes,
|
||||||
+onWeightedTypesChange: (WeightedTypes) => void,
|
+onWeightedTypesChange: (WeightedTypes) => void,
|
||||||
+maxEntriesPerList: number,
|
+maxEntriesPerList: number,
|
||||||
+defaultNodeFilter: ?NodeAddressT,
|
+defaultNodeType: ?NodeType,
|
||||||
|};
|
|};
|
||||||
type PagerankTableState = {|
|
type PagerankTableState = {|
|
||||||
topLevelFilter: NodeAddressT,
|
selectedNodeType: NodeType,
|
||||||
showWeightConfig: boolean,
|
showWeightConfig: boolean,
|
||||||
|};
|
|};
|
||||||
export class PagerankTable extends React.PureComponent<
|
export class PagerankTable extends React.PureComponent<
|
||||||
|
@ -32,20 +33,11 @@ export class PagerankTable extends React.PureComponent<
|
||||||
> {
|
> {
|
||||||
constructor(props: PagerankTableProps): void {
|
constructor(props: PagerankTableProps): void {
|
||||||
super();
|
super();
|
||||||
const {defaultNodeFilter, adapters} = props;
|
const selectedNodeType = NullUtil.orElse(
|
||||||
if (defaultNodeFilter != null) {
|
props.defaultNodeType,
|
||||||
const nodeTypes = adapters.static().nodeTypes();
|
fallbackNodeType
|
||||||
if (!nodeTypes.some((x) => x.prefix === defaultNodeFilter)) {
|
|
||||||
throw new Error(
|
|
||||||
`invalid defaultNodeFilter ${defaultNodeFilter}: doesn't match any type`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const topLevelFilter = NullUtil.orElse(
|
|
||||||
props.defaultNodeFilter,
|
|
||||||
NodeAddress.empty
|
|
||||||
);
|
);
|
||||||
this.state = {topLevelFilter, showWeightConfig: false};
|
this.state = {selectedNodeType, showWeightConfig: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderConfigurationRow() {
|
renderConfigurationRow() {
|
||||||
|
@ -116,9 +108,11 @@ export class PagerankTable extends React.PureComponent<
|
||||||
<label>
|
<label>
|
||||||
<span>Filter by node type: </span>
|
<span>Filter by node type: </span>
|
||||||
<select
|
<select
|
||||||
value={this.state.topLevelFilter}
|
value={this.state.selectedNodeType.prefix}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
this.setState({topLevelFilter: e.target.value});
|
const nodePrefix = e.target.value;
|
||||||
|
const nodeType = adapters.static().typeMatchingNode(nodePrefix);
|
||||||
|
this.setState({selectedNodeType: nodeType});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value={NodeAddress.empty}>Show all</option>
|
<option value={NodeAddress.empty}>Show all</option>
|
||||||
|
@ -138,7 +132,7 @@ export class PagerankTable extends React.PureComponent<
|
||||||
if (pnd == null || adapters == null || maxEntriesPerList == null) {
|
if (pnd == null || adapters == null || maxEntriesPerList == null) {
|
||||||
throw new Error("Impossible.");
|
throw new Error("Impossible.");
|
||||||
}
|
}
|
||||||
const topLevelFilter = this.state.topLevelFilter;
|
const selectedNodeTypePrefix = this.state.selectedNodeType.prefix;
|
||||||
const sharedProps = {pnd, adapters, maxEntriesPerList};
|
const sharedProps = {pnd, adapters, maxEntriesPerList};
|
||||||
return (
|
return (
|
||||||
<table
|
<table
|
||||||
|
@ -161,7 +155,7 @@ export class PagerankTable extends React.PureComponent<
|
||||||
<NodeRowList
|
<NodeRowList
|
||||||
sharedProps={sharedProps}
|
sharedProps={sharedProps}
|
||||||
nodes={Array.from(pnd.keys()).filter((node) =>
|
nodes={Array.from(pnd.keys()).filter((node) =>
|
||||||
NodeAddress.hasPrefix(node, topLevelFilter)
|
NodeAddress.hasPrefix(node, selectedNodeTypePrefix)
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {shallow} from "enzyme";
|
import {shallow} from "enzyme";
|
||||||
|
|
||||||
import {NodeAddress, type NodeAddressT} from "../../core/graph";
|
import {NodeAddress} from "../../core/graph";
|
||||||
|
|
||||||
import {PagerankTable} from "./Table";
|
import {PagerankTable} from "./Table";
|
||||||
import {example, COLUMNS} from "./sharedTestUtils";
|
import {example, COLUMNS} from "./sharedTestUtils";
|
||||||
|
@ -11,17 +11,19 @@ import {NodeRowList} from "./Node";
|
||||||
import {WeightConfig} from "../weights/WeightConfig";
|
import {WeightConfig} from "../weights/WeightConfig";
|
||||||
import {defaultWeightsForAdapter} from "../weights/weights";
|
import {defaultWeightsForAdapter} from "../weights/weights";
|
||||||
import {FactorioStaticAdapter} from "../../plugins/demo/explorerAdapter";
|
import {FactorioStaticAdapter} from "../../plugins/demo/explorerAdapter";
|
||||||
|
import {type NodeType} from "../../analysis/types";
|
||||||
|
import {fallbackNodeType} from "../../analysis/fallbackDeclaration";
|
||||||
|
|
||||||
require("../../webutil/testUtil").configureEnzyme();
|
require("../../webutil/testUtil").configureEnzyme();
|
||||||
describe("explorer/pagerankTable/Table", () => {
|
describe("explorer/pagerankTable/Table", () => {
|
||||||
describe("PagerankTable", () => {
|
describe("PagerankTable", () => {
|
||||||
async function setup(defaultNodeFilter?: NodeAddressT) {
|
async function setup(defaultNodeType?: NodeType) {
|
||||||
const {pnd, adapters, weightedTypes} = await example();
|
const {pnd, adapters, weightedTypes} = await example();
|
||||||
const onWeightedTypesChange = jest.fn();
|
const onWeightedTypesChange = jest.fn();
|
||||||
const maxEntriesPerList = 321;
|
const maxEntriesPerList = 321;
|
||||||
const element = shallow(
|
const element = shallow(
|
||||||
<PagerankTable
|
<PagerankTable
|
||||||
defaultNodeFilter={defaultNodeFilter}
|
defaultNodeType={defaultNodeType}
|
||||||
weightedTypes={weightedTypes}
|
weightedTypes={weightedTypes}
|
||||||
onWeightedTypesChange={onWeightedTypesChange}
|
onWeightedTypesChange={onWeightedTypesChange}
|
||||||
pnd={pnd}
|
pnd={pnd}
|
||||||
|
@ -124,20 +126,20 @@ describe("explorer/pagerankTable/Table", () => {
|
||||||
);
|
);
|
||||||
expect(actualNodes).not.toHaveLength(0);
|
expect(actualNodes).not.toHaveLength(0);
|
||||||
});
|
});
|
||||||
it("filter defaults to show all if defaultNodeFilter not passed", async () => {
|
it("filter defaults to show all if defaultNodeType not passed", async () => {
|
||||||
const {element} = await setup();
|
const {element} = await setup();
|
||||||
expect(element.state().topLevelFilter).toEqual(NodeAddress.empty);
|
expect(element.state().selectedNodeType).toEqual(fallbackNodeType);
|
||||||
});
|
});
|
||||||
it("filter defaults to defaultNodeFilter if available", async () => {
|
it("selectedNodeType defaults to defaultNodeType if available", async () => {
|
||||||
const filter = NodeAddress.fromParts(["factorio", "inserter"]);
|
const nodeType: NodeType = {
|
||||||
const {element} = await setup(filter);
|
name: "testNodeType",
|
||||||
expect(element.state().topLevelFilter).toEqual(filter);
|
pluralName: "testNodeTypes",
|
||||||
});
|
prefix: NodeAddress.empty,
|
||||||
it("raises an error if defaultNodeFilter doesn't match any node type", async () => {
|
defaultWeight: 1,
|
||||||
const badFilter = NodeAddress.fromParts(["doesn't", "exist"]);
|
description: "test type",
|
||||||
await expect(setup(badFilter)).rejects.toThrow(
|
};
|
||||||
"invalid defaultNodeFilter"
|
const {element} = await setup(nodeType);
|
||||||
);
|
expect(element.state().selectedNodeType).toEqual(nodeType);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ const commentNodeType = Object.freeze({
|
||||||
description: "NodeType for a GitHub comment",
|
description: "NodeType for a GitHub comment",
|
||||||
});
|
});
|
||||||
|
|
||||||
const userNodeType = Object.freeze({
|
export const userNodeType = Object.freeze({
|
||||||
name: "User",
|
name: "User",
|
||||||
pluralName: "Users",
|
pluralName: "Users",
|
||||||
prefix: N.Prefix.user,
|
prefix: N.Prefix.user,
|
||||||
|
|
Loading…
Reference in New Issue