提交 b4801513 编写于 作者: J Jason Park

Show contributors

上级 99a6a60d
......@@ -11,7 +11,7 @@ const {
GITHUB_BOT_USERNAME,
GITHUB_BOT_PASSWORD,
GITHUB_ORG = 'algorithm-visualizer',
GITHUB_REPO_ALGORITHMS = 'algorithms',
GITHUB_REPO = 'algorithms',
} = process.env;
const __PROD__ = NODE_ENV === 'production';
......@@ -27,9 +27,7 @@ const githubBotAuth = {
password: GITHUB_BOT_PASSWORD,
};
const githubOrg = GITHUB_ORG;
const githubRepos = {
algorithms: GITHUB_REPO_ALGORITHMS
};
const githubRepo = GITHUB_REPO;
const builtPath = path.resolve(__dirname, 'built');
const frontendBuiltPath = path.resolve(builtPath, 'frontend');
......@@ -50,7 +48,7 @@ module.exports = {
githubClientSecret,
githubBotAuth,
githubOrg,
githubRepos,
githubRepo,
frontendBuiltPath,
backendBuiltPath,
frontendSrcPath,
......
import GitHub from 'github-api';
import { githubBotAuth, githubOrg, githubRepo } from '/environment';
const gh = new GitHub(githubBotAuth);
const repo = gh.getRepo(githubOrg, githubRepo);
export {
repo,
};
\ No newline at end of file
......@@ -2,6 +2,8 @@ import express from 'express';
import fs from 'fs';
import path from 'path';
import { NotFoundError } from '/common/error';
import { exec } from 'child_process';
import { repo } from '/common/github';
const router = express.Router();
......@@ -10,46 +12,98 @@ const createKey = name => name.toLowerCase().replace(/ /g, '-');
const list = dirPath => fs.readdirSync(dirPath).filter(filename => !filename.startsWith('.'));
const cacheHierarchy = () => {
const getCategory = categoryName => {
const allFiles = [];
const cacheCategory = categoryName => {
const categoryKey = createKey(categoryName);
const categoryPath = getPath(categoryName);
const algorithms = list(categoryPath).map(algorithmName => getAlgorithm(categoryName, algorithmName));
const algorithms = list(categoryPath).map(algorithmName => cacheAlgorithm(categoryName, algorithmName));
return {
key: categoryKey,
name: categoryName,
algorithms,
};
};
const getAlgorithm = (categoryName, algorithmName) => {
const cacheAlgorithm = (categoryName, algorithmName) => {
const algorithmKey = createKey(algorithmName);
const algorithmPath = getPath(categoryName, algorithmName);
const files = list(algorithmPath);
const files = list(algorithmPath).map(fileName => cacheFile(categoryName, algorithmName, fileName));
allFiles.push(...files);
return {
key: algorithmKey,
name: algorithmName,
files,
};
};
return list(getPath()).map(getCategory);
};
const cacheFile = (categoryName, algorithmName, fileName) => {
const filePath = getPath(categoryName, algorithmName, fileName);
const content = fs.readFileSync(filePath, 'utf-8');
return {
name: fileName,
path: filePath,
content,
contributors: [],
toJSON: () => fileName,
};
};
const hierarchy = list(getPath()).map(cacheCategory);
const hierarchy = cacheHierarchy();
const commitAuthors = {};
const cacheCommitAuthors = (page, done) => {
repo.listCommits({ per_page: 100, page }).then(({ data }) => {
if (data.length) {
data.forEach(({ sha, commit, author }) => {
if (!author) return;
const { login, avatar_url } = author;
commitAuthors[sha] = { login, avatar_url };
});
cacheCommitAuthors(page + 1, done);
} else done && done();
}).catch(console.error);
};
const cacheContributors = (fileIndex, done) => {
const file = allFiles[fileIndex];
if (file) {
const cwd = getPath();
exec(`git --no-pager log --follow --format="%H" "${file.path}"`, { cwd }, (error, stdout, stderr) => {
if (!error && !stderr) {
const output = stdout.toString().replace(/\n$/, '');
const shas = output.split('\n').reverse();
const contributors = [];
for (const sha of shas) {
const author = commitAuthors[sha];
if (author && !contributors.find(contributor => contributor.login === author.login)) {
contributors.push(author);
}
}
file.contributors = contributors;
}
cacheContributors(fileIndex + 1, done);
});
} else done && done();
};
cacheCommitAuthors(1, () => cacheContributors(0));
return hierarchy;
};
const cachedHierarchy = cacheHierarchy(); // TODO: cache again when webhooked
const getHierarchy = (req, res, next) => {
res.json({ hierarchy });
res.json({ hierarchy: cachedHierarchy });
};
const getFile = (req, res, next) => {
const { categoryKey, algorithmKey, fileName } = req.params;
const category = hierarchy.find(category => category.key === categoryKey);
const category = cachedHierarchy.find(category => category.key === categoryKey);
if (!category) return next(new NotFoundError());
const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
if (!algorithm) return next(new NotFoundError());
if (!algorithm.files.includes(fileName)) return next(new NotFoundError());
const file = algorithm.files.find(file => file.name === fileName);
if (!file) return next(new NotFoundError());
const filePath = getPath(category.name, algorithm.name, fileName);
res.sendFile(filePath);
const { content, contributors } = file;
res.json({ file: { content, contributors } });
};
router.route('/')
......
Subproject commit 417e8c9ffe9bf8411f0099ea01098c0919468a07
Subproject commit f17c57048f65f287d12e13e6f35b606034aeeb31
......@@ -6,7 +6,7 @@ import { Workspace, WSSectionContainer, WSTabContainer } from '/workspace/compon
import { Section } from '/workspace/core';
import { actions as toastActions } from '/reducers/toast';
import { actions as envActions } from '/reducers/env';
import { HierarchyApi, GitHubApi } from '/apis';
import { GitHubApi, HierarchyApi } from '/apis';
import { tracerManager } from '/core';
import styles from './stylesheet.scss';
import 'axios-progress-bar/dist/nprogress.css'
......@@ -63,7 +63,6 @@ class App extends React.Component {
updateDirectory({ categoryKey = null, algorithmKey = null }) {
if (categoryKey && algorithmKey) {
this.props.setDirectory(categoryKey, algorithmKey);
HierarchyApi.getFile(categoryKey, algorithmKey, 'code.js').then(code => tracerManager.setCode(code));
}
}
......
......@@ -28,7 +28,11 @@ class Button extends React.Component {
return to ? (
<Link to={to} {...props} />
) : href ? (
<a href={href} {...props} />
/^https?:\/\//i.test(href) ? (
<a href={href} rel="noopener" target="_blank" {...props} />
) : (
<a href={href} {...props} />
)
) : (
<div {...props} />
);
......
import React from 'react';
import AceEditor from 'react-ace';
import { connect } from 'react-redux';
import 'brace/mode/javascript';
import 'brace/theme/tomorrow_night_eighties';
import { tracerManager } from '/core';
import { classes } from '/common/util';
import styles from './stylesheet.scss';
import { HierarchyApi } from '/apis';
import { ContributorsViewer } from '/components';
import { actions as envActions } from '/reducers/env';
// TODO: code should not be reloaded when reopening tab
@connect(
({ env }) => ({
env
}), {
...envActions
}
)
class CodeEditor extends React.Component {
constructor(props) {
super(props);
const { lineIndicator, code } = tracerManager;
const { lineIndicator } = tracerManager;
this.state = {
lineMarker: this.createLineMarker(lineIndicator),
code,
file: null,
};
}
componentDidMount() {
tracerManager.setOnUpdateCode(code => this.setState({ code }));
const { categoryKey, algorithmKey } = this.props.env;
this.loadFile(categoryKey, algorithmKey);
tracerManager.setOnUpdateLineIndicator(lineIndicator => this.setState({ lineMarker: this.createLineMarker(lineIndicator) }));
}
componentWillReceiveProps(nextProps) {
const { categoryKey, algorithmKey } = nextProps.env;
if (categoryKey !== this.props.env.categoryKey ||
algorithmKey !== this.props.env.algorithmKey) {
this.loadFile(categoryKey, algorithmKey);
}
}
componentWillUnmount() {
tracerManager.setOnUpdateCode(null);
tracerManager.setOnUpdateLineIndicator(null);
}
loadFile(categoryKey, algorithmKey) {
HierarchyApi.getFile(categoryKey, algorithmKey, 'code.js')
.then(({ file }) => {
this.setState({ file });
tracerManager.setCode(file.content);
})
.catch(() => this.setState({ file: null }));
}
createLineMarker(lineIndicator) {
if (lineIndicator === null) return null;
const { lineNumber, cursor } = lineIndicator;
......@@ -42,21 +72,30 @@ class CodeEditor extends React.Component {
};
}
handleChangeCode(code) {
const file = { ...this.state.file, content: code };
this.setState({ file });
tracerManager.setCode(code);
}
render() {
const { lineMarker, code } = this.state;
const { lineMarker, file } = this.state;
const { className, relativeWeight } = this.props;
return (
<AceEditor
className={classes(styles.code_editor, className)}
mode="javascript"
theme="tomorrow_night_eighties"
name="code_editor"
editorProps={{ $blockScrolling: true }}
onChange={code => tracerManager.setCode(code)}
markers={lineMarker ? [lineMarker] : []}
value={code}
width={`${relativeWeight}`} /> // trick to update on resize
return file && (
<div className={classes(styles.code_editor, className)}>
<AceEditor
className={styles.ace_editor}
mode="javascript"
theme="tomorrow_night_eighties"
name="code_editor"
editorProps={{ $blockScrolling: true }}
onChange={code => this.handleChangeCode(code)}
markers={lineMarker ? [lineMarker] : []}
value={file.content}
width={`${relativeWeight}`} />
<ContributorsViewer contributors={file.contributors} />
</div> // TODO: trick to update on resize
);
}
}
......
@import "~/common/stylesheet/index";
.code_editor {
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
.current_line_marker {
background: rgba($color-highlight, 0.4);
border: 1px solid $color-highlight;
position: absolute;
.ace_editor {
flex: 1;
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
animation: line_highlight .1s;
}
.current_line_marker {
background: rgba($color-highlight, 0.4);
border: 1px solid $color-highlight;
position: absolute;
width: 100% !important;
@keyframes line_highlight {
from {
background: rgba($color-highlight, 0.1);
animation: line_highlight .1s;
}
to {
background: rgba($color-highlight, 0.4);
@keyframes line_highlight {
from {
background: rgba($color-highlight, 0.1);
}
to {
background: rgba($color-highlight, 0.4);
}
}
}
}
\ No newline at end of file
import React from 'react';
import { classes } from '/common/util';
import styles from './stylesheet.scss';
import { Button } from '/components';
class ContributorsViewer extends React.Component {
render() {
const { className, contributors } = this.props;
return (
<div className={classes(styles.contributors_viewer, className)}>
<span className={classes(styles.contributor, styles.label)}>Contributed by</span>
{
contributors.map(contributor => (
<Button className={styles.contributor} icon={contributor.avatar_url} key={contributor.login}
href={`https://github.com/${contributor.login}`}>
{contributor.login}
</Button>
))
}
</div>
);
}
}
export default ContributorsViewer;
@import "~/common/stylesheet/index";
.contributors_viewer {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 4px;
background-color: $theme-normal;
.contributor {
height: 28px;
padding: 0 6px;
font-weight: bold;
&.label {
display: flex;
align-items: center;
white-space: nowrap;
}
}
}
\ No newline at end of file
......@@ -2,7 +2,9 @@ import React from 'react';
import { connect } from 'react-redux';
import { actions as envActions } from '/reducers/env';
import { HierarchyApi } from '/apis/index';
import { MarkdownViewer } from '/components';
import { ContributorsViewer, MarkdownViewer } from '/components';
import styles from './stylesheet.scss';
import { classes } from '/common/util';
@connect(
({ env }) => ({
......@@ -16,14 +18,14 @@ class DescriptionViewer extends React.Component {
super(props);
this.state = {
source: null,
file: null,
};
}
componentDidMount() {
const { categoryKey, algorithmKey } = this.props.env;
const href = `/algorithm/${categoryKey}/${algorithmKey}`;
this.loadMarkdown(href);
this.loadFile(href);
}
componentWillReceiveProps(nextProps) {
......@@ -31,21 +33,25 @@ class DescriptionViewer extends React.Component {
if (categoryKey !== this.props.env.categoryKey ||
algorithmKey !== this.props.env.algorithmKey) {
const href = `/algorithm/${categoryKey}/${algorithmKey}`;
this.loadMarkdown(href);
this.loadFile(href);
}
}
loadMarkdown(href) {
loadFile(href) {
const [, , categoryKey, algorithmKey] = href.split('/');
HierarchyApi.getFile(categoryKey, algorithmKey, 'desc.md')
.then(source => this.setState({ source }))
.catch(() => this.setState({ source: null }));
.then(({ file }) => this.setState({ file }))
.catch(() => this.setState({ file: null }));
}
render() {
const { source } = this.state;
return (
<MarkdownViewer source={source} onClickLink={href => this.loadMarkdown(href)} />
const { className } = this.props;
const { file } = this.state;
return file && (
<div className={classes(styles.description_viewer, className)}>
<MarkdownViewer className={styles.markdown_viewer} source={file.content} onClickLink={href => this.loadFile(href)} />
<ContributorsViewer contributors={file.contributors} />
</div>
);
}
}
......
@import "~/common/stylesheet/index";
.description_viewer {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
padding: 16px;
font-size: $font-size-large;
overflow-y: auto;
li {
margin: 10px 0px;
}
.complexity_type {
font-weight: bold;
}
a {
text-decoration: underline;
cursor: pointer;
}
h3 {
border-bottom: 1px solid rgb(81, 81, 81);
padding: 5px;
margin: 2px;
.markdown_viewer {
flex: 1;
}
}
\ No newline at end of file
......@@ -8,7 +8,7 @@ class MarkdownViewer extends React.Component {
const { className, source, onClickLink } = this.props;
const link = ({ href, ...rest }) => {
return /^https?:\/\//.test(href) ? (
return /^https?:\/\//i.test(href) ? (
<a href={href} rel="noopener" target="_blank" {...rest} />
) : (
<a onClick={() => onClickLink(href)} {...rest} />
......
export { default as App } from './App';
export { default as Button } from './Button';
export { default as CodeEditor } from './CodeEditor';
export { default as ContributorsViewer } from './ContributorsViewer';
export { default as DescriptionViewer } from './DescriptionViewer';
export { default as Ellipsis } from './Ellipsis';
export { default as ExpandableListItem } from './ExpandableListItem';
......
......@@ -46,11 +46,6 @@ class TracerManager {
this.onError = onError;
}
setOnUpdateCode(onUpdateCode) {
this.onUpdateCode = onUpdateCode;
if (this.onUpdateCode) this.onUpdateCode(this.code);
}
render() {
Object.values(this.datas).forEach(data => data.render());
if (this.onRender) this.onRender(this.renderers);
......@@ -79,7 +74,6 @@ class TracerManager {
setCode(code) {
this.code = code;
this.runInitial();
if (this.onUpdateCode) this.onUpdateCode(code);
}
reset(seed = new Seed()) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册