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:
parent
ac46a98a0b
commit
cdceedef8d
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue