Display urls in the cred explorer (#860)

This commit modifies the plugin adapter's `nodeDescription` method so
that it may return a React node.

This enables the GitHub plugin's `nodeDescription` method to include
hyperlinks directly to the referenced content on GitHub. This makes
examining e.g. comment cred much easier.

I've also made two other changes to the descriptions:
- Pull requests diffs now color-encode the additions and deletions
- Descriptions for comments and reviews no longer include the authors

The Git plugin's behavior is unchanged.

Test plan:
I loaded a large repository in the cred explorer and verified that
exploring comments and pulls and issues is much easier. The descriptions
are as expected for every category of node. Snapshot tests updated.

Fixes #590.
This commit is contained in:
Dandelion Mané 2018-09-20 10:48:05 -07:00 committed by GitHub
parent ac46a98a0b
commit cdceedef8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 264 additions and 43 deletions

View File

@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
- Hyperlink to GitHub entities (#860)
- Add GitHub reactions to the graph (#846)
- Detect references to commits (#833)
- Detect references in commit messages (#829)

View File

@ -1,5 +1,6 @@
// @flow
import {type Node as ReactNode} from "react";
import {Graph, type NodeAddressT, type EdgeAddressT} from "../../core/graph";
import type {Assets} from "../assets";
import type {Repo} from "../../core/repo";
@ -30,6 +31,6 @@ export interface StaticPluginAdapter {
export interface DynamicPluginAdapter {
graph(): Graph;
nodeDescription(NodeAddressT): string;
nodeDescription(NodeAddressT): ReactNode;
static (): StaticPluginAdapter;
}

View File

@ -14,7 +14,7 @@ import type {PagerankNodeDecomposition} from "../../../core/attribution/pagerank
export function nodeDescription(
address: NodeAddressT,
adapters: DynamicAdapterSet
): string {
): ReactNode {
const adapter = adapters.adapterMatchingNode(address);
try {
return adapter.nodeDescription(address);

View File

@ -1,13 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`plugins/github/render descriptions are as expected 1`] = `
Object {
"comment": "comment by @wchargin on review by @wchargin of #5 (+1/0): This pull request will be more contentious. I can feel it...",
"commit": "commit 0a223346b4e6dec0127b1e6aa892c4ee0424b66a",
"issue": "#2: A referencing issue.",
"pull": "#5 (+1/0): This pull request will be more contentious. I can feel it...",
"repo": "sourcecred/example-github",
"review": "review by @wchargin of #5 (+1/0): This pull request will be more contentious. I can feel it...",
"userlike": "@wchargin",
}
exports[`plugins/github/render renders the right description for a comment 1`] = `
<span>
<a
href="https://github.com/sourcecred/example-github/pull/5#discussion_r171460198"
rel="nofollow noopener"
target="_blank"
>
comment
</a>
on
<span>
<a
href="https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899"
rel="nofollow noopener"
target="_blank"
>
review
</a>
on
<span>
<a
href="https://github.com/sourcecred/example-github/pull/5"
rel="nofollow noopener"
target="_blank"
>
#5
</a>
<span>
(
<span
style="color:green"
>
+1
</span>
/
<span
style="color:red"
>
0
</span>
)
</span>
: This pull request will be more contentious. I can feel it...
</span>
</span>
</span>
`;
exports[`plugins/github/render renders the right description for a commit 1`] = `
<span>
Commit
<a
href="https://github.com/sourcecred/example-github/commit/0a223346b4e6dec0127b1e6aa892c4ee0424b66a"
rel="nofollow noopener"
target="_blank"
>
0a22334
</a>
</span>
`;
exports[`plugins/github/render renders the right description for a issue 1`] = `
<span>
<a
href="https://github.com/sourcecred/example-github/issues/2"
rel="nofollow noopener"
target="_blank"
>
#2
</a>
: A referencing issue.
</span>
`;
exports[`plugins/github/render renders the right description for a pull 1`] = `
<span>
<a
href="https://github.com/sourcecred/example-github/pull/5"
rel="nofollow noopener"
target="_blank"
>
#5
</a>
<span>
(
<span
style="color:green"
>
+1
</span>
/
<span
style="color:red"
>
0
</span>
)
</span>
: This pull request will be more contentious. I can feel it...
</span>
`;
exports[`plugins/github/render renders the right description for a repo 1`] = `
<a
href="https://github.com/sourcecred/example-github"
rel="nofollow noopener"
target="_blank"
>
sourcecred/example-github
</a>
`;
exports[`plugins/github/render renders the right description for a review 1`] = `
<span>
<a
href="https://github.com/sourcecred/example-github/pull/5#pullrequestreview-100313899"
rel="nofollow noopener"
target="_blank"
>
review
</a>
on
<span>
<a
href="https://github.com/sourcecred/example-github/pull/5"
rel="nofollow noopener"
target="_blank"
>
#5
</a>
<span>
(
<span
style="color:green"
>
+1
</span>
/
<span
style="color:red"
>
0
</span>
)
</span>
: This pull request will be more contentious. I can feel it...
</span>
</span>
`;
exports[`plugins/github/render renders the right description for a userlike 1`] = `
<a
href="https://github.com/wchargin"
rel="nofollow noopener"
target="_blank"
>
@wchargin
</a>
`;

View File

@ -1,31 +1,96 @@
// @flow
import React, {type Node as ReactNode} from "react";
import * as R from "./relationalView";
export function description(e: R.Entity) {
const withAuthors = (x: R.AuthoredEntity) => {
const authors = Array.from(x.authors());
if (authors.length === 0) {
// ghost author - probably a deleted account
return "";
}
return "by " + authors.map((x) => description(x)).join(" & ") + " ";
};
function EntityUrl(props) {
return (
<a href={props.entity.url()} target="_blank" rel="nofollow noopener">
{props.children}
</a>
);
}
function repo(x: R.Repo) {
return (
<EntityUrl entity={x}>
{x.owner()}/{x.name()}
</EntityUrl>
);
}
function hyperlinkedNumber(x: R.Issue | R.Pull) {
return <EntityUrl entity={x}>#{x.number()}</EntityUrl>;
}
function issue(x: R.Issue) {
return (
<span>
{hyperlinkedNumber(x)}: {x.title()}
</span>
);
}
function pull(x: R.Pull) {
const additions = <span style={{color: "green"}}>+{x.additions()}</span>;
const deletions = <span style={{color: "red"}}>{x.deletions()}</span>;
const diff = (
<span>
({additions}/{deletions})
</span>
);
return (
<span>
{hyperlinkedNumber(x)} {diff}: {x.title()}
</span>
);
}
function review(x: R.Review) {
const leader = <EntityUrl entity={x}>review</EntityUrl>;
return (
<span>
{leader} on {description(x.parent())}
</span>
);
}
function comment(x: R.Comment) {
const leader = <EntityUrl entity={x}>comment</EntityUrl>;
return (
<span>
{leader} on {description(x.parent())}
</span>
);
}
function userlike(x: R.Userlike) {
return <EntityUrl entity={x}>@{x.login()}</EntityUrl>;
}
// The commit type is included for completeness's sake and to
// satisfy the typechecker, but won't ever be seen in the frontend
// because the commit has a Git plugin prefix and will therefore by
// handled by the git plugin adapter
function commit(x: R.Commit) {
// TODO(@wchargin): Ensure this hash is unambiguous
const shortHash = x.address().hash.slice(0, 7);
return (
<span>
Commit <EntityUrl entity={x}>{shortHash}</EntityUrl>
</span>
);
}
export function description(e: R.Entity): ReactNode {
const handlers = {
repo: (x) => `${x.owner()}/${x.name()}`,
issue: (x) => `#${x.number()}: ${x.title()}`,
pull: (x) => {
const diff = `+${x.additions()}/\u2212${x.deletions()}`;
return `#${x.number()} (${diff}): ${x.title()}`;
},
review: (x) => `review ${withAuthors(x)}of ${description(x.parent())}`,
comment: (x) => `comment ${withAuthors(x)}on ${description(x.parent())}`,
// The commit type is included for completeness's sake and to
// satisfy the typechecker, but won't ever be seen in the frontend
// because the commit has a Git plugin prefix and will therefore by
// handled by the git plugin adapter
commit: (x) => `commit ${x.address().hash}`,
userlike: (x) => `@${x.login()}`,
repo,
issue,
pull,
review,
comment,
commit,
userlike,
};
return R.match(handlers, e);
}

View File

@ -1,16 +1,19 @@
// @flow
import {render} from "enzyme";
import {exampleEntities} from "./example/example";
import {description} from "./render";
import enzymeToJSON from "enzyme-to-json";
require("../../app/testUtil").configureEnzyme();
describe("plugins/github/render", () => {
it("descriptions are as expected", () => {
const examples = exampleEntities();
const withDescriptions = {};
for (const name of Object.keys(exampleEntities())) {
for (const name of Object.keys(examples)) {
it(`renders the right description for a ${name}`, () => {
const entity = examples[name];
withDescriptions[name] = description(entity);
}
expect(withDescriptions).toMatchSnapshot();
const renderedEntity = render(description(entity));
expect(enzymeToJSON(renderedEntity)).toMatchSnapshot();
});
}
});