提交 0be0e02d 编写于 作者: J Jason Park

Improve visualization performance and revise toolbar buttons

上级 b422d707
......@@ -70,6 +70,15 @@ const GitHubApi = {
let jsWorker = null;
const TracerApi = {
md: ({ code }) => Promise.resolve([{
tracerKey: '0-MarkdownTracer-Markdown',
method: 'construct',
args: ['MarkdownTracer', 'Markdown'],
}, {
tracerKey: '0-MarkdownTracer-Markdown',
method: 'set',
args: [code],
}]),
js: ({ code }) => new Promise((resolve, reject) => {
if (jsWorker) jsWorker.terminate();
jsWorker = new Worker('/api/tracers/js');
......@@ -85,4 +94,4 @@ export {
CategoryApi,
GitHubApi,
TracerApi,
};
\ No newline at end of file
};
......@@ -8,6 +8,7 @@ $color-alert: #f3bd58;
$color-selected: #2962ff;
$color-patched: #c51162;
$color-highlight: #29d;
$color-active: #00e676;
:export {
themeDark: $theme-dark;
......@@ -20,4 +21,5 @@ $color-highlight: #29d;
colorSelected: $color-selected;
colorPatched: $color-patched;
colorHighlight: $color-highlight;
}
\ No newline at end of file
colorActive: $color-active;
}
......@@ -11,7 +11,6 @@ import 'axios-progress-bar/dist/nprogress.css';
import {
CodeEditor,
Header,
MarkdownViewer,
Navigator,
ResizableContainer,
TabContainer,
......@@ -19,10 +18,9 @@ import {
VisualizationViewer,
} from '/components';
import { CategoryApi, GitHubApi } from '/apis';
import { tracerManager } from '/core';
import { actions } from '/reducers';
import { extension, refineGist } from '/common/util';
import { languages, exts, us } from '/common/config';
import { exts, languages, us } from '/common/config';
import { README_MD, SCRATCH_PAPER_MD } from '/skeletons';
import styles from './stylesheet.scss';
......@@ -36,7 +34,6 @@ class App extends React.Component {
this.state = {
navigatorOpened: true,
workspaceWeights: [1, 2, 2],
viewerTabIndex: 0,
editorTabIndex: -1,
};
}
......@@ -53,15 +50,11 @@ class App extends React.Component {
CategoryApi.getCategories()
.then(({ categories }) => this.props.setCategories(categories))
.catch(this.props.showErrorToast);
tracerManager.setOnError(error => this.props.showErrorToast({ name: error.name, message: error.message }));
}
componentWillUnmount() {
delete window.signIn;
delete window.signOut;
tracerManager.setOnError(null);
}
componentWillReceiveProps(nextProps) {
......@@ -172,10 +165,6 @@ class App extends React.Component {
this.setState({ workspaceWeights });
}
handleChangeViewerTabIndex(viewerTabIndex) {
this.setState({ viewerTabIndex });
}
handleChangeEditorTabIndex(editorTabIndex) {
const { files } = this.props.current;
if (editorTabIndex === files.length) this.handleAddFile();
......@@ -203,8 +192,11 @@ class App extends React.Component {
handleDeleteFile(file) {
const { files } = this.props.current;
const { editorTabIndex } = this.state;
if (files.indexOf(file) < editorTabIndex) this.handleChangeEditorTabIndex(editorTabIndex - 1);
else this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2));
if (files.indexOf(file) < editorTabIndex) {
this.handleChangeEditorTabIndex(editorTabIndex - 1);
} else {
this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2));
}
this.props.deleteFile(file);
}
......@@ -220,19 +212,21 @@ class App extends React.Component {
serializeFiles(files) === serializeFiles(lastFiles);
}
getDescription() {
const { files } = this.props.current;
const readmeFile = files.find(file => file.name === 'README.md');
if (!readmeFile) return '';
const groups = /^\s*# .*\n+([^\n]+)/.exec(readmeFile.content);
return groups && groups[1] || '';
}
render() {
const { navigatorOpened, workspaceWeights, viewerTabIndex, editorTabIndex } = this.state;
const { navigatorOpened, workspaceWeights, editorTabIndex } = this.state;
const { titles, files } = this.props.current;
const gistSaved = this.isGistSaved();
const readmeFile = files.find(file => file.name === 'README.md') || {
name: 'README.md',
content: `# ${titles[1]}\nREADME.md not found`,
contributors: [us],
};
const groups = /^\s*# .*\n+([^\n]+)/.exec(readmeFile.content);
const description = groups && groups[1] || '';
const description = this.getDescription();
const editorTitles = files.map(file => file.name);
if (files[editorTabIndex]) {
......@@ -253,17 +247,13 @@ class App extends React.Component {
</Helmet>
<Header className={styles.header} onClickTitleBar={() => this.toggleNavigatorOpened()}
navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()}
loadAlgorithm={params => this.loadAlgorithm(params)}
onAction={() => this.handleChangeViewerTabIndex(1)} gistSaved={gistSaved} />
loadAlgorithm={params => this.loadAlgorithm(params)} gistSaved={gistSaved}
file={files[editorTabIndex]} />
<ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
visibles={[navigatorOpened, true, true]}
onChangeWeights={weights => this.handleChangeWorkspaceWeights(weights)}>
<Navigator loadAlgorithm={params => this.loadAlgorithm(params)} />
<TabContainer titles={['Description', 'Visualization']} tabIndex={viewerTabIndex}
onChangeTabIndex={tabIndex => this.handleChangeViewerTabIndex(tabIndex)}>
<MarkdownViewer source={readmeFile.content} />
<VisualizationViewer />
</TabContainer>
<VisualizationViewer className={styles.visualization_viewer} />
<TabContainer className={styles.editor_tab_container} titles={editorTitles} tabIndex={editorTabIndex}
onChangeTabIndex={tabIndex => this.handleChangeEditorTabIndex(tabIndex)}>
{
......
......@@ -55,6 +55,10 @@ button {
.workspace {
flex: 1;
.visualization_viewer {
background-color: $theme-dark;
}
.editor_tab_container {
.input_title {
input {
......@@ -75,4 +79,4 @@ button {
right: 0;
z-index: 99;
}
}
\ No newline at end of file
}
......@@ -2,6 +2,7 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faExclamationCircle from '@fortawesome/fontawesome-free-solid/faExclamationCircle';
import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner';
import { classes } from '/common/util';
import { Ellipsis } from '/components';
import styles from './stylesheet.scss';
......@@ -22,7 +23,7 @@ class Button extends React.Component {
}
render() {
let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, ...rest } = this.props;
let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, inProgress, ...rest } = this.props;
const { confirming } = this.state;
if (confirmNeeded) {
......@@ -54,7 +55,8 @@ class Button extends React.Component {
typeof icon === 'string' ?
<div className={classes(styles.icon, styles.image)} key="icon"
style={{ backgroundImage: `url(${icon})` }} /> :
<FontAwesomeIcon className={styles.icon} fixedWidth icon={icon} key="icon" />
<FontAwesomeIcon className={styles.icon} fixedWidth icon={inProgress ? faSpinner : icon} spin={inProgress}
key="icon" />
),
children,
],
......
......@@ -54,7 +54,7 @@
font-weight: bold;
.icon {
color: #00e676;
color: $color-active;
}
}
}
......@@ -76,4 +76,4 @@
&.confirming {
color: $color-alert;
}
}
\ No newline at end of file
}
......@@ -10,7 +10,6 @@ import 'brace/theme/tomorrow_night_eighties';
import 'brace/ext/searchbox';
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
import faUser from '@fortawesome/fontawesome-free-solid/faUser';
import { tracerManager } from '/core';
import { classes, extension } from '/common/util';
import { actions } from '/reducers';
import { connect } from 'react-redux';
......@@ -18,58 +17,22 @@ import { languages } from '/common/config';
import { Button, Ellipsis } from '/components';
import styles from './stylesheet.scss';
@connect(({ current, env }) => ({ current, env }), actions)
@connect(({ current, env, player }) => ({ current, env, player }), actions)
class CodeEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
lineMarker: null,
};
}
componentDidMount() {
const { file } = this.props;
tracerManager.setFile(file, true);
tracerManager.setOnUpdateLineIndicator(lineIndicator => this.setState({ lineMarker: this.createLineMarker(lineIndicator) }));
}
componentWillReceiveProps(nextProps) {
const { file } = nextProps;
if (file !== this.props.file) {
tracerManager.setFile(file, extension(file.name) === 'js');
}
}
componentWillUnmount() {
tracerManager.setOnUpdateLineIndicator(null);
}
createLineMarker(lineIndicator) {
if (lineIndicator === null) return null;
const { lineNumber, cursor } = lineIndicator;
return {
startRow: lineNumber,
startCol: 0,
endRow: lineNumber,
endCol: Infinity,
className: styles.current_line_marker,
type: 'line',
inFront: true,
_key: cursor,
};
this.props.shouldBuild();
}
handleChangeCode(code) {
const { file } = this.props;
this.props.modifyFile({ ...file, content: code });
if (extension(file.name) === 'md') this.props.shouldBuild();
}
render() {
const { className, file, onDeleteFile } = this.props;
const { user } = this.props.env;
const { lineMarker } = this.state;
const { lineIndicator } = this.props.player;
const fileExt = extension(file.name);
const language = languages.find(language => language.ext === fileExt);
......@@ -84,7 +47,16 @@ class CodeEditor extends React.Component {
name="code_editor"
editorProps={{ $blockScrolling: true }}
onChange={code => this.handleChangeCode(code)}
markers={lineMarker ? [lineMarker] : []}
markers={lineIndicator ? [{
startRow: lineIndicator.lineNumber,
startCol: 0,
endRow: lineIndicator.lineNumber,
endCol: Infinity,
className: styles.current_line_marker,
type: 'line',
inFront: true,
_key: lineIndicator.cursor,
}] : []}
value={file.content} />
<div className={classes(styles.contributors_viewer, className)}>
<span className={classes(styles.contributor, styles.label)}>Contributed by</span>
......
import React from 'react';
import { connect } from 'react-redux';
import InputRange from 'react-input-range';
import AutosizeInput from 'react-input-autosize';
import screenfull from 'screenfull';
import Promise from 'bluebird';
......@@ -8,9 +7,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight';
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown';
import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight';
import faPlay from '@fortawesome/fontawesome-free-solid/faPlay';
import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft';
import faPause from '@fortawesome/fontawesome-free-solid/faPause';
import faExpandArrowsAlt from '@fortawesome/fontawesome-free-solid/faExpandArrowsAlt';
import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
......@@ -21,29 +17,11 @@ import { GitHubApi } from '/apis';
import { classes, refineGist } from '/common/util';
import { actions } from '/reducers';
import { languages } from '/common/config';
import { Button, Ellipsis, ListItem } from '/components';
import { tracerManager } from '/core';
import { Button, Ellipsis, ListItem, Player } from '/components';
import styles from './stylesheet.scss';
@connect(({ current, env }) => ({ current, env }), actions)
class Header extends React.Component {
constructor(props) {
super(props);
const { interval, paused, started } = tracerManager;
this.state = {
interval, paused, started,
};
}
componentDidMount() {
tracerManager.setOnUpdateStatus(update => this.setState(update));
}
componentWillUnmount() {
tracerManager.setOnUpdateStatus(null);
}
handleClickFullScreen() {
if (screenfull.enabled) {
if (screenfull.isFullscreen) {
......@@ -97,12 +75,10 @@ class Header extends React.Component {
}
render() {
const { interval, paused, started } = this.state;
const { className, onClickTitleBar, navigatorOpened, onAction, gistSaved } = this.props;
const { className, onClickTitleBar, navigatorOpened, gistSaved, file } = this.props;
const { gistId, titles } = this.props.current;
const { ext, user } = this.props.env;
// TODO: remove the 'run' button and add 'build' and 'play' buttons
return (
<header className={classes(styles.header, className)}>
<div className={styles.row}>
......@@ -159,42 +135,7 @@ class Header extends React.Component {
</div>
</Button>
</div>
<div className={styles.section} onClick={onAction}>
{
started ? (
<Button icon={faPlay} primary onClick={() => tracerManager.run()} active>Rerun</Button>
) : (
<Button icon={faPlay} primary onClick={() => tracerManager.run()}>Run</Button>
)
}
<Button icon={faChevronLeft} primary disabled={!started}
onClick={() => tracerManager.prev()}>Prev</Button>
{
paused ? (
<Button icon={faPause} primary onClick={() => tracerManager.resume()} active>Resume</Button>
) : (
<Button icon={faPause} primary disabled={!started}
onClick={() => tracerManager.pause()}>Pause</Button>
)
}
<Button icon={faCaretRight} reverse primary disabled={!started}
onClick={() => tracerManager.next()}>Next</Button>
<div className={styles.interval}>
Speed
<InputRange
classNames={{
inputRange: styles.range,
labelContainer: styles.range_label_container,
slider: styles.range_slider,
track: styles.range_track,
}}
maxValue={2000}
minValue={100}
step={100}
value={interval}
onChange={interval => tracerManager.setInterval(interval)} />
</div>
</div>
<Player className={styles.section} file={file} />
</div>
</header>
);
......
......@@ -36,51 +36,6 @@
}
}
.interval {
display: flex;
align-items: center;
padding: 0 12px;
white-space: nowrap;
&:hover {
background-color: $color-shadow;
}
.range {
position: relative;
height: 16px;
width: 60px;
margin-left: 8px;
.range_label_container {
display: none;
}
.range_track {
top: 50%;
height: 6px;
margin-top: -3px;
background-color: $theme-light;
cursor: pointer;
display: block;
position: relative;
}
.range_slider {
top: 0;
width: 6px;
height: 12px;
margin-left: -3px;
margin-top: -3px;
appearance: none;
background-color: $color-font;
cursor: pointer;
display: block;
position: absolute;
}
}
}
.btn_dropdown {
position: relative;
font-weight: bold;
......@@ -110,4 +65,4 @@
}
}
}
}
\ No newline at end of file
}
import React from 'react';
import { connect } from 'react-redux';
import Promise from 'bluebird';
import InputRange from 'react-input-range';
import faPlay from '@fortawesome/fontawesome-free-solid/faPlay';
import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft';
import faChevronRight from '@fortawesome/fontawesome-free-solid/faChevronRight';
import faPause from '@fortawesome/fontawesome-free-solid/faPause';
import faWrench from '@fortawesome/fontawesome-free-solid/faWrench';
import { classes, extension } from '/common/util';
import { TracerApi } from '/apis';
import { CompileError } from '/common/error';
import { actions } from '/reducers';
import { Button } from '/components';
import styles from './stylesheet.scss';
import ProgressBar from '../ProgressBar';
@connect(({ player }) => ({ player }), actions)
class Player extends React.Component {
constructor(props) {
super(props);
this.state = {
interval: 500,
playing: false,
building: false,
};
this.reset();
}
componentDidMount() {
const { file } = this.props;
this.build(file);
}
componentWillReceiveProps(nextProps) {
const { file } = nextProps;
const { buildAt } = nextProps.player;
if (buildAt !== this.props.player.buildAt) {
this.build(file);
}
}
reset(traces = []) {
const chunks = [{
traces: [],
lineNumber: undefined,
}];
while (traces.length) {
const trace = traces.shift();
if (trace.method === 'delay') {
const [lineNumber] = trace.args;
chunks[chunks.length - 1].lineNumber = lineNumber;
chunks.push({
traces: [],
lineNumber: undefined,
});
} else {
chunks[chunks.length - 1].traces.push(trace);
}
}
this.props.setChunks(chunks);
this.props.setCursor(0);
this.pause();
this.props.setLineIndicator(undefined);
}
build(file) {
if (!file) return;
this.setState({ building: true });
this.reset();
const ext = extension(file.name);
(ext in TracerApi ?
TracerApi[ext]({ code: file.content }) :
Promise.reject(new CompileError('Language Not Supported')))
.then(traces => this.reset(traces))
.then(() => this.next())
.catch(error => this.handleError(error))
.finally(() => this.setState({ building: false }));
}
isValidCursor(cursor) {
const { chunks } = this.props.player;
return 1 <= cursor && cursor <= chunks.length;
}
prev() {
this.pause();
const cursor = this.props.player.cursor - 1;
if (!this.isValidCursor(cursor)) return false;
this.props.setCursor(cursor);
return true;
}
resume(wrap = false) {
this.pause();
if (this.next() || wrap && this.props.setCursor(1)) {
this.timer = window.setTimeout(() => this.resume(), this.state.interval);
this.setState({ playing: true });
}
}
pause() {
if (this.timer) {
window.clearInterval(this.timer);
this.timer = null;
this.setState({ playing: false });
}
}
next() {
this.pause();
const cursor = this.props.player.cursor + 1;
if (!this.isValidCursor(cursor)) return false;
this.props.setCursor(cursor);
return true;
}
handleError(error) {
console.error(error);
this.props.showErrorToast({ name: error.name, message: error.message });
}
handleChangeInterval(interval) {
this.setState({ interval });
}
handleChangeProgress(progress) {
const { chunks } = this.props.player;
const cursor = Math.max(1, Math.min(chunks.length, Math.round(progress * chunks.length)));
this.pause();
this.props.setCursor(cursor);
}
render() {
const { className, file } = this.props;
const { chunks, cursor } = this.props.player;
const { interval, playing, building } = this.state;
return (
<div className={classes(styles.player, className)}>
<Button icon={faWrench} primary disabled={building} inProgress={building} onClick={() => this.build(file)}>
{building ? 'Building' : 'Build'}
</Button>
{
playing ? (
<Button icon={faPause} primary active onClick={() => this.pause()}>Pause</Button>
) : (
<Button icon={faPlay} primary onClick={() => this.resume(true)}>Play</Button>
)
}
<Button icon={faChevronLeft} primary disabled={!this.isValidCursor(cursor - 1)} onClick={() => this.prev()} />
<ProgressBar className={styles.progress_bar} current={cursor} total={chunks.length}
onChangeProgress={progress => this.handleChangeProgress(progress)} />
<Button icon={faChevronRight} reverse primary disabled={!this.isValidCursor(cursor + 1)}
onClick={() => this.next()} />
<div className={styles.interval}>
Speed
<InputRange
classNames={{
inputRange: styles.range,
labelContainer: styles.range_label_container,
slider: styles.range_slider,
track: styles.range_track,
}} maxValue={2000} minValue={100} step={100} value={interval}
onChange={interval => this.handleChangeInterval(interval)} />
</div>
</div>
);
}
}
export default Player;
@import "~/common/stylesheet/index";
.player {
.progress_bar {
width: 160px;
}
.interval {
display: flex;
align-items: center;
padding: 0 12px;
white-space: nowrap;
&:hover {
background-color: $color-shadow;
}
.range {
position: relative;
height: 16px;
width: 60px;
margin-left: 8px;
.range_label_container {
display: none;
}
.range_track {
top: 50%;
height: 6px;
margin-top: -3px;
background-color: $theme-light;
cursor: pointer;
display: block;
position: relative;
}
.range_slider {
top: 0;
width: 6px;
height: 12px;
margin-left: -3px;
margin-top: -3px;
appearance: none;
background-color: $color-font;
cursor: pointer;
display: block;
position: absolute;
}
}
}
}
import React from 'react';
import { classes } from '/common/util';
import styles from './stylesheet.scss';
class ProgressBar extends React.Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
handleMouseDown(e) {
this.target = e.target;
console.log(this.target)
this.handleMouseMove(e);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
handleMouseMove(e) {
const { left } = this.target.getBoundingClientRect();
const { offsetWidth } = this.target;
const { onChangeProgress } = this.props;
const progress = (e.clientX - left) / offsetWidth;
if (onChangeProgress) onChangeProgress(progress);
}
handleMouseUp(e) {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
render() {
const { className, total, current } = this.props;
return (
<div className={classes(styles.progress_bar, className)} onMouseDown={this.handleMouseDown}>
<div className={styles.active} style={{ width: `${current / total * 100}%` }} />
<div className={styles.label}>
<span className={styles.current}>{current}</span> / {total}
</div>
</div>
);
}
}
export default ProgressBar;
@import "~/common/stylesheet/index";
.progress_bar {
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: $theme-light;
cursor: pointer;
pointer-events: auto;
> * {
pointer-events: none;
}
.active {
position: absolute;
height: 100%;
left: 0;
background-color: $color-active;
}
.label {
position: absolute;
color: $theme-dark;
.current {
font-weight: bold;
}
}
}
import React from 'react';
import { connect } from 'react-redux';
import { classes } from '/common/util';
import { ResizableContainer } from '/components';
import { actions } from '/reducers';
import styles from './stylesheet.scss';
import { tracerManager } from '../../core';
import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData, MarkdownData } from '/core/datas';
@connect(({ player }) => ({ player }), actions)
class VisualizationViewer extends React.Component {
constructor(props) {
super(props);
this.state = {
renderers: [],
renderersWeights: [],
dataWeights: {},
};
this.datas = [];
}
componentDidMount() {
// TODO: rendereres should remain resized
tracerManager.setOnChangeRenderers(renderers => {
const renderersWeights = renderers.map(() => 1);
this.setState({ renderers, renderersWeights });
const { chunks, cursor } = this.props.player;
this.update(chunks, cursor);
}
componentWillReceiveProps(nextProps) {
const { chunks, cursor } = nextProps.player;
const { chunks: oldChunks, cursor: oldCursor } = this.props.player;
if (chunks !== oldChunks || cursor !== oldCursor) {
this.update(chunks, cursor, oldChunks, oldCursor);
}
}
update(chunks, cursor, oldChunks = [], oldCursor = 0) {
let applyingChunks;
if (cursor > oldCursor) {
applyingChunks = chunks.slice(oldCursor, cursor);
} else {
this.datas = [];
applyingChunks = chunks.slice(0, cursor);
}
applyingChunks.forEach(chunk => this.applyChunk(chunk));
const dataWeights = chunks === oldChunks ? { ...this.state.dataWeights } : {};
this.datas.forEach(data => {
if (!(data.tracerKey in dataWeights)) {
dataWeights[data.tracerKey] = 1;
}
});
this.setState({ dataWeights });
const lastChunk = applyingChunks[applyingChunks.length - 1];
if (lastChunk && lastChunk.lineNumber !== undefined) {
this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor });
} else {
this.props.setLineIndicator(undefined);
}
}
componentWillUnmount() {
tracerManager.setOnChangeRenderers(null);
addTracer(className, tracerKey, title) {
const DataClass = {
Tracer: Data,
MarkdownTracer: MarkdownData,
LogTracer: LogData,
Array2DTracer: Array2DData,
Array1DTracer: Array1DData,
ChartTracer: ChartData,
GraphTracer: GraphData,
}[className];
const data = new DataClass(tracerKey, title, this.datas);
this.datas.push(data);
}
handleChangeRenderersWeights(renderersWeights) {
this.setState({ renderersWeights });
applyTrace(trace) {
const { tracerKey, method, args } = trace;
try {
if (method === 'construct') {
const [className, title] = args;
this.addTracer(className, tracerKey, title);
} else {
const data = this.datas.find(data => data.tracerKey === tracerKey);
data[method](...args);
}
} catch (error) {
this.handleError(error);
}
}
handleError(error) {
console.error(error);
this.props.showErrorToast({ name: error.name, message: error.message });
}
applyChunk(chunk) {
chunk.traces.forEach(trace => this.applyTrace(trace));
}
handleChangeWeights(weights) {
const dataWeights = {};
weights.forEach((weight, i) => {
dataWeights[this.datas[i].tracerKey] = weight;
});
this.setState({ dataWeights });
}
render() {
const { className } = this.props;
const { renderers, renderersWeights } = this.state;
const { dataWeights } = this.state;
return (
<ResizableContainer className={classes(styles.visualization_viewer, className)} weights={renderersWeights}
visibles={renderers.map(() => true)}
onChangeWeights={weights => this.handleChangeRenderersWeights(weights)}>
{renderers}
<ResizableContainer className={classes(styles.visualization_viewer, className)}
weights={this.datas.map(data => dataWeights[data.tracerKey])}
visibles={this.datas.map(() => true)}
onChangeWeights={weights => this.handleChangeWeights(weights)}>
{
this.datas.map(data => data.render())
}
</ResizableContainer>
);
}
......
......@@ -6,8 +6,8 @@ export { default as Ellipsis } from './Ellipsis';
export { default as ExpandableListItem } from './ExpandableListItem';
export { default as Header } from './Header';
export { default as ListItem } from './ListItem';
export { default as MarkdownViewer } from './MarkdownViewer';
export { default as Navigator } from './Navigator';
export { default as Player } from './Player';
export { default as ResizableContainer } from './ResizableContainer';
export { default as TabContainer } from './TabContainer';
export { default as ToastContainer } from './ToastContainer';
......
import { Array2DData } from '/core/datas';
import { tracerManager } from '/core';
import { Array1DRenderer } from '/core/renderers';
class Array1DData extends Array2DData {
getRendererClass() {
return Array1DRenderer;
}
init() {
super.init();
this.chartData = null;
......@@ -30,7 +34,7 @@ class Array1DData extends Array2DData {
}
chart(tracerKey) {
this.chartData = tracerKey ? tracerManager.datas[tracerKey] : null;
this.chartData = tracerKey ? this.findData(tracerKey) : null;
this.syncChartData();
}
......@@ -39,4 +43,4 @@ class Array1DData extends Array2DData {
}
}
export default Array1DData;
\ No newline at end of file
export default Array1DData;
import { Data } from '/core/datas';
import { Array2DRenderer } from '/core/renderers';
class Array2DData extends Data {
getRendererClass() {
return Array2DRenderer;
}
set(array2d = []) {
this.data = [];
for (const array1d of array2d) {
......@@ -60,4 +65,4 @@ class Array2DData extends Data {
}
}
export default Array2DData;
\ No newline at end of file
export default Array2DData;
import { Array1DData } from '/core/datas';
import { ChartRenderer } from '/core/renderers';
class ChartData extends Array1DData {
getRendererClass() {
return ChartRenderer;
}
}
export default ChartData;
\ No newline at end of file
export default ChartData;
class Data {
constructor() {
this.init();
this.reset();
}
init() {
}
setOnRender(onRender) {
this.onRender = onRender;
this.render();
}
render() {
if (this.onRender) this.onRender();
}
set() {
}
reset() {
this.set();
}
delay() {
}
}
export default Data;
\ No newline at end of file
import React from 'react';
import { Renderer } from '/core/renderers';
class Data {
constructor(tracerKey, title, datas) {
this.tracerKey = tracerKey;
this.title = title;
this.datas = datas;
this.init();
this.reset();
}
findData(tracerKey) {
return this.datas.find(data => data.tracerKey === tracerKey);
}
getRendererClass() {
return Renderer;
}
init() {
}
render() {
const RendererClass = this.getRendererClass();
return (
<RendererClass key={this.tracerKey} title={this.title} data={this} />
);
}
set() {
}
reset() {
this.set();
}
}
export default Data;
import { Data } from '/core/datas';
import { distance } from '/common/util';
import { tracerManager } from '/core';
import { GraphRenderer } from '/core/renderers';
class GraphData extends Data {
getRendererClass() {
return GraphRenderer;
}
init() {
super.init();
this.dimensions = {
......@@ -221,8 +225,8 @@ class GraphData extends Data {
}
log(tracerKey) {
this.logData = tracerKey ? tracerManager.datas[tracerKey] : null;
this.logData = tracerKey ? this.findData(tracerKey) : null;
}
}
export default GraphData;
\ No newline at end of file
export default GraphData;
import { Data } from '/core/datas';
import { LogRenderer } from '/core/renderers';
class LogData extends Data {
getRendererClass() {
return LogRenderer;
}
set(messages = []) {
this.messages = messages;
super.set();
......@@ -11,4 +16,4 @@ class LogData extends Data {
}
}
export default LogData;
\ No newline at end of file
export default LogData;
import { Data } from '/core/datas';
import { MarkdownRenderer } from '/core/renderers';
class MarkdownData extends Data {
getRendererClass() {
return MarkdownRenderer;
}
set(markdown = '') {
this.markdown = markdown;
super.set();
}
}
export default MarkdownData;
export { default as Data } from './Data';
export { default as MarkdownData } from './MarkdownData';
export { default as LogData } from './LogData';
export { default as Array2DData } from './Array2DData';
export { default as Array1DData } from './Array1DData';
......
export { default as tracerManager } from './tracerManager';
\ No newline at end of file
import React from 'react';
import ReactMarkdown from 'react-markdown'
import { classes } from '/common/util';
import { Renderer } from '/core/renderers';
import styles from './stylesheet.scss';
import ReactMarkdown from 'react-markdown';
class MarkdownViewer extends React.Component {
render() {
const { className, source, onClickLink } = this.props;
class MarkdownRenderer extends Renderer {
renderData() {
const { markdown } = this.props.data;
const link = ({ href, ...rest }) => {
return !onClickLink || /^https?:\/\//i.test(href) ? (
return (
<a href={href} rel="noopener" target="_blank" {...rest} />
) : (
<a onClick={() => onClickLink(href)} {...rest} />
);
};
......@@ -24,15 +22,14 @@ class MarkdownViewer extends React.Component {
} else {
newSrc = src;
}
return <img src={newSrc} {...rest} />
return <img src={newSrc} {...rest} />;
};
return (
<ReactMarkdown className={classes(styles.markdown_viewer, className)} source={source}
renderers={{ link, image }} escapeHtml={false} />
<ReactMarkdown className={styles.markdown} source={markdown} renderers={{ link, image }} escapeHtml={false} />
);
}
}
export default MarkdownViewer;
export default MarkdownRenderer;
@import "~/common/stylesheet/index";
.markdown_viewer {
.markdown {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 16px;
flex: 1;
align-self: stretch;
padding: 24px;
font-size: $font-size-large;
overflow-y: auto;
a {
text-decoration: underline;
cursor: pointer;
}
}
\ No newline at end of file
}
......@@ -21,39 +21,9 @@ class Renderer extends React.Component {
this.zoomMin = 1 / 20;
}
componentDidMount() {
const { data } = this.props;
this.mountData(data);
}
componentWillUnmount() {
const { data } = this.props;
this.unmountData(data);
}
componentWillReceiveProps(nextProps) {
const { data } = nextProps;
if (data !== this.props.data) {
this.unmountData(this.props.data);
this.mountData(data);
}
}
shouldComponentUpdate() {
return false;
}
componentDidUpdate(prevProps, prevState, snapshot) {
}
mountData(data) {
data.setOnRender(() => this.refresh());
}
unmountData(data) {
data.setOnRender(null);
}
handleMouseDown(e) {
const { clientX, clientY } = e;
this.lastX = clientX;
......
......@@ -3,7 +3,7 @@
.renderer {
position: relative;
flex: 1;
flex-direction: column-reverse;
flex-direction: column;
display: flex;
align-items: center;
justify-content: center;
......@@ -21,4 +21,4 @@
padding: 4px 6px;
font-size: $font-size-large;
}
}
\ No newline at end of file
}
export { default as Renderer } from './Renderer';
export { default as MarkdownRenderer } from './MarkdownRenderer';
export { default as LogRenderer } from './LogRenderer';
export { default as Array2DRenderer } from './Array2DRenderer';
export { default as Array1DRenderer } from './Array1DRenderer';
......
import React from 'react';
import Promise from 'bluebird';
import { extension } from '/common/util';
import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData } from '/core/datas';
import { Array1DRenderer, Array2DRenderer, ChartRenderer, GraphRenderer, LogRenderer, Renderer } from '/core/renderers';
import { TracerApi } from '/apis';
import { CompileError } from '/common/error';
class TracerManager {
constructor() {
this.interval = 500;
this.paused = false;
this.started = false;
this.lineIndicator = null;
this.file = { name: '', content: '', contributors: [] };
this.reset();
}
setOnChangeRenderers(onChangeRenderers) {
this.onChangeRenderers = onChangeRenderers;
if (this.onChangeRenderers) this.onChangeRenderers(this.renderers);
}
setOnUpdateStatus(onUpdateStatus) {
this.onUpdateStatus = onUpdateStatus;
if (this.onUpdateStatus) {
const { interval, paused, started } = this;
this.onUpdateStatus({ interval, paused, started });
}
}
setOnUpdateLineIndicator(onUpdateLineIndicator) {
this.onUpdateLineIndicator = onUpdateLineIndicator;
if (this.onUpdateLineIndicator) this.onUpdateLineIndicator(this.lineIndicator);
}
setOnError(onError) {
this.onError = onError;
}
render() {
Object.values(this.datas).forEach(data => data.render());
}
setInterval(interval) {
this.interval = interval;
if (this.onUpdateStatus) this.onUpdateStatus({ interval });
}
setPaused(paused) {
this.paused = paused;
if (this.onUpdateStatus) this.onUpdateStatus({ paused });
}
setStarted(started) {
this.started = started;
if (this.onUpdateStatus) this.onUpdateStatus({ started });
}
setLineIndicator(lineIndicator) {
this.lineIndicator = lineIndicator;
if (this.onUpdateLineIndicator) this.onUpdateLineIndicator(lineIndicator);
}
setFile(file, initialRun) {
this.file = file;
if (initialRun) this.runInitial();
}
reset(traces = []) {
this.traces = traces;
this.resetCursor();
this.stopTimer();
this.setPaused(false);
this.setStarted(false);
this.setLineIndicator(null);
}
resetCursor() {
this.renderers = [];
this.datas = {};
this.cursor = 0;
this.chunkCursor = 0;
if (this.onChangeRenderers) this.onChangeRenderers(this.renderers);
}
addTracer(className, tracerKey, title) {
const [DataClass, RendererClass] = {
Tracer: [Data, Renderer],
LogTracer: [LogData, LogRenderer],
Array2DTracer: [Array2DData, Array2DRenderer],
Array1DTracer: [Array1DData, Array1DRenderer],
ChartTracer: [ChartData, ChartRenderer],
GraphTracer: [GraphData, GraphRenderer],
}[className];
const data = new DataClass();
this.datas[tracerKey] = data;
const renderer = (
<RendererClass key={tracerKey} title={title} data={data} wsProps={{ fixed: true }} />
);
this.renderers.push(renderer);
if (this.onChangeRenderers) this.onChangeRenderers(this.renderers);
}
applyTrace() {
if (this.cursor >= this.traces.length) return false;
const trace = this.traces[this.cursor++];
const { tracerKey, method, args } = trace;
try {
if (method === 'construct') {
const [className, title] = args;
this.addTracer(className, tracerKey, title);
return true;
} else {
const data = this.datas[tracerKey];
const delay = method === 'delay';
const newArgs = [...args];
if (delay) {
const lineNumber = newArgs.shift();
this.setLineIndicator({ lineNumber, cursor: this.cursor });
}
data[method](...newArgs);
return !delay;
}
} catch (error) {
if (this.started) this.handleError(error);
return false;
}
}
applyTraceChunk(render = true) {
if (this.cursor >= this.traces.length) return false;
while (this.applyTrace()) ;
this.chunkCursor++;
if (render) this.render();
return true;
}
startTimer() {
this.stopTimer();
if (this.applyTraceChunk()) {
this.timer = window.setTimeout(() => this.startTimer(), this.interval);
} else {
this.setPaused(false);
this.setStarted(false);
}
}
stopTimer() {
if (this.timer) {
window.clearInterval(this.timer);
this.timer = null;
}
}
execute() {
const { name, content } = this.file;
const ext = extension(name);
if (ext in TracerApi) {
return TracerApi[ext]({ code: content });
} else {
return Promise.reject(new CompileError('Language Not Supported'));
}
}
runInitial() {
this.reset();
this.render();
this.execute()
.then(traces => {
this.reset(traces);
this.applyTraceChunk();
})
.catch(error => {
});
}
run() {
this.reset();
this.render();
this.execute()
.then(traces => {
this.reset(traces);
this.resume();
this.setStarted(true);
})
.catch(error => {
this.handleError(error);
});
}
prev() {
this.pause();
const lastChunk = this.chunkCursor - 1;
this.resetCursor();
do {
this.applyTraceChunk(false);
} while (this.chunkCursor < lastChunk);
this.render();
}
resume() {
this.startTimer();
this.setPaused(false);
}
pause() {
this.stopTimer();
this.setPaused(true);
}
next() {
this.pause();
this.applyTraceChunk();
}
handleError(error) {
console.error(error);
if (this.onError) this.onError(error);
}
}
const tracerManager = new TracerManager();
export default tracerManager;
\ No newline at end of file
import { actions as currentActions } from './current';
import { actions as directoryActions } from './directory';
import { actions as envActions } from './env';
import { actions as playerActions } from './player';
import { actions as toastActions } from './toast';
export { default as current } from './current';
export { default as directory } from './directory';
export { default as env } from './env';
export { default as player } from './player';
export { default as toast } from './toast';
export const actions = {
...currentActions,
...directoryActions,
...envActions,
...playerActions,
...toastActions,
};
\ No newline at end of file
};
import { combineActions, createAction, handleActions } from 'redux-actions';
const prefix = 'PLAYER';
const shouldBuild = createAction(`${prefix}/SHOULD_BUILD`, () => ({ buildAt: Date.now() }));
const setChunks = createAction(`${prefix}/SET_CHUNKS`, chunks => ({ chunks }));
const setCursor = createAction(`${prefix}/SET_CURSOR`, cursor => ({ cursor }));
const setLineIndicator = createAction(`${prefix}/SET_LINE_INDICATOR`, lineIndicator => ({ lineIndicator }));
export const actions = {
shouldBuild,
setChunks,
setCursor,
setLineIndicator,
};
const defaultState = {
buildAt: 0,
chunks: [],
cursor: 0,
lineIndicator: undefined,
};
export default handleActions({
[combineActions(
shouldBuild,
setChunks,
setCursor,
setLineIndicator,
)]: (state, { payload }) => ({
...state,
...payload,
}),
}, defaultState);
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册