Display edges in Cred Explorer (#489)
Previously, when expanding a node in the cred explorer, it would display the neighboring nodes, but not any information about the edges linking to that node. If the same node was reached by multiple edges, this information was not communicated to the user. As of this commit, it now concisely communicates what kind of edge was connecting the chosen node to its adjacencies. There's a new `edgeVerb` method that plugin adapters must implement, which gives a direction-based verb descriptiong of the edge, e.g. "authors" or "is authored by". Test plan: Unit tests added to the PagerankTable tests, and hand inspection. Paired with @wchargin
This commit is contained in:
parent
9b13f3d5bd
commit
8921b5b942
|
@ -7,8 +7,11 @@ import {
|
|||
Graph,
|
||||
NodeAddress,
|
||||
type NodeAddressT,
|
||||
type Neighbor,
|
||||
Direction,
|
||||
type Edge,
|
||||
EdgeAddress,
|
||||
type EdgeAddressT,
|
||||
} from "../../core/graph";
|
||||
import type {PagerankResult} from "../../core/attribution/pagerank";
|
||||
import type {PluginAdapter} from "../pluginAdapter";
|
||||
|
@ -48,6 +51,44 @@ export function nodeDescription(
|
|||
}
|
||||
}
|
||||
|
||||
function edgeVerb(
|
||||
address: EdgeAddressT,
|
||||
direction: "FORWARD" | "BACKWARD",
|
||||
adapters: $ReadOnlyArray<PluginAdapter>
|
||||
): string {
|
||||
const adapter = adapters.find((adapter) =>
|
||||
EdgeAddress.hasPrefix(address, adapter.edgePrefix())
|
||||
);
|
||||
if (adapter == null) {
|
||||
const result = EdgeAddress.toString(address);
|
||||
console.warn(`No adapter for ${result}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.renderer().edgeVerb(address, direction);
|
||||
} catch (e) {
|
||||
const result = EdgeAddress.toString(address);
|
||||
console.error(`Error getting description for ${result}: ${e.message}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function neighborVerb(
|
||||
{node, edge}: Neighbor,
|
||||
adapters: $ReadOnlyArray<PluginAdapter>
|
||||
): string {
|
||||
const forwardVerb = edgeVerb(edge.address, "FORWARD", adapters);
|
||||
const backwardVerb = edgeVerb(edge.address, "BACKWARD", adapters);
|
||||
if (edge.src === edge.dst) {
|
||||
return `${forwardVerb} and ${backwardVerb}`;
|
||||
} else if (edge.dst === node) {
|
||||
return forwardVerb;
|
||||
} else {
|
||||
return backwardVerb;
|
||||
}
|
||||
}
|
||||
|
||||
export class PagerankTable extends React.PureComponent<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -132,7 +173,7 @@ export class PagerankTable extends React.PureComponent<Props, State> {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RecursiveTables
|
||||
<NodesTables
|
||||
addresses={
|
||||
topLevelFilter == null
|
||||
? Array.from(graph.nodes())
|
||||
|
@ -153,7 +194,9 @@ export class PagerankTable extends React.PureComponent<Props, State> {
|
|||
|
||||
type RTState = {expanded: boolean};
|
||||
type RTProps = {|
|
||||
+address: NodeAddressT,
|
||||
+node: NodeAddressT,
|
||||
// Present if this RT shows a neighbor (not a top-level node)
|
||||
+edge: ?Edge,
|
||||
+graph: Graph,
|
||||
+pagerankResult: PagerankResult,
|
||||
+depth: number,
|
||||
|
@ -167,13 +210,15 @@ class RecursiveTable extends React.PureComponent<RTProps, RTState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {address, adapters, depth, graph, pagerankResult} = this.props;
|
||||
const {node, edge, adapters, depth, graph, pagerankResult} = this.props;
|
||||
const {expanded} = this.state;
|
||||
const probability = pagerankResult.get(address);
|
||||
const probability = pagerankResult.get(node);
|
||||
if (probability == null) {
|
||||
throw new Error(`no PageRank value for ${NodeAddress.toString(address)}`);
|
||||
throw new Error(`no PageRank value for ${NodeAddress.toString(node)}`);
|
||||
}
|
||||
const modifiedLogScore = Math.log(probability) + 10;
|
||||
const edgeVerbString =
|
||||
edge == null ? null : neighborVerb({node, edge}, adapters);
|
||||
return [
|
||||
<tr
|
||||
key="self"
|
||||
|
@ -193,23 +238,35 @@ class RecursiveTable extends React.PureComponent<RTProps, RTState> {
|
|||
>
|
||||
{expanded ? "\u2212" : "+"}
|
||||
</button>
|
||||
<span>{nodeDescription(address, adapters)}</span>
|
||||
<span>
|
||||
{edgeVerbString != null && (
|
||||
<React.Fragment>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 700,
|
||||
fontSize: "smaller",
|
||||
}}
|
||||
>
|
||||
{edgeVerbString}
|
||||
</span>{" "}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{nodeDescription(node, adapters)}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{textAlign: "right"}}>{modifiedLogScore.toFixed(2)}</td>
|
||||
</tr>,
|
||||
expanded && (
|
||||
<RecursiveTables
|
||||
<NeighborsTables
|
||||
key="children"
|
||||
addresses={Array.from(
|
||||
new Set( // deduplicate same node reached by several edges
|
||||
Array.from(
|
||||
graph.neighbors(address, {
|
||||
direction: Direction.ANY,
|
||||
nodePrefix: NodeAddress.empty,
|
||||
edgePrefix: EdgeAddress.empty,
|
||||
})
|
||||
).map((neighbor) => neighbor.node)
|
||||
)
|
||||
neighbors={Array.from(
|
||||
graph.neighbors(node, {
|
||||
direction: Direction.ANY,
|
||||
nodePrefix: NodeAddress.empty,
|
||||
edgePrefix: EdgeAddress.empty,
|
||||
})
|
||||
)}
|
||||
graph={graph}
|
||||
pagerankResult={pagerankResult}
|
||||
|
@ -221,7 +278,7 @@ class RecursiveTable extends React.PureComponent<RTProps, RTState> {
|
|||
}
|
||||
}
|
||||
|
||||
type RecursiveTablesProps = {|
|
||||
type NodesTablesProps = {|
|
||||
+addresses: $ReadOnlyArray<NodeAddressT>,
|
||||
+graph: Graph,
|
||||
+pagerankResult: PagerankResult,
|
||||
|
@ -229,27 +286,26 @@ type RecursiveTablesProps = {|
|
|||
+adapters: $ReadOnlyArray<PluginAdapter>,
|
||||
|};
|
||||
|
||||
class RecursiveTables extends React.PureComponent<RecursiveTablesProps> {
|
||||
class NodesTables extends React.PureComponent<NodesTablesProps> {
|
||||
render() {
|
||||
const {addresses, graph, pagerankResult, depth, adapters} = this.props;
|
||||
return addresses
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const x = pagerankResult.get(a);
|
||||
const y = pagerankResult.get(b);
|
||||
if (x == null) {
|
||||
throw new Error(`No pagerank result for ${NodeAddress.toString(a)}`);
|
||||
return sortBy(
|
||||
addresses,
|
||||
(x) => {
|
||||
const p = pagerankResult.get(x);
|
||||
if (p == null) {
|
||||
throw new Error(`No pagerank result for ${NodeAddress.toString(x)}`);
|
||||
}
|
||||
if (y == null) {
|
||||
throw new Error(`No pagerank result for ${NodeAddress.toString(b)}`);
|
||||
}
|
||||
return y - x;
|
||||
})
|
||||
return -p;
|
||||
},
|
||||
(x) => x
|
||||
)
|
||||
.slice(0, MAX_TABLE_ENTRIES)
|
||||
.map((address) => (
|
||||
<RecursiveTable
|
||||
depth={depth}
|
||||
address={address}
|
||||
node={address}
|
||||
edge={null}
|
||||
graph={graph}
|
||||
pagerankResult={pagerankResult}
|
||||
key={address}
|
||||
|
@ -258,3 +314,42 @@ class RecursiveTables extends React.PureComponent<RecursiveTablesProps> {
|
|||
));
|
||||
}
|
||||
}
|
||||
|
||||
type NeighborsTablesProps = {|
|
||||
+neighbors: $ReadOnlyArray<Neighbor>,
|
||||
+graph: Graph,
|
||||
+pagerankResult: PagerankResult,
|
||||
+depth: number,
|
||||
+adapters: $ReadOnlyArray<PluginAdapter>,
|
||||
|};
|
||||
|
||||
class NeighborsTables extends React.PureComponent<NeighborsTablesProps> {
|
||||
render() {
|
||||
const {neighbors, graph, pagerankResult, depth, adapters} = this.props;
|
||||
return sortBy(
|
||||
neighbors,
|
||||
({node}) => {
|
||||
const p = pagerankResult.get(node);
|
||||
if (p == null) {
|
||||
throw new Error(
|
||||
`No pagerank result for ${NodeAddress.toString(node)}`
|
||||
);
|
||||
}
|
||||
return -p;
|
||||
},
|
||||
({edge}) => edge
|
||||
)
|
||||
.slice(0, MAX_TABLE_ENTRIES)
|
||||
.map(({node, edge}) => (
|
||||
<RecursiveTable
|
||||
depth={depth}
|
||||
node={node}
|
||||
edge={edge}
|
||||
graph={graph}
|
||||
pagerankResult={pagerankResult}
|
||||
key={edge.address}
|
||||
adapters={adapters}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,14 @@ import React from "react";
|
|||
import {mount, shallow} from "enzyme";
|
||||
import enzymeToJSON from "enzyme-to-json";
|
||||
|
||||
import {PagerankTable, nodeDescription} from "./PagerankTable";
|
||||
import {PagerankTable, nodeDescription, neighborVerb} from "./PagerankTable";
|
||||
import {pagerank} from "../../core/attribution/pagerank";
|
||||
import sortBy from "lodash.sortby";
|
||||
|
||||
import {
|
||||
Graph,
|
||||
type NodeAddressT,
|
||||
Direction,
|
||||
NodeAddress,
|
||||
EdgeAddress,
|
||||
} from "../../core/graph";
|
||||
|
@ -31,14 +32,17 @@ function example() {
|
|||
function addEdge(parts, src, dst) {
|
||||
const edge = {address: EdgeAddress.fromParts(parts), src, dst};
|
||||
graph.addEdge(edge);
|
||||
return edge;
|
||||
}
|
||||
|
||||
addEdge(["a"], nodes.fooAlpha, nodes.fooBeta);
|
||||
addEdge(["b"], nodes.fooAlpha, nodes.bar1);
|
||||
addEdge(["c"], nodes.fooAlpha, nodes.xox);
|
||||
addEdge(["d"], nodes.bar1, nodes.bar1);
|
||||
addEdge(["e"], nodes.bar1, nodes.xox);
|
||||
addEdge(["e'"], nodes.bar1, nodes.xox);
|
||||
const edges = {
|
||||
fooA: addEdge(["foo", "a"], nodes.fooAlpha, nodes.fooBeta),
|
||||
fooB: addEdge(["foo", "b"], nodes.fooAlpha, nodes.bar1),
|
||||
fooC: addEdge(["foo", "c"], nodes.fooAlpha, nodes.xox),
|
||||
barD: addEdge(["bar", "d"], nodes.bar1, nodes.bar1),
|
||||
barE: addEdge(["bar", "e"], nodes.bar1, nodes.xox),
|
||||
barF: addEdge(["bar", "f"], nodes.bar1, nodes.xox),
|
||||
};
|
||||
|
||||
const adapters = [
|
||||
{
|
||||
|
@ -48,8 +52,11 @@ function example() {
|
|||
},
|
||||
renderer: () => ({
|
||||
nodeDescription: (x) => `foo: ${NodeAddress.toString(x)}`,
|
||||
edgeVerb: (_unused_e, direction) =>
|
||||
direction === "FORWARD" ? "foos" : "is fooed by",
|
||||
}),
|
||||
nodePrefix: () => NodeAddress.fromParts(["foo"]),
|
||||
edgePrefix: () => EdgeAddress.fromParts(["foo"]),
|
||||
nodeTypes: () => [
|
||||
{name: "alpha", prefix: NodeAddress.fromParts(["foo", "a"])},
|
||||
{name: "beta", prefix: NodeAddress.fromParts(["foo", "b"])},
|
||||
|
@ -62,8 +69,11 @@ function example() {
|
|||
},
|
||||
renderer: () => ({
|
||||
nodeDescription: (x) => `bar: ${NodeAddress.toString(x)}`,
|
||||
edgeVerb: (_unused_e, direction) =>
|
||||
direction === "FORWARD" ? "bars" : "is barred by",
|
||||
}),
|
||||
nodePrefix: () => NodeAddress.fromParts(["bar"]),
|
||||
edgePrefix: () => EdgeAddress.fromParts(["bar"]),
|
||||
nodeTypes: () => [
|
||||
{name: "alpha", prefix: NodeAddress.fromParts(["bar", "a"])},
|
||||
],
|
||||
|
@ -75,8 +85,10 @@ function example() {
|
|||
},
|
||||
renderer: () => ({
|
||||
nodeDescription: (_unused_arg) => `xox node!`,
|
||||
edgeVerb: (_unused_e, _unused_direction) => `xox'd`,
|
||||
}),
|
||||
nodePrefix: () => NodeAddress.fromParts(["xox"]),
|
||||
edgePrefix: () => EdgeAddress.fromParts(["xox"]),
|
||||
nodeTypes: () => [],
|
||||
},
|
||||
{
|
||||
|
@ -88,6 +100,7 @@ function example() {
|
|||
throw new Error("Impossible!");
|
||||
},
|
||||
nodePrefix: () => NodeAddress.fromParts(["unused"]),
|
||||
edgePrefix: () => EdgeAddress.fromParts(["unused"]),
|
||||
nodeTypes: () => [],
|
||||
},
|
||||
];
|
||||
|
@ -97,7 +110,7 @@ function example() {
|
|||
froWeight: 1,
|
||||
}));
|
||||
|
||||
return {adapters, nodes, graph, pagerankResult};
|
||||
return {adapters, nodes, edges, graph, pagerankResult};
|
||||
}
|
||||
|
||||
describe("app/credExplorer/PagerankTable", () => {
|
||||
|
@ -165,7 +178,7 @@ describe("app/credExplorer/PagerankTable", () => {
|
|||
|
||||
describe("full rendering", () => {
|
||||
function exampleRender() {
|
||||
const {nodes, adapters, graph, pagerankResult} = example();
|
||||
const {nodes, edges, adapters, graph, pagerankResult} = example();
|
||||
const element = mount(
|
||||
<PagerankTable
|
||||
pagerankResult={pagerankResult}
|
||||
|
@ -176,20 +189,13 @@ describe("app/credExplorer/PagerankTable", () => {
|
|||
verifyNoAdapterWarning();
|
||||
const select = element.find("select");
|
||||
expect(select).toHaveLength(1);
|
||||
return {nodes, adapters, graph, pagerankResult, element, select};
|
||||
return {nodes, edges, adapters, graph, pagerankResult, element, select};
|
||||
}
|
||||
it("full render doesn't crash or error", () => {
|
||||
example();
|
||||
});
|
||||
|
||||
describe("tables ", () => {
|
||||
it("are sorted by score", () => {
|
||||
const {element, graph, pagerankResult} = exampleRender();
|
||||
const rows = element.find("RecursiveTable");
|
||||
expect(rows).toHaveLength(Array.from(graph.nodes()).length);
|
||||
const scores = rows.map((x) => pagerankResult.get(x.prop("address")));
|
||||
expect(scores).toEqual(sortBy(scores).reverse());
|
||||
});
|
||||
describe("tables", () => {
|
||||
function expectColumnCorrect(
|
||||
element: *,
|
||||
name: string,
|
||||
|
@ -206,54 +212,114 @@ describe("app/credExplorer/PagerankTable", () => {
|
|||
const actual = tables.map((x) =>
|
||||
tdToExpected(x.find("td").at(headerIndex))
|
||||
);
|
||||
const expected = tables.map((x) =>
|
||||
addressToExpected(x.prop("address"))
|
||||
);
|
||||
const expected = tables.map((x) => addressToExpected(x.prop("node")));
|
||||
expect(actual).toEqual(expected);
|
||||
}
|
||||
it("has a node description column", () => {
|
||||
const {element, adapters} = exampleRender();
|
||||
expectColumnCorrect(
|
||||
element,
|
||||
"Node",
|
||||
(td) => td.find("span").text(),
|
||||
(address) => nodeDescription(address, adapters)
|
||||
);
|
||||
verifyNoAdapterWarning();
|
||||
});
|
||||
it("has a log score column", () => {
|
||||
const {element, pagerankResult} = exampleRender();
|
||||
expectColumnCorrect(
|
||||
element,
|
||||
"log(score)",
|
||||
(td) => td.text(),
|
||||
(address) => {
|
||||
const probability = pagerankResult.get(address);
|
||||
if (probability == null) {
|
||||
throw new Error(address);
|
||||
}
|
||||
const modifiedLogScore = Math.log(probability) + 10;
|
||||
return modifiedLogScore.toFixed(2);
|
||||
}
|
||||
);
|
||||
});
|
||||
it("subtables have depth-based styling", () => {
|
||||
const {element} = exampleRender();
|
||||
const getLevel = (level) => {
|
||||
const rt = element.find("RecursiveTable").at(level);
|
||||
const button = rt.find("button").first();
|
||||
return {rt, button};
|
||||
};
|
||||
getLevel(0).button.simulate("click");
|
||||
getLevel(1).button.simulate("click");
|
||||
const f = ({rt, button}) => ({
|
||||
row: rt
|
||||
.find("tr")
|
||||
.first()
|
||||
.prop("style"),
|
||||
button: button.prop("style"),
|
||||
describe("top-level", () => {
|
||||
it("are sorted by score", () => {
|
||||
const {element, graph, pagerankResult} = exampleRender();
|
||||
const rows = element.find("RecursiveTable");
|
||||
expect(rows).toHaveLength(Array.from(graph.nodes()).length);
|
||||
const scores = rows.map((x) => pagerankResult.get(x.prop("node")));
|
||||
expect(scores.every((x) => x != null)).toBe(true);
|
||||
expect(scores).toEqual(sortBy(scores).reverse());
|
||||
});
|
||||
it("has a node description column", () => {
|
||||
const {element, adapters} = exampleRender();
|
||||
expectColumnCorrect(
|
||||
element,
|
||||
"Node",
|
||||
(td) => td.find("span").text(),
|
||||
(address) => nodeDescription(address, adapters)
|
||||
);
|
||||
verifyNoAdapterWarning();
|
||||
});
|
||||
it("has a log score column", () => {
|
||||
const {element, pagerankResult} = exampleRender();
|
||||
expectColumnCorrect(
|
||||
element,
|
||||
"log(score)",
|
||||
(td) => td.text(),
|
||||
(address) => {
|
||||
const probability = pagerankResult.get(address);
|
||||
if (probability == null) {
|
||||
throw new Error(address);
|
||||
}
|
||||
const modifiedLogScore = Math.log(probability) + 10;
|
||||
return modifiedLogScore.toFixed(2);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("sub-tables", () => {
|
||||
it("have depth-based styling", () => {
|
||||
const {element} = exampleRender();
|
||||
const getLevel = (level) => {
|
||||
const rt = element.find("RecursiveTable").at(level);
|
||||
const button = rt.find("button").first();
|
||||
return {rt, button};
|
||||
};
|
||||
getLevel(0).button.simulate("click");
|
||||
getLevel(1).button.simulate("click");
|
||||
const f = ({rt, button}) => ({
|
||||
row: rt
|
||||
.find("tr")
|
||||
.first()
|
||||
.prop("style"),
|
||||
button: button.prop("style"),
|
||||
});
|
||||
expect([0, 1, 2].map((x) => f(getLevel(x)))).toMatchSnapshot();
|
||||
});
|
||||
it("display extra information about edges", () => {
|
||||
const {element, nodes, graph, adapters} = exampleRender();
|
||||
const getLevel = (level) => {
|
||||
const rt = element.find("RecursiveTable").at(level);
|
||||
const button = rt.find("button").first();
|
||||
return {rt, button};
|
||||
};
|
||||
getLevel(0).button.simulate("click");
|
||||
const nt = element.find("NeighborsTables");
|
||||
expect(nt).toHaveLength(1);
|
||||
const expectedNeighbors = Array.from(
|
||||
graph.neighbors(nodes.bar1, {
|
||||
direction: Direction.ANY,
|
||||
nodePrefix: NodeAddress.empty,
|
||||
edgePrefix: EdgeAddress.empty,
|
||||
})
|
||||
);
|
||||
expect(nt.prop("neighbors")).toEqual(expectedNeighbors);
|
||||
const subTables = nt.find("RecursiveTable");
|
||||
expect(subTables).toHaveLength(expectedNeighbors.length);
|
||||
const actualEdgeVerbs = subTables.map((x) =>
|
||||
x
|
||||
.find("span")
|
||||
.children()
|
||||
.find("span")
|
||||
.text()
|
||||
);
|
||||
const expectedEdgeVerbs = subTables.map((x) => {
|
||||
const edge = x.prop("edge");
|
||||
const node = x.prop("node");
|
||||
return neighborVerb({edge, node}, adapters);
|
||||
});
|
||||
|
||||
expect(actualEdgeVerbs).toEqual(expectedEdgeVerbs);
|
||||
const actualFullDescriptions = subTables.map((x) =>
|
||||
x
|
||||
.find("span")
|
||||
.first()
|
||||
.text()
|
||||
);
|
||||
const expectedFullDescriptions = subTables.map((x) => {
|
||||
const edge = x.prop("edge");
|
||||
const node = x.prop("node");
|
||||
const nd = nodeDescription(node, adapters);
|
||||
const ev = neighborVerb({node, edge}, adapters);
|
||||
return `${ev} ${nd}`;
|
||||
});
|
||||
expect(actualFullDescriptions).toEqual(expectedFullDescriptions);
|
||||
expect(actualFullDescriptions).toMatchSnapshot();
|
||||
});
|
||||
expect([0, 1, 2].map((x) => f(getLevel(x)))).toMatchSnapshot();
|
||||
});
|
||||
it("button toggles between +/- and adds sub-RecursiveTable", () => {
|
||||
const {element} = exampleRender();
|
||||
|
@ -261,15 +327,15 @@ describe("app/credExplorer/PagerankTable", () => {
|
|||
const button = rt().find("button");
|
||||
expect(button).toEqual(expect.anything());
|
||||
expect(button.text()).toEqual("+");
|
||||
expect(rt().find("RecursiveTables")).toHaveLength(0);
|
||||
expect(rt().find("NeighborsTables")).toHaveLength(0);
|
||||
|
||||
button.simulate("click");
|
||||
expect(button.text()).toEqual("\u2212");
|
||||
expect(rt().find("RecursiveTables")).toHaveLength(1);
|
||||
expect(rt().find("NeighborsTables")).toHaveLength(1);
|
||||
|
||||
button.simulate("click");
|
||||
expect(button.text()).toEqual("+");
|
||||
expect(rt().find("RecursiveTables")).toHaveLength(0);
|
||||
expect(rt().find("NeighborsTables")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -305,7 +371,7 @@ describe("app/credExplorer/PagerankTable", () => {
|
|||
selectFilterByName(select, "\u2003beta");
|
||||
const rt = element.find("RecursiveTable");
|
||||
expect(rt).toHaveLength(1);
|
||||
expect(rt.prop("address")).toEqual(example().nodes.fooBeta);
|
||||
expect(rt.prop("node")).toEqual(example().nodes.fooBeta);
|
||||
});
|
||||
it("filter doesn't apply to sub-tables", () => {
|
||||
const {select, element} = exampleRender();
|
||||
|
@ -319,7 +385,7 @@ describe("app/credExplorer/PagerankTable", () => {
|
|||
const rts = element.find("RecursiveTable");
|
||||
expect(rts).toHaveLength(2);
|
||||
const subRt = rts.last();
|
||||
expect(subRt.prop("address")).toEqual(example().nodes.fooAlpha);
|
||||
expect(subRt.prop("node")).toEqual(example().nodes.fooAlpha);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -53,7 +53,16 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`app/credExplorer/PagerankTable full rendering tables subtables have depth-based styling 1`] = `
|
||||
exports[`app/credExplorer/PagerankTable full rendering tables sub-tables display extra information about edges 1`] = `
|
||||
Array [
|
||||
"bars and is barred by bar: NodeAddress[\\"bar\\",\\"a\\",\\"1\\"]",
|
||||
"bars xox node!",
|
||||
"bars xox node!",
|
||||
"is fooed by foo: NodeAddress[\\"foo\\",\\"a\\",\\"1\\"]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`app/credExplorer/PagerankTable full rendering tables sub-tables have depth-based styling 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"button": Object {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// @flow
|
||||
|
||||
import type {Graph, NodeAddressT} from "../core/graph";
|
||||
import type {Graph, NodeAddressT, EdgeAddressT} from "../core/graph";
|
||||
|
||||
export interface Renderer {
|
||||
nodeDescription(NodeAddressT): string;
|
||||
edgeVerb(EdgeAddressT, "FORWARD" | "BACKWARD"): string;
|
||||
}
|
||||
|
||||
export interface PluginAdapter {
|
||||
|
@ -11,6 +12,7 @@ export interface PluginAdapter {
|
|||
graph(): Graph;
|
||||
renderer(): Renderer;
|
||||
nodePrefix(): NodeAddressT;
|
||||
edgePrefix(): EdgeAddressT;
|
||||
nodeTypes(): Array<{|
|
||||
+name: string,
|
||||
+prefix: NodeAddressT,
|
||||
|
|
|
@ -5,7 +5,8 @@ import type {
|
|||
} from "../../app/pluginAdapter";
|
||||
import {Graph} from "../../core/graph";
|
||||
import * as N from "./nodes";
|
||||
import {description} from "./render";
|
||||
import * as E from "./edges";
|
||||
import {description, edgeVerb} from "./render";
|
||||
|
||||
export async function createPluginAdapter(
|
||||
repoOwner: string,
|
||||
|
@ -38,6 +39,9 @@ class PluginAdapter implements IPluginAdapter {
|
|||
nodePrefix() {
|
||||
return N._Prefix.base;
|
||||
}
|
||||
edgePrefix() {
|
||||
return E._Prefix.base;
|
||||
}
|
||||
nodeTypes() {
|
||||
return [
|
||||
{name: "Blob", prefix: N._Prefix.blob},
|
||||
|
@ -55,4 +59,7 @@ class Renderer implements IRenderer {
|
|||
const address = N.fromRaw((node: any));
|
||||
return description(address);
|
||||
}
|
||||
edgeVerb(edgeAddress, direction) {
|
||||
return edgeVerb(E.fromRaw((edgeAddress: any)), direction);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import * as N from "./nodes";
|
||||
import * as E from "./edges";
|
||||
|
||||
export function description(address: N.StructuredAddress) {
|
||||
switch (address.type) {
|
||||
|
@ -18,3 +19,24 @@ export function description(address: N.StructuredAddress) {
|
|||
throw new Error(`unknown type: ${(address.type: empty)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function edgeVerb(
|
||||
address: E.StructuredAddress,
|
||||
direction: "FORWARD" | "BACKWARD"
|
||||
) {
|
||||
const forward = direction === "FORWARD";
|
||||
switch (address.type) {
|
||||
case "HAS_TREE":
|
||||
return forward ? "has tree" : "owned by";
|
||||
case "HAS_PARENT":
|
||||
return forward ? "has parent" : "is parent of";
|
||||
case "INCLUDES":
|
||||
return forward ? "includes" : "is included by";
|
||||
case "BECOMES":
|
||||
return forward ? "evolves to" : "evolves from";
|
||||
case "HAS_CONTENTS":
|
||||
return forward ? "has contents" : "is contents of";
|
||||
default:
|
||||
throw new Error(`unknown type: ${(address.type: empty)}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@ import type {
|
|||
import {type Graph, NodeAddress} from "../../core/graph";
|
||||
import {createGraph} from "./createGraph";
|
||||
import * as N from "./nodes";
|
||||
import * as E from "./edges";
|
||||
import {RelationalView} from "./relationalView";
|
||||
import {description} from "./render";
|
||||
import {description, edgeVerb} from "./render";
|
||||
|
||||
export async function createPluginAdapter(
|
||||
repoOwner: string,
|
||||
|
@ -43,6 +44,9 @@ class PluginAdapter implements IPluginAdapter {
|
|||
nodePrefix() {
|
||||
return N._Prefix.base;
|
||||
}
|
||||
edgePrefix() {
|
||||
return E._Prefix.base;
|
||||
}
|
||||
nodeTypes() {
|
||||
return [
|
||||
{name: "Repository", prefix: N._Prefix.repo},
|
||||
|
@ -70,4 +74,7 @@ class Renderer implements IRenderer {
|
|||
}
|
||||
return description(entity);
|
||||
}
|
||||
edgeVerb(edgeAddress, direction) {
|
||||
return edgeVerb(E.fromRaw((edgeAddress: any)), direction);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import * as R from "./relationalView";
|
||||
import * as E from "./edges";
|
||||
|
||||
export function description(e: R.Entity) {
|
||||
const withAuthors = (x: R.AuthoredEntity) => {
|
||||
|
@ -24,3 +25,22 @@ export function description(e: R.Entity) {
|
|||
};
|
||||
return R.match(handlers, e);
|
||||
}
|
||||
|
||||
export function edgeVerb(
|
||||
e: E.StructuredAddress,
|
||||
direction: "FORWARD" | "BACKWARD"
|
||||
) {
|
||||
const forward = direction === "FORWARD";
|
||||
switch (e.type) {
|
||||
case "AUTHORS":
|
||||
return forward ? "authors" : "is authored by";
|
||||
case "MERGED_AS":
|
||||
return forward ? "merges" : "is merged by";
|
||||
case "HAS_PARENT":
|
||||
return forward ? "has parent" : "has child";
|
||||
case "REFERENCES":
|
||||
return forward ? "references" : "is referenced by";
|
||||
default:
|
||||
throw new Error(`Unexpected type ${(e.type: empty)}`);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue