Remove the artifact plugin (#303)

Given that we are undergoing a major world-changing refactor (#190), all
outstanding code needs to be refactored to use the new conventions. We
don't actually use the Artifact Plugin yet, and reading the code, it
needs non-trivial rewrites to be in sync with the new world.

Rather than maintain it now, I am deleting it; we can regain the context
when the time is ripe to setup and integrate the plugin.

Test plan: Travis passes. `yarn start` produces no references to the
artifact editor.
This commit is contained in:
Dandelion Mané 2018-05-25 19:29:36 -07:00 committed by GitHub
parent f0fcf02791
commit c68b78f959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 0 additions and 1745 deletions

View File

@ -3,12 +3,10 @@
import React from "react";
import {BrowserRouter as Router, Route, NavLink} from "react-router-dom";
import ArtifactEditor from "../plugins/artifact/editor/App";
import CredExplorer from "./credExplorer/App";
export default class App extends React.Component<{}> {
render() {
const ARTIFACT_EDITOR_ROUTE = "/plugins/artifact/editor";
const CRED_EXPLORER_ROUTE = "/explorer";
return (
<Router>
@ -21,16 +19,12 @@ export default class App extends React.Component<{}> {
<li>
<NavLink to={CRED_EXPLORER_ROUTE}>Cred Explorer</NavLink>
</li>
<li>
<NavLink to={ARTIFACT_EDITOR_ROUTE}>Artifact Editor</NavLink>
</li>
</ul>
</nav>
<hr />
<Route exact path="/" component={Home} />
<Route path={CRED_EXPLORER_ROUTE} component={CredExplorer} />
<Route path={ARTIFACT_EDITOR_ROUTE} component={ArtifactEditor} />
</div>
</Router>
);

View File

@ -1,45 +0,0 @@
// @flow
import type {Address} from "../../core/address";
import type {Graph} from "../../core/graph";
export const ARTIFACT_PLUGIN_NAME = "sourcecred/artifact-beta";
export const ARTIFACT_NODE_TYPE = "ARTIFACT";
export type ArtifactNodePayload = {|
+name: string,
+description: string,
|};
export type NodePayload = ArtifactNodePayload;
export const INCLUDES_EDGE_TYPE = "INCLUDES";
export type IncludesEdgePayload = {|
+weight: number, // non-negative
|};
export type EdgePayload = IncludesEdgePayload;
const NON_SLUG_CHARACTER: RegExp = /[^a-z]/g;
export function artifactAddress(
graph: Graph,
repoOwner: string,
repoName: string,
artifactName: string
): Address {
const baseName = artifactName.toLowerCase().replace(NON_SLUG_CHARACTER, "-");
const baseId = `${repoOwner}/${repoName}/${baseName}`;
function address(id) {
return {
pluginName: ARTIFACT_PLUGIN_NAME,
id,
type: ARTIFACT_NODE_TYPE,
};
}
let id = baseId;
for (let i = 0; graph.node(address(id)) != null; i++) {
id = baseId + "-" + i;
}
return address(id);
}

View File

@ -1,53 +0,0 @@
// @flow
import {Graph} from "../../core/graph";
import {artifactAddress} from "./artifactPlugin";
describe("artifactPlugin", () => {
describe("artifactAddress", () => {
it("repositoryName included in id", () => {
const a = artifactAddress(
new Graph(),
"not-sourcecred",
"not-artifact-plugin",
"Sample artifact!"
);
expect(a.id.startsWith("not-sourcecred/not-artifact-plugin")).toBe(true);
});
it("slugifies the artifact name", () => {
const a = artifactAddress(
new Graph(),
"not-sourcecred",
"not-artifact-plugin",
"Sample artifact!"
);
expect(a.id).toEqual(
"not-sourcecred/not-artifact-plugin/sample-artifact-"
);
});
it("resolves collisions", () => {
const g = new Graph();
const ids = [];
for (let i = 0; i < 3; i++) {
const a = artifactAddress(
g,
"not-sourcecred",
"not-artifact-plugin",
"Sample artifact!"
);
ids.push(a.id);
g.addNode({
address: a,
payload: {name: "Sample artifact!", description: ""},
});
}
expect(ids).toEqual([
"not-sourcecred/not-artifact-plugin/sample-artifact-",
"not-sourcecred/not-artifact-plugin/sample-artifact--0",
"not-sourcecred/not-artifact-plugin/sample-artifact--1",
]);
});
});
});

View File

@ -1,72 +0,0 @@
// @flow
import React from "react";
import {StyleSheet, css} from "aphrodite/no-important";
import "./pluginAdapter";
import type {Graph, Node} from "../../../core/graph";
import type {NodePayload as ArtifactNodePayload} from "../artifactPlugin";
import type {Settings} from "./SettingsConfig";
import {ArtifactGraphEditor} from "./ArtifactGraphEditor";
import {ContributionList} from "./ContributionList";
import {GithubGraphFetcher} from "./GithubGraphFetcher";
import {SettingsConfig, defaultSettings} from "./SettingsConfig";
import standardAdapterSet from "./standardAdapterSet";
type Props = {};
type State = {
artifacts: Node<ArtifactNodePayload>[],
githubGraph: ?Graph,
artifactGraph: ?Graph,
settings: Settings,
};
export default class App extends React.Component<Props, State> {
constructor() {
super();
this.state = {
artifacts: [],
githubGraph: null,
artifactGraph: null,
settings: defaultSettings(),
};
}
render() {
return (
<div>
<header className={css(styles.header)}>
<h1>Artifact editor</h1>
</header>
<SettingsConfig
onChange={(settings) => {
this.setState({settings});
}}
/>
<GithubGraphFetcher
settings={this.state.settings}
onCreateGraph={(githubGraph) => {
this.setState({githubGraph});
}}
/>
<ArtifactGraphEditor
settings={this.state.settings}
onChange={(artifactGraph) => {
this.setState({artifactGraph});
}}
/>
<ContributionList
graph={this.state.githubGraph}
adapters={standardAdapterSet}
/>
</div>
);
}
}
const styles = StyleSheet.create({
header: {
color: "#f0f",
},
});

View File

@ -1,13 +0,0 @@
// @flow
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
require("../../../app/testUtil").configureAphrodite();
// Check that PropTypes check out.
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -1,124 +0,0 @@
// @flow
import React from "react";
import type {Node} from "../../../core/graph";
import type {Settings} from "./SettingsConfig";
import type {NodePayload} from "../artifactPlugin";
import {Graph} from "../../../core/graph";
import {artifactAddress} from "../artifactPlugin";
type Props = {
settings: Settings,
onChange: (Graph) => void,
};
type State = {
graph: Graph,
artifactInProgressName: string,
};
export class ArtifactGraphEditor extends React.Component<Props, State> {
constructor() {
super();
this.state = {
graph: new Graph(),
artifactInProgressName: "",
};
}
componentDidMount() {
this.props.onChange(this.state.graph);
}
addArtifact(name: string): void {
this.setState(
(state) => {
const node: Node<NodePayload> = {
address: artifactAddress(
state.graph,
this.props.settings.repoOwner,
this.props.settings.repoName,
name
),
payload: {name, description: ""},
};
return {graph: state.graph.copy().addNode(node)};
},
() => {
this.props.onChange(this.state.graph);
}
);
}
updateArtifactDescription(
oldArtifactNode: Node<NodePayload>,
newDescription: string
): void {
this.setState(
(state) => ({
graph: state.graph
.copy()
.removeNode(oldArtifactNode.address)
.addNode({
address: oldArtifactNode.address,
payload: {
name: oldArtifactNode.payload.name,
description: newDescription,
},
}),
}),
() => {
this.props.onChange(this.state.graph);
}
);
}
render() {
return (
<div>
<h2>Artifacts</h2>
<table>
<thead>
<tr>
<th>Artifact</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{this.state.graph.nodes().map((x) => (
<tr key={x.address.id}>
<td>{x.payload.name}</td>
<td>
<textarea
key={`description-${x.address.id}`}
value={x.payload.description}
onChange={(e) => {
this.updateArtifactDescription(x, e.target.value);
}}
/>
</td>
</tr>
))}
</tbody>
</table>
<input
value={this.state.artifactInProgressName}
onChange={(e) => {
const value = e.target.value;
this.setState({
artifactInProgressName: value,
});
}}
/>
<button
onClick={() => {
this.addArtifact(this.state.artifactInProgressName);
this.setState({artifactInProgressName: ""});
}}
>
Add artifact
</button>
</div>
);
}
}

View File

@ -1,113 +0,0 @@
// @flow
import React from "react";
import {shallow} from "enzyme";
import {Graph} from "../../../core/graph";
import {ArtifactGraphEditor} from "./ArtifactGraphEditor";
import {artifactAddress} from "../artifactPlugin";
require("../../../app/testUtil").configureAphrodite();
require("../../../app/testUtil").configureEnzyme();
describe("ArtifactGraphEditor", () => {
function createComponent(onChange) {
return (
<ArtifactGraphEditor
settings={{
githubApiToken: "123youdontneedme",
repoOwner: "sourcecred",
repoName: "artifact-tests",
}}
onChange={onChange}
/>
);
}
it("invokes its callback after mounting, not construction", () => {
const onChange = jest.fn();
const component = createComponent(onChange);
expect(onChange).not.toHaveBeenCalled();
shallow(component);
expect(onChange).toHaveBeenCalledTimes(1);
});
it("adds an artifact to the list", () => {
const onChange = jest.fn();
const element = shallow(createComponent(onChange));
expect(onChange).toHaveBeenLastCalledWith(new Graph());
element
.find("input")
.simulate("change", {target: {value: "Root artifact!"}});
expect(onChange).toHaveBeenCalledTimes(1);
element.find("button").simulate("click");
expect(
element.find("td").filterWhere((x) => x.text() === "Root artifact!")
).toHaveLength(1);
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenLastCalledWith(
new Graph().addNode({
address: artifactAddress(
new Graph(),
"sourcecred",
"artifact-tests",
"Root artifact!"
),
payload: {
name: "Root artifact!",
description: "",
},
})
);
});
it("modifies an artifact's description", () => {
const onChange = jest.fn();
const element = shallow(createComponent(onChange));
element
.find("input")
.simulate("change", {target: {value: "Root artifact!"}});
element.find("button").simulate("click");
element
.find("tr textarea")
.simulate("change", {target: {value: "for garlic, carrots, etc."}});
expect(onChange).toHaveBeenLastCalledWith(
new Graph().addNode({
address: artifactAddress(
new Graph(),
"sourcecred",
"artifact-tests",
"Root artifact!"
),
payload: {
name: "Root artifact!",
description: "for garlic, carrots, etc.",
},
})
);
});
it("does not mutate the graph passed to its callback", () => {
const onChange = jest.fn();
const element = shallow(createComponent(onChange));
const g1 = onChange.mock.calls[0][0];
const g1Copy = g1.copy();
element
.find("input")
.simulate("change", {target: {value: "Root artifact!"}});
element.find("button").simulate("click");
expect(onChange).toHaveBeenCalledTimes(2);
const g2 = onChange.mock.calls[1][0];
const g2Copy = g2.copy();
expect(g1.equals(g1Copy)).toBe(true);
expect(g1.equals(g2)).toBe(false);
element
.find("tr textarea")
.simulate("change", {target: {value: "for garlic, carrots, etc."}});
expect(onChange).toHaveBeenCalledTimes(3);
const g3 = onChange.mock.calls[2][0];
expect(g1.equals(g1Copy)).toBe(true);
expect(g2.equals(g2Copy)).toBe(true);
expect(g3.equals(g2)).toBe(false);
});
});

View File

@ -1,141 +0,0 @@
// @flow
import React from "react";
import type {Node} from "../../../core/graph";
import {AdapterSet} from "./adapterSet";
import {Graph} from "../../../core/graph";
type Props = {
graph: ?Graph,
adapters: AdapterSet,
};
type State = {
typeFilter: ?{|
+pluginName: string,
+type: string,
|},
};
export class ContributionList extends React.Component<Props, State> {
constructor() {
super();
this.state = {
typeFilter: null,
};
}
render() {
return (
<div>
<h2>Contributions</h2>
{this.renderFilterSelect()}
{this.renderTable()}
</div>
);
}
renderFilterSelect() {
if (this.props.graph == null) {
return null;
}
const graph: Graph = this.props.graph;
const typesByPlugin: {[pluginName: string]: Set<string>} = {};
graph.nodes().forEach((node) => {
const adapter = this.props.adapters.getAdapter(node);
if (adapter == null) {
return;
}
if (!typesByPlugin[adapter.pluginName]) {
typesByPlugin[adapter.pluginName] = new Set();
}
typesByPlugin[adapter.pluginName].add(node.address.type);
});
function optionGroup(pluginName: string) {
const header = (
<option key={pluginName} disabled style={{fontWeight: "bold"}}>
{pluginName}
</option>
);
const entries = Array.from(typesByPlugin[pluginName])
.sort()
.map((type) => (
<option key={type} value={JSON.stringify({pluginName, type})}>
{"\u2003" + type}
</option>
));
return [header, ...entries];
}
return (
<label>
Filter by contribution type:{" "}
<select
value={JSON.stringify(this.state.typeFilter)}
onChange={(e) => {
this.setState({typeFilter: JSON.parse(e.target.value)});
}}
>
<option value={JSON.stringify(null)}>Show all</option>
{Object.keys(typesByPlugin)
.sort()
.map(optionGroup)}
</select>
</label>
);
}
renderTable() {
if (this.props.graph == null) {
return <div>(no graph)</div>;
} else {
const graph: Graph = this.props.graph;
const {typeFilter} = this.state;
const shouldDisplay: (node: Node<any>) => boolean = typeFilter
? (node) => {
const adapter = this.props.adapters.getAdapter(node);
return (
!!adapter &&
adapter.pluginName === typeFilter.pluginName &&
node.address.type === typeFilter.type
);
}
: (_) => true;
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Artifact</th>
<th>Weight</th>
</tr>
</thead>
<tbody>
{this.props.graph.nodes().map((node) => {
if (!shouldDisplay(node)) {
return null;
}
const adapter = this.props.adapters.getAdapter(node);
if (adapter == null) {
return (
<tr key={JSON.stringify(node.address)}>
<td colSpan={3}>
<i>unknown</i> (plugin: {node.address.pluginName})
</td>
</tr>
);
} else {
return (
<tr key={JSON.stringify(node.address)}>
<td>{adapter.extractTitle(graph, node)}</td>
<td>[TODO]</td>
<td>[TODO]</td>
</tr>
);
}
})}
</tbody>
</table>
);
}
}
}

View File

@ -1,193 +0,0 @@
// @flow
import type {ReactWrapper} from "enzyme";
import React from "react";
import {shallow} from "enzyme";
import enzymeToJSON from "enzyme-to-json";
import type {Address} from "../../../core/address";
import type {Node} from "../../../core/graph";
import type {PluginAdapter} from "./pluginAdapter";
import {AdapterSet} from "./adapterSet";
import {ContributionList} from "./ContributionList";
import {Graph} from "../../../core/graph";
require("../../../app/testUtil").configureAphrodite();
require("../../../app/testUtil").configureEnzyme();
function createTestData(): * {
type PayloadA = number;
type PayloadB = boolean;
type PayloadC = string;
const PLUGIN_A = "sourcecred/example-plugin-a";
const PLUGIN_B = "sourcecred/example-plugin-b";
const PLUGIN_C = "sourcecred/example-plugin-c";
function makeAddress(
pluginName: typeof PLUGIN_A | typeof PLUGIN_B | typeof PLUGIN_C,
type: string,
id: string
): Address {
return {
pluginName,
id,
type,
};
}
const nodeA1 = () => ({
address: makeAddress(PLUGIN_A, "small", "one"),
payload: (111: PayloadA),
});
const nodeA2 = () => ({
address: makeAddress(PLUGIN_A, "small", "two"),
payload: (234: PayloadA),
});
const nodeA3 = () => ({
address: makeAddress(PLUGIN_A, "big", "three"),
payload: (616: PayloadA),
});
const nodeB4 = () => ({
address: makeAddress(PLUGIN_B, "very true", "four"),
payload: (true: PayloadB),
});
const nodeC5 = () => ({
address: makeAddress(PLUGIN_C, "ctype", "five"),
payload: ("I have no adapter :-(": PayloadC),
});
const edgeA1A2 = () => ({
address: makeAddress(PLUGIN_A, "atype", "one-to-two"),
payload: null,
src: nodeA1().address,
dst: nodeA2().address,
});
const edgeB4A3 = () => ({
address: makeAddress(PLUGIN_C, "ctype", "four-to-three"),
payload: null,
src: nodeB4().address,
dst: nodeA3().address,
});
const graph: () => Graph = () =>
new Graph()
.addNode(nodeA1())
.addNode(nodeA2())
.addNode(nodeA3())
.addNode(nodeB4())
.addNode(nodeC5())
.addEdge(edgeA1A2())
.addEdge(edgeB4A3());
const adapterA: () => PluginAdapter<PayloadA> = () => ({
pluginName: PLUGIN_A,
renderer: class RendererA extends React.Component<{
graph: Graph,
node: Node<PayloadA>,
}> {
render() {
const {graph, node} = this.props;
const neighborCount = graph.neighborhood(node.address, {
direction: "OUT",
}).length;
return (
<span>
<tt>{node.address.id}</tt> has neighbor count{" "}
<strong>{neighborCount}</strong>
</span>
);
}
},
extractTitle(graph: Graph, node: Node<PayloadA>) {
return `the number ${String(node.payload)}`;
},
});
const adapterB: () => PluginAdapter<PayloadB> = () => ({
pluginName: PLUGIN_B,
renderer: class RendererB extends React.Component<{
graph: Graph,
node: Node<PayloadB>,
}> {
render() {
const {node} = this.props;
return (
<span>
Node <em>{node.address.id}</em>: <strong>{node.payload}</strong>
</span>
);
}
},
extractTitle(graph: Graph, node: Node<PayloadB>) {
return String(node.payload).toUpperCase() + "!";
},
});
const adapters: () => AdapterSet = () => {
const result = new AdapterSet();
result.addAdapter(adapterA());
result.addAdapter(adapterB());
return result;
};
return {
PLUGIN_A,
PLUGIN_B,
PLUGIN_C,
nodeA1,
nodeA2,
nodeA3,
nodeB4,
nodeC5,
edgeA1A2,
edgeB4A3,
graph,
adapterA,
adapterB,
adapters,
};
}
describe("ContributionList", () => {
// Render a contribution list with the above test data.
function render() {
const data = createTestData();
const result = shallow(
<ContributionList graph={data.graph()} adapters={data.adapters()} />
);
return result;
}
// Select the unique <option> whose text matches the given patern.
function simulateSelect(container: ReactWrapper, pattern: RegExp): void {
const targetOption = container
.find("option")
.filterWhere((x) => pattern.test(x.text()));
expect(targetOption).toHaveLength(1);
container
.find("select")
.simulate("change", {target: {value: targetOption.prop("value")}});
}
it("renders some test data in the default state", () => {
const result = render();
expect(enzymeToJSON(result)).toMatchSnapshot();
});
it("updates the node table when a filter is selected", () => {
const result = render();
simulateSelect(result, /small/);
expect(enzymeToJSON(result)).toMatchSnapshot();
});
it("resets the node table when a filter is deselected", () => {
const result = render();
const originalHtml = result.html();
simulateSelect(result, /big/);
const intermediateHtml = result.html();
simulateSelect(result, /Show all/);
const finalHtml = result.html();
expect(finalHtml).toEqual(originalHtml);
expect(finalHtml).not.toEqual(intermediateHtml);
});
});

View File

@ -1,37 +0,0 @@
// @flow
import React from "react";
import type {Graph} from "../../../core/graph";
import type {Settings} from "./SettingsConfig";
import fetchGithubRepo from "../../github/fetchGithubRepo";
import {parse} from "../../github/parser";
type Props = {
settings: Settings,
onCreateGraph: (graph: Graph) => void,
};
export class GithubGraphFetcher extends React.Component<Props> {
render() {
const {settings} = this.props;
const haveSettings =
!!settings.githubApiToken && !!settings.repoOwner && !!settings.repoName;
return (
<button onClick={() => this.fetchGraph()} disabled={!haveSettings}>
Fetch GitHub graph
</button>
);
}
fetchGraph() {
const {repoOwner, repoName, githubApiToken} = this.props.settings;
fetchGithubRepo(repoOwner, repoName, githubApiToken)
.then((json) => {
return Promise.resolve(parse(json));
})
.then((graph) => {
this.props.onCreateGraph(graph);
});
}
}

View File

@ -1,5 +0,0 @@
// @flow
import LocalStore from "../../../app/LocalStore";
export default new LocalStore({version: "1", keyPrefix: "artifact-editor"});

View File

@ -1,89 +0,0 @@
// @flow
import React from "react";
import LocalStore from "./LocalStore";
export type Settings = {
githubApiToken: string,
repoOwner: string,
repoName: string,
};
type Props = {
onChange: (Settings) => void,
};
type State = Settings;
const LOCAL_STORE_SETTINGS_KEY = "SettingsConfig.settings";
export function defaultSettings() {
return {
githubApiToken: "",
repoOwner: "",
repoName: "",
};
}
export class SettingsConfig extends React.Component<Props, State> {
constructor() {
super();
this.state = defaultSettings();
}
componentDidMount() {
this.setState(LocalStore.get(LOCAL_STORE_SETTINGS_KEY, this.state), () => {
this.props.onChange(this.state);
});
}
render() {
return (
<div>
<label>
API token{" "}
<input
value={this.state.githubApiToken}
onChange={(e) => {
const value = e.target.value;
this.setState(
{githubApiToken: value},
this._updateSettings.bind(this)
);
}}
/>
</label>
<br />
<label>
Repository owner{" "}
<input
value={this.state.repoOwner}
onChange={(e) => {
const value = e.target.value;
this.setState(
{repoOwner: value},
this._updateSettings.bind(this)
);
}}
/>
</label>
<br />
<label>
Repository name{" "}
<input
value={this.state.repoName}
onChange={(e) => {
const value = e.target.value;
this.setState({repoName: value}, this._updateSettings.bind(this));
}}
/>
</label>
</div>
);
}
_updateSettings() {
LocalStore.set(LOCAL_STORE_SETTINGS_KEY, this.state);
this.props.onChange(this.state);
}
}

View File

@ -1,251 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ContributionList renders some test data in the default state 1`] = `
<div>
<h2>
Contributions
</h2>
<label>
Filter by contribution type:
<select
onChange={[Function]}
value="null"
>
<option
value="null"
>
Show all
</option>
<option
disabled={true}
key="sourcecred/example-plugin-a"
style={
Object {
"fontWeight": "bold",
}
}
>
sourcecred/example-plugin-a
</option>
<option
key="big"
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"big\\"}"
>
big
</option>
<option
key="small"
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"small\\"}"
>
small
</option>
<option
disabled={true}
key="sourcecred/example-plugin-b"
style={
Object {
"fontWeight": "bold",
}
}
>
sourcecred/example-plugin-b
</option>
<option
key="very true"
value="{\\"pluginName\\":\\"sourcecred/example-plugin-b\\",\\"type\\":\\"very true\\"}"
>
very true
</option>
</select>
</label>
<table>
<thead>
<tr>
<th>
Title
</th>
<th>
Artifact
</th>
<th>
Weight
</th>
</tr>
</thead>
<tbody>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"one\\",\\"type\\":\\"small\\"}"
>
<td>
the number 111
</td>
<td>
[TODO]
</td>
<td>
[TODO]
</td>
</tr>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"two\\",\\"type\\":\\"small\\"}"
>
<td>
the number 234
</td>
<td>
[TODO]
</td>
<td>
[TODO]
</td>
</tr>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"three\\",\\"type\\":\\"big\\"}"
>
<td>
the number 616
</td>
<td>
[TODO]
</td>
<td>
[TODO]
</td>
</tr>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-b\\",\\"id\\":\\"four\\",\\"type\\":\\"very true\\"}"
>
<td>
TRUE!
</td>
<td>
[TODO]
</td>
<td>
[TODO]
</td>
</tr>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-c\\",\\"id\\":\\"five\\",\\"type\\":\\"ctype\\"}"
>
<td
colSpan={3}
>
<i>
unknown
</i>
(plugin:
sourcecred/example-plugin-c
)
</td>
</tr>
</tbody>
</table>
</div>
`;
exports[`ContributionList updates the node table when a filter is selected 1`] = `
<div>
<h2>
Contributions
</h2>
<label>
Filter by contribution type:
<select
onChange={[Function]}
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"small\\"}"
>
<option
value="null"
>
Show all
</option>
<option
disabled={true}
key="sourcecred/example-plugin-a"
style={
Object {
"fontWeight": "bold",
}
}
>
sourcecred/example-plugin-a
</option>
<option
key="big"
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"big\\"}"
>
big
</option>
<option
key="small"
value="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"type\\":\\"small\\"}"
>
small
</option>
<option
disabled={true}
key="sourcecred/example-plugin-b"
style={
Object {
"fontWeight": "bold",
}
}
>
sourcecred/example-plugin-b
</option>
<option
key="very true"
value="{\\"pluginName\\":\\"sourcecred/example-plugin-b\\",\\"type\\":\\"very true\\"}"
>
very true
</option>
</select>
</label>
<table>
<thead>
<tr>
<th>
Title
</th>
<th>
Artifact
</th>
<th>
Weight
</th>
</tr>
</thead>
<tbody>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"one\\",\\"type\\":\\"small\\"}"
>
<td>
the number 111
</td>
<td>
[TODO]
</td>
<td>
[TODO]
</td>
</tr>
<tr
key="{\\"pluginName\\":\\"sourcecred/example-plugin-a\\",\\"id\\":\\"two\\",\\"type\\":\\"small\\"}"
>
<td>
the number 234
</td>
<td>
[TODO]
</td>
<td>
[TODO]
</td>
</tr>
</tbody>
</table>
</div>
`;

View File

@ -1,20 +0,0 @@
// @flow
import type {Node} from "../../../core/graph";
import type {PluginAdapter} from "./pluginAdapter";
export class AdapterSet {
adapters: {[pluginName: string]: PluginAdapter<any>};
constructor() {
this.adapters = {};
}
addAdapter(adapter: PluginAdapter<any>): void {
this.adapters[adapter.pluginName] = adapter;
}
getAdapter<NP>(node: Node<NP>): ?PluginAdapter<NP> {
return this.adapters[node.address.pluginName];
}
}

View File

@ -1,420 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`githubPluginAdapter operates on the example repo 1`] = `
Array [
Object {
"id": "https://github.com/decentralion",
"payload": Object {
"login": "decentralion",
"subtype": "USER",
"url": "https://github.com/decentralion",
},
"rendered": <div>
type:
AUTHOR
(details to be implemented)
</div>,
"title": "decentralion",
"type": "AUTHOR",
},
Object {
"id": "https://github.com/sourcecred/example-github",
"payload": Object {
"name": "example-github",
"owner": "sourcecred",
"url": "https://github.com/sourcecred/example-github",
},
"rendered": <div>
type:
REPOSITORY
(details to be implemented)
</div>,
"title": "sourcecred/example-github",
"type": "REPOSITORY",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/1",
"payload": Object {
"body": "This is just an example issue.",
"number": 1,
"title": "An example issue.",
"url": "https://github.com/sourcecred/example-github/issues/1",
},
"rendered": <div>
type:
ISSUE
(details to be implemented)
</div>,
"title": "#1: An example issue.",
"type": "ISSUE",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2",
"payload": Object {
"body": "This issue references another issue, namely #1",
"number": 2,
"title": "A referencing issue.",
"url": "https://github.com/sourcecred/example-github/issues/2",
},
"rendered": <div>
type:
ISSUE
(details to be implemented)
</div>,
"title": "#2: A referencing issue.",
"type": "ISSUE",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768703",
"payload": Object {
"body": "It should also be possible to reference by exact url: https://github.com/sourcecred/example-github/issues/6",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768703",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768850",
"payload": Object {
"body": "We might also reference individual comments directly.
https://github.com/sourcecred/example-github/issues/6#issuecomment-373768538",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-373768850",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576185",
"payload": Object {
"body": "Here's a PR by direct url: https://github.com/sourcecred/example-github/pull/5",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576185",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576220",
"payload": Object {
"body": "a PR review by url: https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576220",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576248",
"payload": Object {
"body": "a PR Review Comment by url: https://github.com/sourcecred/example-github/pull/5#discussion_r171460198",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576248",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576273",
"payload": Object {
"body": "a user by url: https://github.com/wchargin",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576273",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576920",
"payload": Object {
"body": "Here are several references:
#1
#2
#3
https://github.com/sourcecred/example-github/pull/5#discussion_r171460198
https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899
",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576920",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576936",
"payload": Object {
"body": "This comment has no references.",
"url": "https://github.com/sourcecred/example-github/issues/2#issuecomment-385576936",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #2: A referencing issue.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/4",
"payload": Object {
"body": "Alas, its life as an open issue had only just begun.",
"number": 4,
"title": "A closed pull request",
"url": "https://github.com/sourcecred/example-github/issues/4",
},
"rendered": <div>
type:
ISSUE
(details to be implemented)
</div>,
"title": "#4: A closed pull request",
"type": "ISSUE",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/6",
"payload": Object {
"body": "This issue shall shortly have a few comments.",
"number": 6,
"title": "An issue with comments",
"url": "https://github.com/sourcecred/example-github/issues/6",
},
"rendered": <div>
type:
ISSUE
(details to be implemented)
</div>,
"title": "#6: An issue with comments",
"type": "ISSUE",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768442",
"payload": Object {
"body": "A wild COMMENT appeared!",
"url": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768442",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #6: An issue with comments",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768538",
"payload": Object {
"body": "And the maintainer said, \\"Let there be comments!\\"",
"url": "https://github.com/sourcecred/example-github/issues/6#issuecomment-373768538",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #6: An issue with comments",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316",
"payload": Object {
"body": "This comment references an #2, which itself references an issue. This comment is thus allows us to test that in-references are not included when requesting a Post's references.",
"url": "https://github.com/sourcecred/example-github/issues/6#issuecomment-385223316",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #6: An issue with comments",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/7",
"payload": Object {
"body": "Deal with this, naive string display algorithms!!!!!",
"number": 7,
"title": "An issue with an extremely long title, which even has a VerySuperFragicalisticialiManyCharacterUberLongTriplePlusGood word in it, and should really be truncated intelligently or something",
"url": "https://github.com/sourcecred/example-github/issues/7",
},
"rendered": <div>
type:
ISSUE
(details to be implemented)
</div>,
"title": "#7: An issue with an extremely long title, which even has a VerySuperFragicalisticialiManyCharacterUberLongTriplePlusGood word in it, and should really be truncated intelligently or something",
"type": "ISSUE",
},
Object {
"id": "https://github.com/sourcecred/example-github/issues/8",
"payload": Object {
"body": "Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️
Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️",
"number": 8,
"title": "Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️",
"url": "https://github.com/sourcecred/example-github/issues/8",
},
"rendered": <div>
type:
ISSUE
(details to be implemented)
</div>,
"title": "#8: Issue with Unicode: ȴሲ𣐳楢👍 :heart: 𐤔𐤁𐤀𐤑𐤍𐤉𐤔𐤌𐤄𐤍𐤍 ❤️",
"type": "ISSUE",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/3",
"payload": Object {
"body": "Oh look, it's a pull request.",
"number": 3,
"title": "Add README, merge via PR.",
"url": "https://github.com/sourcecred/example-github/pull/3",
},
"rendered": <div>
type:
PULL_REQUEST
(details to be implemented)
</div>,
"title": "#3: Add README, merge via PR.",
"type": "PULL_REQUEST",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222",
"payload": Object {
"body": "It seems apropos to reference something from a pull request comment... eg: #2 ",
"url": "https://github.com/sourcecred/example-github/pull/3#issuecomment-369162222",
},
"rendered": <div>
type:
COMMENT
(details to be implemented)
</div>,
"title": "comment on #3: Add README, merge via PR.",
"type": "COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/5",
"payload": Object {
"body": "@wchargin could you please do the following:
- add a commit comment
- add a review comment requesting some trivial change
- i'll change it
- then approve the pr",
"number": 5,
"title": "This pull request will be more contentious. I can feel it...",
"url": "https://github.com/sourcecred/example-github/pull/5",
},
"rendered": <div>
type:
PULL_REQUEST
(details to be implemented)
</div>,
"title": "#5: This pull request will be more contentious. I can feel it...",
"type": "PULL_REQUEST",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/5#discussion_r171460198",
"payload": Object {
"body": "seems a bit capricious",
"url": "https://github.com/sourcecred/example-github/pull/5#discussion_r171460198",
},
"rendered": <div>
type:
PULL_REQUEST_REVIEW_COMMENT
(details to be implemented)
</div>,
"title": "comment on review of #5: This pull request will be more contentious. I can feel it...",
"type": "PULL_REQUEST_REVIEW_COMMENT",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
"payload": Object {
"body": "hmmm.jpg",
"state": "CHANGES_REQUESTED",
"url": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899",
},
"rendered": <div>
type:
PULL_REQUEST_REVIEW
(details to be implemented)
</div>,
"title": "review of #5: This pull request will be more contentious. I can feel it...",
"type": "PULL_REQUEST_REVIEW",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100314038",
"payload": Object {
"body": "I'm sold",
"state": "APPROVED",
"url": "https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100314038",
},
"rendered": <div>
type:
PULL_REQUEST_REVIEW
(details to be implemented)
</div>,
"title": "review of #5: This pull request will be more contentious. I can feel it...",
"type": "PULL_REQUEST_REVIEW",
},
Object {
"id": "https://github.com/sourcecred/example-github/pull/9",
"payload": Object {
"body": "Nominally paired with @wchargin",
"number": 9,
"title": "An unmerged pull request",
"url": "https://github.com/sourcecred/example-github/pull/9",
},
"rendered": <div>
type:
PULL_REQUEST
(details to be implemented)
</div>,
"title": "#9: An unmerged pull request",
"type": "PULL_REQUEST",
},
Object {
"id": "https://github.com/wchargin",
"payload": Object {
"login": "wchargin",
"subtype": "USER",
"url": "https://github.com/wchargin",
},
"rendered": <div>
type:
AUTHOR
(details to be implemented)
</div>,
"title": "wchargin",
"type": "AUTHOR",
},
]
`;

View File

@ -1,108 +0,0 @@
// @flow
import React from "react";
import {Graph} from "../../../../core/graph";
import type {Node} from "../../../../core/graph";
import type {
NodePayload,
NodeType,
RepositoryNodePayload,
IssueNodePayload,
PullRequestNodePayload,
CommentNodePayload,
PullRequestReviewCommentNodePayload,
PullRequestReviewNodePayload,
AuthorNodePayload,
} from "../../../github/types";
import type {PluginAdapter} from "../pluginAdapter";
import {PLUGIN_NAME} from "../../../github/pluginName";
import {CONTAINS_EDGE_TYPE} from "../../../github/types";
const adapter: PluginAdapter<NodePayload> = {
pluginName: PLUGIN_NAME,
renderer: class GithubNodeRenderer extends React.Component<{
graph: Graph,
node: Node<NodePayload>,
}> {
render() {
const type = this.props.node.address.type;
return <div>type: {type} (details to be implemented)</div>;
}
},
extractTitle(graph: *, node: Node<NodePayload>): string {
// NOTE: If the graph is malformed such that there are containment
// cycles, then this function may blow the stack or fail to
// terminate. (If necessary, we can fix this by tracking all
// previously queried IDs.)
function extractParentTitles(node: Node<NodePayload>): string[] {
return graph
.neighborhood(node.address, {
direction: "IN",
edgeType: CONTAINS_EDGE_TYPE,
})
.map(({neighbor}) => {
return adapter.extractTitle(graph, graph.node(neighbor));
});
}
function extractRepositoryTitle(node: Node<RepositoryNodePayload>) {
return `${node.payload.owner}/${node.payload.name}`;
}
function extractIssueOrPrTitle(
node: Node<IssueNodePayload | PullRequestNodePayload>
) {
return `#${node.payload.number}: ${node.payload.title}`;
}
function extractCommentTitle(
kind: string,
node: Node<CommentNodePayload | PullRequestReviewCommentNodePayload>
) {
const parentTitles = extractParentTitles(node);
if (parentTitles.length === 0) {
// Should never happen.
return "comment (orphaned)";
} else {
// Should just be one parent.
return `comment on ${parentTitles.join(" and ")}`;
}
}
function extractPRReviewTitle(node: Node<PullRequestReviewNodePayload>) {
const parentTitles = extractParentTitles(node);
if (parentTitles.length === 0) {
// Should never happen.
return "pull request review (orphaned)";
} else {
// Should just be one parent.
return `review of ${parentTitles.join(" and ")}`;
}
}
function extractAuthorTitle(node: Node<AuthorNodePayload>) {
return node.payload.login;
}
const anyNode: Node<any> = node;
const type: NodeType = (node.address.type: any);
switch (type) {
case "REPOSITORY":
return extractRepositoryTitle(anyNode);
case "ISSUE":
case "PULL_REQUEST":
return extractIssueOrPrTitle(anyNode);
case "COMMENT":
return extractCommentTitle("comment", anyNode);
case "PULL_REQUEST_REVIEW_COMMENT":
return extractCommentTitle("review comment", anyNode);
case "PULL_REQUEST_REVIEW":
return extractPRReviewTitle(anyNode);
case "AUTHOR":
return extractAuthorTitle(anyNode);
default:
// eslint-disable-next-line no-unused-expressions
(type: empty);
throw new Error(`unknown node type: ${node.address.type}`);
}
},
};
export default adapter;

View File

@ -1,36 +0,0 @@
// @flow
import React from "react";
import {shallow} from "enzyme";
import enzymeToJSON from "enzyme-to-json";
import stringify from "json-stable-stringify";
import {parse} from "../../../github/parser";
import exampleRepoData from "../../../github/demoData/example-github.json";
import adapter from "./githubPluginAdapter";
require("../../../../app/testUtil").configureEnzyme();
describe("githubPluginAdapter", () => {
it("operates on the example repo", () => {
const graph = parse(exampleRepoData);
const result = graph
.nodes()
.map((node) => ({
id: node.address.id,
payload: node.payload,
type: node.address.type,
title: adapter.extractTitle(graph, node),
rendered: enzymeToJSON(
shallow(<adapter.renderer graph={graph} node={node} />)
),
}))
.sort((a, b) => {
const ka = stringify(a.id);
const kb = stringify(b.id);
return ka > kb ? 1 : ka < kb ? -1 : 0;
});
expect(result).toMatchSnapshot();
});
});

View File

@ -1,10 +0,0 @@
// @flow
import type {Graph, Node} from "../../../core/graph";
import type {ComponentType} from "react";
export interface PluginAdapter<-NodePayload> {
pluginName: string;
renderer: $Subtype<ComponentType<{graph: Graph, node: Node<NodePayload>}>>;
extractTitle(graph: Graph, node: Node<NodePayload>): string;
}

View File

@ -1,9 +0,0 @@
// @flow
import {AdapterSet} from "./adapterSet";
import githubPluginAdapter from "./adapters/githubPluginAdapter";
const adapterSet = new AdapterSet();
adapterSet.addAdapter(githubPluginAdapter);
export default adapterSet;