mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-28 12:10:30 +00:00
Remove the repository select from explorer/ (#988)
Historically, a single cred explorer instance could load many different repositories. This turned out to be an anti-feature: we'd rather have a particular url hardlink to exploring the cred for a particular project. This commit removes the repository select from the explorer, and instead mandates that the explorer always has the RepoId passed down from above. Besides providing a better UX, this also greatly simplifies the logic for the explorer, since we no longer have an "initializing state" that doesn't have any RepoId. This builds on the work in #984, and swaps out the old "prototype" page (which has been rendered non-functional by this change) for the new "prototypes" page. Note that it stays at the same route, so links to sourcecred.io/prototype will continue to function. Test plan: Ran `yarn test --full`, and verified that `yarn start` produces a working site.
This commit is contained in:
parent
738853cd02
commit
29065f44d6
@ -7,23 +7,24 @@ import type {LocalStore} from "../webutil/localStore";
|
||||
import CheckedLocalStore from "../webutil/checkedLocalStore";
|
||||
import BrowserLocalStore from "../webutil/browserLocalStore";
|
||||
import Link from "../webutil/Link";
|
||||
import type {RepoId} from "../core/repoId";
|
||||
|
||||
import {PagerankTable} from "./pagerankTable/Table";
|
||||
import type {WeightedTypes} from "../analysis/weights";
|
||||
import {defaultWeightsForAdapterSet} from "./weights/weights";
|
||||
import RepositorySelect from "./RepositorySelect";
|
||||
import {Prefix as GithubPrefix} from "../plugins/github/nodes";
|
||||
import {
|
||||
createStateTransitionMachine,
|
||||
type AppState,
|
||||
type StateTransitionMachineInterface,
|
||||
uninitializedState,
|
||||
initialState,
|
||||
} from "./state";
|
||||
import {StaticAdapterSet} from "./adapters/adapterSet";
|
||||
|
||||
export class AppPage extends React.Component<{|
|
||||
+assets: Assets,
|
||||
+adapters: StaticAdapterSet,
|
||||
+repoId: RepoId,
|
||||
|}> {
|
||||
static _LOCAL_STORE = new CheckedLocalStore(
|
||||
new BrowserLocalStore({
|
||||
@ -36,6 +37,7 @@ export class AppPage extends React.Component<{|
|
||||
const App = createApp(createStateTransitionMachine);
|
||||
return (
|
||||
<App
|
||||
repoId={this.props.repoId}
|
||||
assets={this.props.assets}
|
||||
adapters={this.props.adapters}
|
||||
localStore={AppPage._LOCAL_STORE}
|
||||
@ -48,6 +50,7 @@ type Props = {|
|
||||
+assets: Assets,
|
||||
+localStore: LocalStore,
|
||||
+adapters: StaticAdapterSet,
|
||||
+repoId: RepoId,
|
||||
|};
|
||||
type State = {|
|
||||
appState: AppState,
|
||||
@ -66,7 +69,7 @@ export function createApp(
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
appState: uninitializedState(),
|
||||
appState: initialState(this.props.repoId),
|
||||
weightedTypes: defaultWeightsForAdapterSet(props.adapters),
|
||||
};
|
||||
this.stateTransitionMachine = createSTM(
|
||||
@ -76,7 +79,6 @@ export function createApp(
|
||||
}
|
||||
|
||||
render() {
|
||||
const {localStore} = this.props;
|
||||
const {appState} = this.state;
|
||||
let pagerankTable;
|
||||
if (appState.type === "PAGERANK_EVALUATED") {
|
||||
@ -109,15 +111,6 @@ export function createApp(
|
||||
feedback
|
||||
</Link>
|
||||
</p>
|
||||
<div style={{marginBottom: 10}}>
|
||||
<RepositorySelect
|
||||
assets={this.props.assets}
|
||||
localStore={localStore}
|
||||
onChange={(repoId) =>
|
||||
this.stateTransitionMachine.setRepoId(repoId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={
|
||||
appState.type === "UNINITIALIZED" ||
|
||||
@ -154,9 +147,6 @@ export class LoadingIndicator extends React.PureComponent<{|
|
||||
|
||||
export function loadingText(state: AppState) {
|
||||
switch (state.type) {
|
||||
case "UNINITIALIZED": {
|
||||
return "Initializing...";
|
||||
}
|
||||
case "READY_TO_LOAD_GRAPH": {
|
||||
return {
|
||||
LOADING: "Loading graph...",
|
||||
|
@ -11,10 +11,8 @@ import {DynamicAdapterSet, StaticAdapterSet} from "./adapters/adapterSet";
|
||||
import {FactorioStaticAdapter} from "../plugins/demo/appAdapter";
|
||||
import {defaultWeightsForAdapter} from "./weights/weights";
|
||||
|
||||
import RepositorySelect from "./RepositorySelect";
|
||||
import {PagerankTable} from "./pagerankTable/Table";
|
||||
import {createApp, LoadingIndicator} from "./App";
|
||||
import {uninitializedState} from "./state";
|
||||
import {Prefix as GithubPrefix} from "../plugins/github/nodes";
|
||||
|
||||
require("../webutil/testUtil").configureEnzyme();
|
||||
@ -22,7 +20,6 @@ require("../webutil/testUtil").configureEnzyme();
|
||||
describe("explorer/App", () => {
|
||||
function example() {
|
||||
let setState, getState;
|
||||
const setRepoId = jest.fn();
|
||||
const loadGraph = jest.fn();
|
||||
const runPagerank = jest.fn();
|
||||
const loadGraphAndRunPagerank = jest.fn();
|
||||
@ -31,7 +28,6 @@ describe("explorer/App", () => {
|
||||
setState = _setState;
|
||||
getState = _getState;
|
||||
return {
|
||||
setRepoId,
|
||||
loadGraph,
|
||||
runPagerank,
|
||||
loadGraphAndRunPagerank,
|
||||
@ -43,6 +39,7 @@ describe("explorer/App", () => {
|
||||
assets={new Assets("/foo/")}
|
||||
adapters={new StaticAdapterSet([])}
|
||||
localStore={localStore}
|
||||
repoId={makeRepoId("foo", "bar")}
|
||||
/>
|
||||
);
|
||||
if (setState == null || getState == null) {
|
||||
@ -52,7 +49,6 @@ describe("explorer/App", () => {
|
||||
el,
|
||||
setState,
|
||||
getState,
|
||||
setRepoId,
|
||||
loadGraph,
|
||||
runPagerank,
|
||||
loadGraphAndRunPagerank,
|
||||
@ -62,7 +58,6 @@ describe("explorer/App", () => {
|
||||
|
||||
const emptyAdapters = new DynamicAdapterSet(new StaticAdapterSet([]), []);
|
||||
const exampleStates = {
|
||||
uninitialized: uninitializedState,
|
||||
readyToLoadGraph: (loadingState) => {
|
||||
return () => ({
|
||||
type: "READY_TO_LOAD_GRAPH",
|
||||
@ -95,8 +90,7 @@ describe("explorer/App", () => {
|
||||
});
|
||||
it("setState is wired properly", () => {
|
||||
const {setState, el} = example();
|
||||
expect(uninitializedState()).not.toBe(uninitializedState()); // sanity check
|
||||
const newState = uninitializedState();
|
||||
const newState = exampleStates.readyToLoadGraph("LOADING")();
|
||||
setState(newState);
|
||||
expect(el.state().appState).toBe(newState);
|
||||
});
|
||||
@ -118,18 +112,6 @@ describe("explorer/App", () => {
|
||||
});
|
||||
|
||||
describe("when in state:", () => {
|
||||
function testRepositorySelect(stateFn) {
|
||||
it("creates a working RepositorySelect", () => {
|
||||
const {el, setRepoId, setState, localStore} = example();
|
||||
setState(stateFn());
|
||||
const rs = el.find(RepositorySelect);
|
||||
const newRepoId = makeRepoId("zoo", "zod");
|
||||
rs.props().onChange(newRepoId);
|
||||
expect(setRepoId).toHaveBeenCalledWith(newRepoId);
|
||||
expect(rs.props().localStore).toBe(localStore);
|
||||
});
|
||||
}
|
||||
|
||||
function testAnalyzeCredButton(stateFn, {disabled}) {
|
||||
const adjective = disabled ? "disabled" : "working";
|
||||
it(`has a ${adjective} analyze cred button`, () => {
|
||||
@ -203,17 +185,12 @@ describe("explorer/App", () => {
|
||||
{analyzeCredDisabled, hasPagerankTable}
|
||||
) {
|
||||
describe(suiteName, () => {
|
||||
testRepositorySelect(stateFn);
|
||||
testAnalyzeCredButton(stateFn, {disabled: analyzeCredDisabled});
|
||||
testPagerankTable(stateFn, hasPagerankTable);
|
||||
testLoadingIndicator(stateFn);
|
||||
});
|
||||
}
|
||||
|
||||
stateTestSuite("UNINITIALIZED", exampleStates.uninitialized, {
|
||||
analyzeCredDisabled: true,
|
||||
hasPagerankTable: false,
|
||||
});
|
||||
describe("READY_TO_LOAD_GRAPH", () => {
|
||||
for (const loadingState of ["LOADING", "NOT_LOADING", "FAILED"]) {
|
||||
stateTestSuite(
|
||||
@ -262,11 +239,6 @@ describe("explorer/App", () => {
|
||||
expect(el.text()).toEqual(expectedText);
|
||||
});
|
||||
}
|
||||
testStatusText(
|
||||
"initializing",
|
||||
exampleStates.uninitialized,
|
||||
"Initializing..."
|
||||
);
|
||||
testStatusText(
|
||||
"ready to load graph",
|
||||
exampleStates.readyToLoadGraph("NOT_LOADING"),
|
||||
|
@ -1,190 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, {type Node} from "react";
|
||||
import sortBy from "lodash.sortby";
|
||||
import deepEqual from "lodash.isequal";
|
||||
|
||||
import * as NullUtil from "../util/null";
|
||||
import type {LocalStore} from "../webutil/localStore";
|
||||
import type {Assets} from "../webutil/assets";
|
||||
|
||||
import {fromJSON, REPO_ID_REGISTRY_API} from "./repoIdRegistry";
|
||||
import {type RepoId, stringToRepoId, repoIdToString} from "../core/repoId";
|
||||
export const REPO_ID_KEY = "selectedRepository";
|
||||
|
||||
export type Status =
|
||||
| {|+type: "LOADING"|}
|
||||
| {|
|
||||
+type: "VALID",
|
||||
+availableRepoIds: $ReadOnlyArray<RepoId>,
|
||||
+selectedRepoId: RepoId,
|
||||
|}
|
||||
| {|+type: "NO_REPOS"|}
|
||||
| {|+type: "FAILURE"|};
|
||||
|
||||
type Props = {|
|
||||
+assets: Assets,
|
||||
+onChange: (x: RepoId) => void,
|
||||
+localStore: LocalStore,
|
||||
|};
|
||||
type State = {|status: Status|};
|
||||
export default class RepositorySelect extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: {type: "LOADING"},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {assets, localStore} = this.props;
|
||||
loadStatus(assets, localStore).then((status) => {
|
||||
this.setState({status});
|
||||
if (status.type === "VALID") {
|
||||
this.props.onChange(status.selectedRepoId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChange(selectedRepoId: RepoId) {
|
||||
const status = this.state.status;
|
||||
if (status.type === "VALID") {
|
||||
const newStatus = {...status, selectedRepoId};
|
||||
this.setState({status: newStatus});
|
||||
}
|
||||
this.props.onChange(selectedRepoId);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LocalStoreRepositorySelect
|
||||
onChange={(selectedRepoId) => this.onChange(selectedRepoId)}
|
||||
status={this.state.status}
|
||||
localStore={this.props.localStore}
|
||||
>
|
||||
{({status, onChange}) => (
|
||||
<PureRepositorySelect onChange={onChange} status={status} />
|
||||
)}
|
||||
</LocalStoreRepositorySelect>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadStatus(
|
||||
assets: Assets,
|
||||
localStore: LocalStore
|
||||
): Promise<Status> {
|
||||
try {
|
||||
const response = await fetch(assets.resolve(REPO_ID_REGISTRY_API));
|
||||
if (response.status === 404) {
|
||||
return {type: "NO_REPOS"};
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.error(response);
|
||||
return {type: "FAILURE"};
|
||||
}
|
||||
const json = await response.json();
|
||||
const availableRepoIds = fromJSON(json);
|
||||
if (availableRepoIds.length === 0) {
|
||||
return {type: "NO_REPOS"};
|
||||
}
|
||||
const localStoreRepoId = localStore.get(REPO_ID_KEY, null);
|
||||
const selectedRepoId = NullUtil.orElse(
|
||||
availableRepoIds.find((x) => deepEqual(x, localStoreRepoId)),
|
||||
availableRepoIds[availableRepoIds.length - 1]
|
||||
);
|
||||
const sortedRepoIds = sortBy(
|
||||
availableRepoIds,
|
||||
(r) => r.owner,
|
||||
(r) => r.name
|
||||
);
|
||||
return {type: "VALID", availableRepoIds: sortedRepoIds, selectedRepoId};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {type: "FAILURE"};
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalStoreRepositorySelect extends React.Component<{|
|
||||
+status: Status,
|
||||
+onChange: (repoId: RepoId) => void,
|
||||
+localStore: LocalStore,
|
||||
+children: ({
|
||||
status: Status,
|
||||
onChange: (selectedRepoId: RepoId) => void,
|
||||
}) => Node,
|
||||
|}> {
|
||||
render() {
|
||||
return this.props.children({
|
||||
status: this.props.status,
|
||||
onChange: (repoId) => {
|
||||
this.props.onChange(repoId);
|
||||
this.props.localStore.set(REPO_ID_KEY, repoId);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type PureRepositorySelectProps = {|
|
||||
+onChange: (x: RepoId) => void,
|
||||
+status: Status,
|
||||
|};
|
||||
export class PureRepositorySelect extends React.PureComponent<
|
||||
PureRepositorySelectProps
|
||||
> {
|
||||
renderSelect(
|
||||
availableRepoIds: $ReadOnlyArray<RepoId>,
|
||||
selectedRepoId: ?RepoId
|
||||
) {
|
||||
return (
|
||||
<label>
|
||||
<span>Please choose a repository to inspect:</span>{" "}
|
||||
{selectedRepoId != null && (
|
||||
<select
|
||||
value={repoIdToString(selectedRepoId)}
|
||||
onChange={(e) => {
|
||||
const repoId = stringToRepoId(e.target.value);
|
||||
this.props.onChange(repoId);
|
||||
}}
|
||||
>
|
||||
{availableRepoIds.map((repoId) => {
|
||||
const repoIdString = repoIdToString(repoId);
|
||||
return (
|
||||
<option value={repoIdString} key={repoIdString}>
|
||||
{repoIdString}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
renderError(text: string) {
|
||||
return <span style={{fontWeight: "bold", color: "red"}}>{text}</span>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {status} = this.props;
|
||||
switch (status.type) {
|
||||
case "LOADING":
|
||||
// Just show an empty select while we wait.
|
||||
return this.renderSelect([], null);
|
||||
case "VALID":
|
||||
return this.renderSelect(
|
||||
status.availableRepoIds,
|
||||
status.selectedRepoId
|
||||
);
|
||||
case "NO_REPOS":
|
||||
return this.renderError("Error: No repositories found.");
|
||||
case "FAILURE":
|
||||
return this.renderError(
|
||||
"Error: Unable to load repository registry. " +
|
||||
"See console for details."
|
||||
);
|
||||
default:
|
||||
throw new Error((status.type: empty));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,411 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {shallow, mount} from "enzyme";
|
||||
|
||||
import * as NullUtil from "../util/null";
|
||||
import testLocalStore from "../webutil/testLocalStore";
|
||||
import RepositorySelect, {
|
||||
PureRepositorySelect,
|
||||
LocalStoreRepositorySelect,
|
||||
loadStatus,
|
||||
type Status,
|
||||
REPO_ID_KEY,
|
||||
} from "./RepositorySelect";
|
||||
import {Assets} from "../webutil/assets";
|
||||
|
||||
import {
|
||||
toJSON,
|
||||
type RepoIdRegistry,
|
||||
REPO_ID_REGISTRY_API,
|
||||
} from "./repoIdRegistry";
|
||||
import {makeRepoId} from "../core/repoId";
|
||||
|
||||
require("../webutil/testUtil").configureEnzyme();
|
||||
require("../webutil/testUtil").configureAphrodite();
|
||||
|
||||
describe("explorer/RepositorySelect", () => {
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
});
|
||||
|
||||
function mockRegistry(registry: RepoIdRegistry) {
|
||||
fetch.mockResponseOnce(JSON.stringify(toJSON(registry)));
|
||||
}
|
||||
describe("PureRepositorySelect", () => {
|
||||
it("doesn't render a select while loading", () => {
|
||||
const e = shallow(
|
||||
<PureRepositorySelect status={{type: "LOADING"}} onChange={jest.fn()} />
|
||||
);
|
||||
const span = e.find("span");
|
||||
expect(span.text()).toBe("Please choose a repository to inspect:");
|
||||
const select = e.find("select");
|
||||
expect(select).toHaveLength(0);
|
||||
});
|
||||
it("renders an error message if no repositories are available", () => {
|
||||
const e = shallow(
|
||||
<PureRepositorySelect
|
||||
status={{type: "NO_REPOS"}}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const span = e.find("span");
|
||||
expect(span.text()).toBe("Error: No repositories found.");
|
||||
});
|
||||
it("renders an error message if there was an error while loading", () => {
|
||||
const e = shallow(
|
||||
<PureRepositorySelect status={{type: "FAILURE"}} onChange={jest.fn()} />
|
||||
);
|
||||
const span = e.find("span");
|
||||
expect(span.text()).toBe(
|
||||
"Error: Unable to load repository registry. See console for details."
|
||||
);
|
||||
});
|
||||
it("renders a select with all available repoIds as options", () => {
|
||||
const availableRepoIds = [
|
||||
makeRepoId("foo", "bar"),
|
||||
makeRepoId("zod", "zoink"),
|
||||
];
|
||||
const selectedRepoId = availableRepoIds[0];
|
||||
const e = shallow(
|
||||
<PureRepositorySelect
|
||||
status={{type: "VALID", availableRepoIds, selectedRepoId}}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const options = e.find("option");
|
||||
expect(options.map((x) => x.text())).toEqual(["foo/bar", "zod/zoink"]);
|
||||
});
|
||||
it("the selectedRepoId is selected", () => {
|
||||
const availableRepoIds = [
|
||||
makeRepoId("foo", "bar"),
|
||||
makeRepoId("zod", "zoink"),
|
||||
];
|
||||
const selectedRepoId = availableRepoIds[0];
|
||||
const e = shallow(
|
||||
<PureRepositorySelect
|
||||
status={{type: "VALID", availableRepoIds, selectedRepoId}}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(e.find("select").prop("value")).toBe("foo/bar");
|
||||
});
|
||||
it("clicking an option triggers the onChange", () => {
|
||||
const availableRepoIds = [
|
||||
makeRepoId("foo", "bar"),
|
||||
makeRepoId("zod", "zoink"),
|
||||
];
|
||||
const onChange = jest.fn();
|
||||
const e = shallow(
|
||||
<PureRepositorySelect
|
||||
status={{
|
||||
type: "VALID",
|
||||
availableRepoIds,
|
||||
selectedRepoId: availableRepoIds[0],
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
e.find("select").simulate("change", {target: {value: "zod/zoink"}});
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenLastCalledWith(availableRepoIds[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadStatus", () => {
|
||||
const assets = new Assets("/my/gateway/");
|
||||
function expectLoadValidStatus(
|
||||
localStore,
|
||||
expectedAvailableRepoIds,
|
||||
expectedSelectedRepoId
|
||||
) {
|
||||
const result = loadStatus(assets, localStore);
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith("/my/gateway" + REPO_ID_REGISTRY_API);
|
||||
expect.assertions(7);
|
||||
return result.then((status) => {
|
||||
expect(status.type).toBe("VALID");
|
||||
if (status.type !== "VALID") {
|
||||
throw new Error("Impossible");
|
||||
}
|
||||
expect(status.availableRepoIds).toEqual(expectedAvailableRepoIds);
|
||||
expect(status.selectedRepoId).toEqual(expectedSelectedRepoId);
|
||||
});
|
||||
}
|
||||
it("calls fetch and handles a simple success", () => {
|
||||
const repoId = makeRepoId("foo", "bar");
|
||||
mockRegistry([repoId]);
|
||||
return expectLoadValidStatus(testLocalStore(), [repoId], repoId);
|
||||
});
|
||||
it("returns repoIds in sorted order, and selects the last repoId", () => {
|
||||
const repoIds = [
|
||||
makeRepoId("a", "b"),
|
||||
makeRepoId("a", "z"),
|
||||
makeRepoId("foo", "bar"),
|
||||
];
|
||||
const nonSortedRepoIds = [repoIds[2], repoIds[0], repoIds[1]];
|
||||
mockRegistry(nonSortedRepoIds);
|
||||
return expectLoadValidStatus(testLocalStore(), repoIds, repoIds[1]);
|
||||
});
|
||||
it("returns FAILURE on invalid fetch response", () => {
|
||||
fetch.mockResponseOnce(JSON.stringify(["hello"]));
|
||||
expect.assertions(4);
|
||||
return loadStatus(assets, testLocalStore()).then((status) => {
|
||||
expect(status).toEqual({type: "FAILURE"});
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
// $ExpectFlowError
|
||||
console.error = jest.fn();
|
||||
});
|
||||
});
|
||||
it("returns FAILURE on fetch failure", () => {
|
||||
fetch.mockReject(new Error("some failure"));
|
||||
expect.assertions(4);
|
||||
return loadStatus(assets, testLocalStore()).then((status) => {
|
||||
expect(status).toEqual({type: "FAILURE"});
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
// $ExpectFlowError
|
||||
console.error = jest.fn();
|
||||
});
|
||||
});
|
||||
it("returns NO_REPOS on fetch 404", () => {
|
||||
fetch.mockResponseOnce("irrelevant", {status: 404});
|
||||
expect.assertions(3);
|
||||
return loadStatus(assets, testLocalStore()).then((status) => {
|
||||
expect(status).toEqual({type: "NO_REPOS"});
|
||||
});
|
||||
});
|
||||
it("loads selectedRepoId from localStore, if available", () => {
|
||||
const repoIds = [
|
||||
makeRepoId("a", "b"),
|
||||
makeRepoId("a", "z"),
|
||||
makeRepoId("foo", "bar"),
|
||||
];
|
||||
mockRegistry(repoIds);
|
||||
const localStore = testLocalStore();
|
||||
localStore.set(REPO_ID_KEY, {owner: "a", name: "z"});
|
||||
return expectLoadValidStatus(localStore, repoIds, repoIds[1]);
|
||||
});
|
||||
it("ignores selectedRepoId from localStore, if not available", () => {
|
||||
const repoIds = [
|
||||
makeRepoId("a", "b"),
|
||||
makeRepoId("a", "z"),
|
||||
makeRepoId("foo", "bar"),
|
||||
];
|
||||
mockRegistry(repoIds);
|
||||
const localStore = testLocalStore();
|
||||
localStore.set(REPO_ID_KEY, {owner: "non", name: "existent"});
|
||||
return expectLoadValidStatus(localStore, repoIds, repoIds[2]);
|
||||
});
|
||||
it("ignores malformed value in localStore", () => {
|
||||
const repoIds = [
|
||||
makeRepoId("a", "b"),
|
||||
makeRepoId("a", "z"),
|
||||
makeRepoId("foo", "bar"),
|
||||
];
|
||||
mockRegistry(repoIds);
|
||||
const localStore = testLocalStore();
|
||||
localStore.set(REPO_ID_KEY, 42);
|
||||
return expectLoadValidStatus(localStore, repoIds, repoIds[2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LocalStoreRepositorySelect", () => {
|
||||
it("instantiates the child component", () => {
|
||||
const status = {type: "LOADING"};
|
||||
const onChange = jest.fn();
|
||||
const e = shallow(
|
||||
<LocalStoreRepositorySelect
|
||||
onChange={onChange}
|
||||
status={status}
|
||||
localStore={testLocalStore()}
|
||||
>
|
||||
{({status, onChange}) => (
|
||||
<PureRepositorySelect status={status} onChange={onChange} />
|
||||
)}
|
||||
</LocalStoreRepositorySelect>
|
||||
);
|
||||
const child = e.find("PureRepositorySelect");
|
||||
expect(child.props().status).toEqual(status);
|
||||
});
|
||||
it("passes onChange result up to parent", () => {
|
||||
const status = {type: "LOADING"};
|
||||
const onChange = jest.fn();
|
||||
let childOnChange;
|
||||
shallow(
|
||||
<LocalStoreRepositorySelect
|
||||
onChange={onChange}
|
||||
status={status}
|
||||
localStore={testLocalStore()}
|
||||
>
|
||||
{({status, onChange}) => {
|
||||
childOnChange = onChange;
|
||||
return <PureRepositorySelect status={status} onChange={onChange} />;
|
||||
}}
|
||||
</LocalStoreRepositorySelect>
|
||||
);
|
||||
const repoId = {owner: "foo", name: "bar"};
|
||||
NullUtil.get(childOnChange)(repoId);
|
||||
expect(onChange).toHaveBeenCalledWith(repoId);
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("stores onChange result in localStore", () => {
|
||||
const status = {type: "LOADING"};
|
||||
const onChange = jest.fn();
|
||||
const localStore = testLocalStore();
|
||||
let childOnChange;
|
||||
shallow(
|
||||
<LocalStoreRepositorySelect
|
||||
onChange={onChange}
|
||||
status={status}
|
||||
localStore={localStore}
|
||||
>
|
||||
{({status, onChange}) => {
|
||||
childOnChange = onChange;
|
||||
return <PureRepositorySelect status={status} onChange={onChange} />;
|
||||
}}
|
||||
</LocalStoreRepositorySelect>
|
||||
);
|
||||
const repoId = {owner: "foo", name: "bar"};
|
||||
NullUtil.get(childOnChange)(repoId);
|
||||
expect(localStore.get(REPO_ID_KEY)).toEqual(repoId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RepositorySelect", () => {
|
||||
const assets = new Assets("/my/gateway/");
|
||||
|
||||
it("calls `loadStatus` with the proper assets", () => {
|
||||
mockRegistry([makeRepoId("irrelevant", "unused")]);
|
||||
shallow(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={jest.fn()}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
// A bit of overlap with tests for `loadStatus` directly---it'd be
|
||||
// nicer to spy on `loadStatus`, but that's at module top level,
|
||||
// so `RepositorySelect` closes over it directly.
|
||||
expect(fetch).toHaveBeenCalledWith("/my/gateway" + REPO_ID_REGISTRY_API);
|
||||
});
|
||||
|
||||
it("initially renders a LocalStoreRepositorySelect with status LOADING", () => {
|
||||
mockRegistry([makeRepoId("irrelevant", "unused")]);
|
||||
const e = shallow(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={jest.fn()}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
const child = e.find(LocalStoreRepositorySelect);
|
||||
const status = child.props().status;
|
||||
const onChange = jest.fn();
|
||||
expect(status).toEqual({type: "LOADING"});
|
||||
const grandChild = child.props().children({status, onChange});
|
||||
expect(grandChild.type).toBe(PureRepositorySelect);
|
||||
});
|
||||
|
||||
function waitForUpdate(enzymeWrapper) {
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
enzymeWrapper.update();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("on successful load, sets the status on the child", async () => {
|
||||
const onChange = jest.fn();
|
||||
const selectedRepoId = makeRepoId("foo", "bar");
|
||||
mockRegistry([selectedRepoId]);
|
||||
const e = shallow(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={onChange}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
await waitForUpdate(e);
|
||||
const childStatus = e.props().status;
|
||||
const availableRepoIds = [selectedRepoId];
|
||||
expect(childStatus).toEqual({
|
||||
type: "VALID",
|
||||
selectedRepoId,
|
||||
availableRepoIds,
|
||||
});
|
||||
});
|
||||
|
||||
it("on successful load, passes the status to the onChange", async () => {
|
||||
const onChange = jest.fn();
|
||||
const repoId = makeRepoId("foo", "bar");
|
||||
mockRegistry([repoId]);
|
||||
const e = shallow(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={onChange}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
await waitForUpdate(e);
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith(repoId);
|
||||
});
|
||||
|
||||
it("on failed load, onChange not called", async () => {
|
||||
const onChange = jest.fn();
|
||||
fetch.mockReject(new Error("something bad"));
|
||||
|
||||
const e = shallow(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={onChange}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
await waitForUpdate(e);
|
||||
expect(onChange).toHaveBeenCalledTimes(0);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
// $ExpectFlowError
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
it("child onChange triggers parent onChange", () => {
|
||||
const onChange = jest.fn();
|
||||
const repoId = makeRepoId("foo", "bar");
|
||||
mockRegistry([repoId]);
|
||||
const e = mount(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={onChange}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
const child = e.find(PureRepositorySelect);
|
||||
child.props().onChange(repoId);
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith(repoId);
|
||||
});
|
||||
|
||||
it("selecting child option updates top-level state", async () => {
|
||||
const onChange = jest.fn();
|
||||
const repoIds = [makeRepoId("foo", "bar"), makeRepoId("z", "a")];
|
||||
mockRegistry(repoIds);
|
||||
const e = mount(
|
||||
<RepositorySelect
|
||||
assets={assets}
|
||||
onChange={onChange}
|
||||
localStore={testLocalStore()}
|
||||
/>
|
||||
);
|
||||
await waitForUpdate(e);
|
||||
const child = e.find(PureRepositorySelect);
|
||||
child.props().onChange(repoIds[0]);
|
||||
const status: Status = e.state().status;
|
||||
expect(status.type).toEqual("VALID");
|
||||
if (status.type !== "VALID") {
|
||||
throw new Error("Impossible");
|
||||
}
|
||||
expect(status.selectedRepoId).toEqual(repoIds[0]);
|
||||
});
|
||||
});
|
||||
});
|
@ -26,14 +26,10 @@ import {weightsToEdgeEvaluator} from "../analysis/weightsToEdgeEvaluator";
|
||||
|
||||
export type LoadingState = "NOT_LOADING" | "LOADING" | "FAILED";
|
||||
export type AppState =
|
||||
| Uninitialized
|
||||
| ReadyToLoadGraph
|
||||
| ReadyToRunPagerank
|
||||
| PagerankEvaluated;
|
||||
|
||||
export type Uninitialized = {|
|
||||
+type: "UNINITIALIZED",
|
||||
|};
|
||||
export type ReadyToLoadGraph = {|
|
||||
+type: "READY_TO_LOAD_GRAPH",
|
||||
+repoId: RepoId,
|
||||
@ -53,6 +49,10 @@ export type PagerankEvaluated = {|
|
||||
+loading: LoadingState,
|
||||
|};
|
||||
|
||||
export function initialState(repoId: RepoId): ReadyToLoadGraph {
|
||||
return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"};
|
||||
}
|
||||
|
||||
export function createStateTransitionMachine(
|
||||
getState: () => AppState,
|
||||
setState: (AppState) => void
|
||||
@ -65,13 +65,8 @@ export function createStateTransitionMachine(
|
||||
);
|
||||
}
|
||||
|
||||
export function uninitializedState(): AppState {
|
||||
return {type: "UNINITIALIZED"};
|
||||
}
|
||||
|
||||
// Exported for testing purposes.
|
||||
export interface StateTransitionMachineInterface {
|
||||
+setRepoId: (RepoId) => void;
|
||||
+loadGraph: (Assets, StaticAdapterSet) => Promise<boolean>;
|
||||
+runPagerank: (WeightedTypes, NodeAddressT) => Promise<void>;
|
||||
+loadGraphAndRunPagerank: (
|
||||
@ -119,15 +114,6 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||
this.pagerank = pagerank;
|
||||
}
|
||||
|
||||
setRepoId(repoId: RepoId) {
|
||||
const newState: AppState = {
|
||||
type: "READY_TO_LOAD_GRAPH",
|
||||
repoId: repoId,
|
||||
loading: "NOT_LOADING",
|
||||
};
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
/** Loads the graph, reports whether it was successful */
|
||||
async loadGraph(
|
||||
assets: Assets,
|
||||
@ -222,9 +208,6 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||
) {
|
||||
const state = this.getState();
|
||||
const type = state.type;
|
||||
if (type === "UNINITIALIZED") {
|
||||
throw new Error("Tried to load and run from incorrect state");
|
||||
}
|
||||
switch (type) {
|
||||
case "READY_TO_LOAD_GRAPH":
|
||||
const loadedGraph = await this.loadGraph(assets, adapters);
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import {
|
||||
StateTransitionMachine,
|
||||
uninitializedState,
|
||||
type AppState,
|
||||
type GraphWithAdapters,
|
||||
} from "./state";
|
||||
@ -82,62 +81,12 @@ describe("explorer/state", () => {
|
||||
return new Map();
|
||||
}
|
||||
function loading(state: AppState) {
|
||||
if (state.type === "UNINITIALIZED") {
|
||||
throw new Error("Tried to get invalid loading");
|
||||
}
|
||||
return state.loading;
|
||||
}
|
||||
function getRepoId(state: AppState) {
|
||||
if (state.type === "UNINITIALIZED") {
|
||||
throw new Error("Tried to get invalid repoId");
|
||||
}
|
||||
return state.repoId;
|
||||
}
|
||||
|
||||
describe("setRepoId", () => {
|
||||
describe("in UNINITIALIZED", () => {
|
||||
it("transitions to READY_TO_LOAD_GRAPH", () => {
|
||||
const {getState, stm} = example(uninitializedState());
|
||||
const repoId = makeRepoId("foo", "bar");
|
||||
stm.setRepoId(repoId);
|
||||
const state = getState();
|
||||
expect(state.type).toBe("READY_TO_LOAD_GRAPH");
|
||||
expect(getRepoId(state)).toEqual(repoId);
|
||||
});
|
||||
});
|
||||
it("stays in READY_TO_LOAD_GRAPH with new repoId", () => {
|
||||
const {getState, stm} = example(readyToLoadGraph());
|
||||
const repoId = makeRepoId("zoink", "zod");
|
||||
stm.setRepoId(repoId);
|
||||
const state = getState();
|
||||
expect(state.type).toBe("READY_TO_LOAD_GRAPH");
|
||||
expect(getRepoId(state)).toEqual(repoId);
|
||||
});
|
||||
it("transitions READY_TO_RUN_PAGERANK to READY_TO_LOAD_GRAPH with new repoId", () => {
|
||||
const {getState, stm} = example(readyToRunPagerank());
|
||||
const repoId = makeRepoId("zoink", "zod");
|
||||
stm.setRepoId(repoId);
|
||||
const state = getState();
|
||||
expect(state.type).toBe("READY_TO_LOAD_GRAPH");
|
||||
expect(getRepoId(state)).toEqual(repoId);
|
||||
});
|
||||
it("transitions PAGERANK_EVALUATED to READY_TO_LOAD_GRAPH with new repoId", () => {
|
||||
const {getState, stm} = example(pagerankEvaluated());
|
||||
const repoId = makeRepoId("zoink", "zod");
|
||||
stm.setRepoId(repoId);
|
||||
const state = getState();
|
||||
expect(state.type).toBe("READY_TO_LOAD_GRAPH");
|
||||
expect(getRepoId(state)).toEqual(repoId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadGraph", () => {
|
||||
it("can only be called when READY_TO_LOAD_GRAPH", async () => {
|
||||
const badStates = [
|
||||
uninitializedState(),
|
||||
readyToRunPagerank(),
|
||||
pagerankEvaluated(),
|
||||
];
|
||||
const badStates = [readyToRunPagerank(), pagerankEvaluated()];
|
||||
for (const b of badStates) {
|
||||
const {stm} = example(b);
|
||||
await expect(
|
||||
@ -182,26 +131,6 @@ describe("explorer/state", () => {
|
||||
}
|
||||
expect(state.graphWithAdapters).toBe(gwa);
|
||||
});
|
||||
it("does not transition if repoId transition happens first", async () => {
|
||||
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
|
||||
const swappedRepoId = makeRepoId("too", "fast");
|
||||
loadGraphMock.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
stm.setRepoId(swappedRepoId);
|
||||
resolve(graphWithAdapters());
|
||||
})
|
||||
);
|
||||
const succeeded = await stm.loadGraph(
|
||||
new Assets("/my/gateway/"),
|
||||
new StaticAdapterSet([])
|
||||
);
|
||||
expect(succeeded).toBe(false);
|
||||
const state = getState();
|
||||
expect(loading(state)).toBe("NOT_LOADING");
|
||||
expect(state.type).toBe("READY_TO_LOAD_GRAPH");
|
||||
expect(getRepoId(state)).toEqual(swappedRepoId);
|
||||
});
|
||||
it("sets loading state FAILED on reject", async () => {
|
||||
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
|
||||
const error = new Error("Oh no!");
|
||||
@ -223,13 +152,11 @@ describe("explorer/state", () => {
|
||||
|
||||
describe("runPagerank", () => {
|
||||
it("can only be called when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
|
||||
const badStates = [uninitializedState(), readyToLoadGraph()];
|
||||
for (const b of badStates) {
|
||||
const {stm} = example(b);
|
||||
await expect(
|
||||
stm.runPagerank(weightedTypes(), NodeAddress.empty)
|
||||
).rejects.toThrow("incorrect state");
|
||||
}
|
||||
const badState = readyToLoadGraph();
|
||||
const {stm} = example(badState);
|
||||
await expect(
|
||||
stm.runPagerank(weightedTypes(), NodeAddress.empty)
|
||||
).rejects.toThrow("incorrect state");
|
||||
});
|
||||
it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
|
||||
const goodStates = [readyToRunPagerank(), pagerankEvaluated()];
|
||||
@ -259,22 +186,6 @@ describe("explorer/state", () => {
|
||||
const args = pagerankMock.mock.calls[0];
|
||||
expect(args[2].totalScoreNodePrefix).toBe(foo);
|
||||
});
|
||||
it("does not transition if a repoId change happens first", async () => {
|
||||
const {getState, stm, pagerankMock} = example(readyToRunPagerank());
|
||||
const swappedRepoId = makeRepoId("too", "fast");
|
||||
pagerankMock.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
stm.setRepoId(swappedRepoId);
|
||||
resolve(graphWithAdapters());
|
||||
})
|
||||
);
|
||||
await stm.runPagerank(weightedTypes(), NodeAddress.empty);
|
||||
const state = getState();
|
||||
expect(loading(state)).toBe("NOT_LOADING");
|
||||
expect(state.type).toBe("READY_TO_LOAD_GRAPH");
|
||||
expect(getRepoId(state)).toBe(swappedRepoId);
|
||||
});
|
||||
it("sets loading state FAILED on reject", async () => {
|
||||
const {getState, stm, pagerankMock} = example(readyToRunPagerank());
|
||||
const error = new Error("Oh no!");
|
||||
@ -291,17 +202,6 @@ describe("explorer/state", () => {
|
||||
});
|
||||
|
||||
describe("loadGraphAndRunPagerank", () => {
|
||||
it("errors if called with uninitialized state", async () => {
|
||||
const {stm} = example(uninitializedState());
|
||||
await expect(
|
||||
stm.loadGraphAndRunPagerank(
|
||||
new Assets("gateway"),
|
||||
new StaticAdapterSet([]),
|
||||
weightedTypes(),
|
||||
NodeAddress.empty
|
||||
)
|
||||
).rejects.toThrow("incorrect state");
|
||||
});
|
||||
it("when READY_TO_LOAD_GRAPH, loads graph then runs pagerank", async () => {
|
||||
const {stm} = example(readyToLoadGraph());
|
||||
(stm: any).loadGraph = jest.fn();
|
||||
|
@ -4,28 +4,14 @@ import React, {type ComponentType} from "react";
|
||||
|
||||
import type {RepoId} from "../core/repoId";
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import HomepageExplorer from "./homepageExplorer";
|
||||
|
||||
export default function makeProjectPage(
|
||||
repoId: RepoId
|
||||
): ComponentType<{|+assets: Assets|}> {
|
||||
return class ProjectPage extends React.Component<{|+assets: Assets|}> {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
marginBottom: 200,
|
||||
padding: "0 10px",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<strong>TODO:</strong> Render an explorer for{" "}
|
||||
{`${repoId.owner}/${repoId.name}`}
|
||||
</p>.
|
||||
</div>
|
||||
);
|
||||
return <HomepageExplorer assets={this.props.assets} repoId={repoId} />;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {StaticAppAdapter as GithubAdapter} from "../plugins/github/appAdapter";
|
||||
import {StaticAppAdapter as GitAdapter} from "../plugins/git/appAdapter";
|
||||
import {GithubGitGateway} from "../plugins/github/githubGitGateway";
|
||||
import {AppPage} from "../explorer/App";
|
||||
import type {RepoId} from "../core/repoId";
|
||||
|
||||
function homepageStaticAdapters(): StaticAdapterSet {
|
||||
return new StaticAdapterSet([
|
||||
@ -18,10 +19,15 @@ function homepageStaticAdapters(): StaticAdapterSet {
|
||||
|
||||
export default class HomepageExplorer extends React.Component<{|
|
||||
+assets: Assets,
|
||||
+repoId: RepoId,
|
||||
|}> {
|
||||
render() {
|
||||
return (
|
||||
<AppPage assets={this.props.assets} adapters={homepageStaticAdapters()} />
|
||||
<AppPage
|
||||
assets={this.props.assets}
|
||||
repoId={this.props.repoId}
|
||||
adapters={homepageStaticAdapters()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -39,13 +39,13 @@ function makeRouteData(registry /*: RepoIdRegistry */) /*: RouteData */ {
|
||||
navTitle: "Home",
|
||||
},
|
||||
{
|
||||
path: "/prototypes/",
|
||||
path: "/prototype/",
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./PrototypesPage").default(registry),
|
||||
},
|
||||
title: "SourceCred prototypes",
|
||||
navTitle: null, // for now
|
||||
title: "SourceCred prototype",
|
||||
navTitle: "Prototype",
|
||||
},
|
||||
...registry.map((repo) => ({
|
||||
path: `/prototypes/${repo.owner}/${repo.name}/`,
|
||||
@ -56,15 +56,6 @@ function makeRouteData(registry /*: RepoIdRegistry */) /*: RouteData */ {
|
||||
title: `${repo.owner}/${repo.name} • SourceCred`,
|
||||
navTitle: null,
|
||||
})),
|
||||
{
|
||||
path: "/prototype/",
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./homepageExplorer").default,
|
||||
},
|
||||
title: "SourceCred prototype",
|
||||
navTitle: "Prototype",
|
||||
},
|
||||
{
|
||||
path: "/discord-invite/",
|
||||
contents: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user