Frontend gets plugins from disk, not TimelineCred (#1625)

This commit modifies the frontend so that it now pulls plugin
declarations from disk, rather than from the TimelineCred. This will
allow us to decouple the TimelineCred from the PluginDeclarations, which
is another step towards #1557.

As proof that the frontend no longer gets plugins from the TimelineCred,
I removed the public `plugins()` method on TimelineCred.

Test plan: The frontend is somewhat sketchily tested. `yarn test`
passing is good, manual inspection of the frontend is also necessary;
I've done this.
This commit is contained in:
Dandelion Mané 2020-02-04 10:44:42 -08:00 committed by GitHub
parent aeaa945a27
commit 1073374dc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 76 additions and 46 deletions

View File

@ -76,10 +76,6 @@ export class TimelineCred {
return this._params;
}
plugins(): $ReadOnlyArray<PluginDeclaration> {
return this._plugins;
}
/**
* Creates a new TimelineCred based on the new Parameters.
* Holds the graph and config constant.
@ -151,7 +147,7 @@ export class TimelineCred {
* with a type specified as a user type by one of the plugin declarations.
*/
userNodes(): $ReadOnlyArray<CredNode> {
const userTypes = [].concat(...this.plugins().map((p) => p.userTypes));
const userTypes = [].concat(...this._plugins.map((p) => p.userTypes));
return this.credSortedNodes(userTypes.map((x) => x.prefix));
}

View File

@ -5,6 +5,10 @@ import type {Assets} from "../webutil/assets";
import {TimelineExplorer} from "./TimelineExplorer";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {encodeProjectId, type ProjectId} from "../core/project";
import {
type PluginDeclarations,
fromJSON as pluginsFromJSON,
} from "../analysis/pluginDeclaration";
export type Props = {|
+assets: Assets,
@ -19,6 +23,7 @@ export type Loading = {|+type: "LOADING"|};
export type LoadSuccess = {|
+type: "SUCCESS",
+timelineCred: TimelineCred,
+pluginDeclarations: PluginDeclarations,
|};
export type LoadError = {|+type: "ERROR", +error: any|};
@ -62,11 +67,12 @@ export class TimelineApp extends React.Component<Props, State> {
);
}
case "SUCCESS": {
const {timelineCred} = loadResult;
const {timelineCred, pluginDeclarations} = loadResult;
return (
<TimelineExplorer
initialTimelineCred={timelineCred}
projectId={this.props.projectId}
pluginDeclarations={pluginDeclarations}
/>
);
}
@ -90,9 +96,24 @@ export async function defaultLoader(
return TimelineCred.fromJSON(await response.json());
}
async function fetchPluginDeclarations(): Promise<PluginDeclarations> {
const encodedId = encodeProjectId(projectId);
const url = assets.resolve(
`api/v1/data/projects/${encodedId}/pluginDeclarations.json`
);
const response = await fetch(url);
if (!response.ok) {
return Promise.reject(response);
}
return pluginsFromJSON(await response.json());
}
try {
const timelineCred = await fetchCred();
return {type: "SUCCESS", timelineCred};
const [timelineCred, pluginDeclarations] = await Promise.all([
fetchCred(),
fetchPluginDeclarations(),
]);
return {type: "SUCCESS", timelineCred, pluginDeclarations};
} catch (e) {
console.error(e);
return {type: "ERROR", error: e};

View File

@ -11,13 +11,17 @@ import {TimelineCredView} from "./TimelineCredView";
import Link from "../webutil/Link";
import {WeightConfig} from "./weights/WeightConfig";
import {WeightsFileManager} from "./weights/WeightsFileManager";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {
type PluginDeclarations,
type PluginDeclaration,
} from "../analysis/pluginDeclaration";
import * as NullUtil from "../util/null";
import {format} from "d3-format";
export type Props = {
projectId: string,
initialTimelineCred: TimelineCred,
pluginDeclarations: PluginDeclarations,
};
export type State = {
@ -89,7 +93,7 @@ export class TimelineExplorer extends React.Component<Props, State> {
);
const weightConfig = (
<WeightConfig
declarations={this.state.timelineCred.plugins()}
declarations={this.props.pluginDeclarations}
nodeWeights={this.state.weights.nodeWeights}
edgeWeights={this.state.weights.edgeWeights}
onNodeWeightChange={(prefix, weight) => {
@ -201,7 +205,7 @@ export class TimelineExplorer extends React.Component<Props, State> {
<option key={"All users"} value={""}>
All users
</option>
{this.state.timelineCred.plugins().map(optionGroup)}
{this.props.pluginDeclarations.map(optionGroup)}
</select>
</label>
);

View File

@ -93,7 +93,7 @@ export function createApp(
);
let pagerankTable;
if (appState.type === "PAGERANK_EVALUATED") {
const declarations = appState.timelineCred.plugins();
const declarations = appState.pluginDeclarations;
const weightConfig = (
<WeightConfig
declarations={declarations}

View File

@ -72,6 +72,7 @@ describe("explorer/legacy/App", () => {
defaultParams(),
[]
),
pluginDeclarations: [],
});
},
pagerankEvaluated: (loadingState) => {
@ -86,6 +87,7 @@ describe("explorer/legacy/App", () => {
defaultParams(),
[]
),
pluginDeclarations: [],
pagerankNodeDecomposition: new Map(),
});
},

View File

@ -5,7 +5,7 @@ import deepEqual from "lodash.isequal";
import {Graph, type NodeAddressT} from "../../core/graph";
import type {Assets} from "../../webutil/assets";
import {type EdgeEvaluator} from "../../analysis/pagerank";
import {defaultLoader} from "../TimelineApp";
import {defaultLoader, type LoadSuccess} from "../TimelineApp";
import {
type PagerankNodeDecomposition,
type PagerankOptions,
@ -15,7 +15,10 @@ import {TimelineCred} from "../../analysis/timeline/timelineCred";
import type {Weights} from "../../core/weights";
import {weightsToEdgeEvaluator} from "../../analysis/weightsToEdgeEvaluator";
import {combineTypes} from "../../analysis/pluginDeclaration";
import {
combineTypes,
type PluginDeclarations,
} from "../../analysis/pluginDeclaration";
/*
This models the UI states of the credExplorer/App as a state machine.
@ -40,11 +43,13 @@ export type ReadyToRunPagerank = {|
+type: "READY_TO_RUN_PAGERANK",
+projectId: string,
+timelineCred: TimelineCred,
+pluginDeclarations: PluginDeclarations,
+loading: LoadingState,
|};
export type PagerankEvaluated = {|
+type: "PAGERANK_EVALUATED",
+timelineCred: TimelineCred,
+pluginDeclarations: PluginDeclarations,
+projectId: string,
+pagerankNodeDecomposition: PagerankNodeDecomposition,
+loading: LoadingState,
@ -58,12 +63,7 @@ export function createStateTransitionMachine(
getState: () => AppState,
setState: (AppState) => void
): StateTransitionMachine {
return new StateTransitionMachine(
getState,
setState,
doLoadTimelineCred,
pagerank
);
return new StateTransitionMachine(getState, setState, doLoad, pagerank);
}
// Exported for testing purposes.
@ -83,10 +83,7 @@ export interface StateTransitionMachineInterface {
export class StateTransitionMachine implements StateTransitionMachineInterface {
getState: () => AppState;
setState: (AppState) => void;
doLoadTimelineCred: (
assets: Assets,
projectId: string
) => Promise<TimelineCred>;
doLoad: (assets: Assets, projectId: string) => Promise<LoadSuccess>;
pagerank: (
Graph,
EdgeEvaluator,
@ -96,10 +93,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
constructor(
getState: () => AppState,
setState: (AppState) => void,
doLoadTimelineCred: (
assets: Assets,
projectId: string
) => Promise<TimelineCred>,
doLoad: (assets: Assets, projectId: string) => Promise<LoadSuccess>,
pagerank: (
Graph,
EdgeEvaluator,
@ -108,7 +102,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
) {
this.getState = getState;
this.setState = setState;
this.doLoadTimelineCred = doLoadTimelineCred;
this.doLoad = doLoad;
this.pagerank = pagerank;
}
@ -124,10 +118,14 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
let newState: ?AppState;
let success = true;
try {
const timelineCred = await this.doLoadTimelineCred(assets, projectId);
const {pluginDeclarations, timelineCred} = await this.doLoad(
assets,
projectId
);
newState = {
type: "READY_TO_RUN_PAGERANK",
timelineCred,
pluginDeclarations,
projectId,
loading: "NOT_LOADING",
};
@ -159,7 +157,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
this.setState(loadingState);
const graph = state.timelineCred.weightedGraph().graph;
let newState: ?AppState;
const types = combineTypes(state.timelineCred.plugins());
const types = combineTypes(state.pluginDeclarations);
try {
const pagerankNodeDecomposition = await this.pagerank(
graph,
@ -173,6 +171,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
type: "PAGERANK_EVALUATED",
pagerankNodeDecomposition,
timelineCred: state.timelineCred,
pluginDeclarations: state.pluginDeclarations,
projectId: state.projectId,
loading: "NOT_LOADING",
};
@ -213,13 +212,13 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
}
}
export async function doLoadTimelineCred(
export async function doLoad(
assets: Assets,
projectId: string
): Promise<TimelineCred> {
): Promise<LoadSuccess> {
const loadResult = await defaultLoader(assets, projectId);
if (loadResult.type !== "SUCCESS") {
throw new Error(loadResult);
}
return loadResult.timelineCred;
return loadResult;
}

View File

@ -13,6 +13,7 @@ import type {
} from "../../analysis/pagerank";
import {TimelineCred} from "../../analysis/timeline/timelineCred";
import {defaultParams} from "../../analysis/timeline/params";
import {type LoadSuccess} from "../TimelineApp";
describe("explorer/legacy/state", () => {
function example(startingState: AppState) {
@ -21,9 +22,9 @@ describe("explorer/legacy/state", () => {
const setState = (appState) => {
stateContainer.appState = appState;
};
const loadTimelineCredMock: JestMockFn<
const loadMock: JestMockFn<
[Assets, string],
Promise<TimelineCred>
Promise<LoadSuccess>
> = jest.fn();
const pagerankMock: JestMockFn<
@ -33,10 +34,10 @@ describe("explorer/legacy/state", () => {
const stm = new StateTransitionMachine(
getState,
setState,
loadTimelineCredMock,
loadMock,
pagerankMock
);
return {getState, stm, loadTimelineCredMock, pagerankMock};
return {getState, stm, loadMock, pagerankMock};
}
function readyToLoadGraph(): AppState {
return {
@ -57,6 +58,7 @@ describe("explorer/legacy/state", () => {
defaultParams(),
[]
),
pluginDeclarations: [],
};
}
function pagerankEvaluated(): AppState {
@ -70,6 +72,7 @@ describe("explorer/legacy/state", () => {
defaultParams(),
[]
),
pluginDeclarations: [],
pagerankNodeDecomposition: pagerankNodeDecomposition(),
loading: "NOT_LOADING",
};
@ -92,12 +95,12 @@ describe("explorer/legacy/state", () => {
}
});
it("passes along the projectId", () => {
const {stm, loadTimelineCredMock} = example(readyToLoadGraph());
expect(loadTimelineCredMock).toHaveBeenCalledTimes(0);
const {stm, loadMock} = example(readyToLoadGraph());
expect(loadMock).toHaveBeenCalledTimes(0);
const assets = new Assets("/my/gateway/");
stm.loadTimelineCred(assets);
expect(loadTimelineCredMock).toHaveBeenCalledTimes(1);
expect(loadTimelineCredMock).toHaveBeenCalledWith(assets, "foo/bar");
expect(loadMock).toHaveBeenCalledTimes(1);
expect(loadMock).toHaveBeenCalledWith(assets, "foo/bar");
});
it("immediately sets loading status", () => {
const {getState, stm} = example(readyToLoadGraph());
@ -107,7 +110,7 @@ describe("explorer/legacy/state", () => {
expect(getState().type).toBe("READY_TO_LOAD_GRAPH");
});
it("transitions to READY_TO_RUN_PAGERANK on success", async () => {
const {getState, stm, loadTimelineCredMock} = example(readyToLoadGraph());
const {getState, stm, loadMock} = example(readyToLoadGraph());
const timelineCred = new TimelineCred(
WeightedGraph.empty(),
@ -116,7 +119,12 @@ describe("explorer/legacy/state", () => {
defaultParams(),
[]
);
loadTimelineCredMock.mockReturnValue(Promise.resolve(timelineCred));
const loadResult = {
type: "SUCCESS",
timelineCred,
pluginDeclarations: [],
};
loadMock.mockReturnValue(Promise.resolve(loadResult));
const succeeded = await stm.loadTimelineCred(new Assets("/my/gateway/"));
expect(succeeded).toBe(true);
const state = getState();
@ -128,11 +136,11 @@ describe("explorer/legacy/state", () => {
expect(state.timelineCred).toBe(timelineCred);
});
it("sets loading state FAILED on reject", async () => {
const {getState, stm, loadTimelineCredMock} = example(readyToLoadGraph());
const {getState, stm, loadMock} = example(readyToLoadGraph());
const error = new Error("Oh no!");
// $ExpectFlowError
console.error = jest.fn();
loadTimelineCredMock.mockReturnValue(Promise.reject(error));
loadMock.mockReturnValue(Promise.reject(error));
const succeeded = await stm.loadTimelineCred(new Assets("/my/gateway/"));
expect(succeeded).toBe(false);
const state = getState();