feat(xod-client, xod-client-electron): deployment pane

Closes #959
This commit is contained in:
Evgeny Kochetkov
2017-12-25 15:08:23 +03:00
parent 97f7a2e659
commit 1b020104e1
30 changed files with 654 additions and 314 deletions

View File

@@ -24,7 +24,7 @@
"doc:fz": "cd docs && make",
"lerna": "lerna",
"lint": "eslint packages/*/src packages/*/test packages/*/test-func --ext js,jsx --max-warnings 0",
"start:electron": "lerna run start --scope xod-client-electron",
"start:electron": "lerna run start --scope xod-client-electron --stream",
"start:spectron-repl": "./tools/spectron-repl",
"storybook": "lerna run storybook --stream --scope xod-client",
"test": "XOD_HM_DEF=true lerna run test",

View File

@@ -140,11 +140,22 @@ export const upload = R.curry(
export const buildAndUpload = R.curry(
(sketchFilePath, fqbn, packagesDir, librariesDir, buildDir, portName, builderToolDir) =>
build(sketchFilePath, fqbn, packagesDir, librariesDir, buildDir, builderToolDir)
.then((res) => {
if (res.exitCode !== 0) {
return Promise.reject(Object.assign(new Error(res.stderr), res));
.then((buildResult) => {
if (buildResult.exitCode !== 0) {
return Promise.reject(Object.assign(new Error(buildResult.stderr), buildResult));
}
return upload(sketchFilePath, fqbn, packagesDir, buildDir, portName);
return upload(sketchFilePath, fqbn, packagesDir, buildDir, portName)
.then((uploadResult) => {
if (uploadResult.exitCode !== 0) {
return Promise.reject(Object.assign(new Error(uploadResult.stderr), uploadResult));
}
return {
exitCode: uploadResult.exitCode,
stdout: [buildResult.stdout, uploadResult.stdout].join('\n'),
stderr: [buildResult.stderr, uploadResult.stderr].join('\n'),
};
});
})
);

View File

@@ -267,8 +267,9 @@ class App extends client.App {
/>
<client.Editor
size={this.state.size}
onUploadClick={this.onUpload}
onUploadAndDebugClick={this.onUpload}
/>
<client.SnackBar />
<PopupInstallApp
isVisible={this.state.popupInstallApp}
onClose={this.hideInstallAppPopup}

View File

@@ -192,7 +192,7 @@ const deployToArduino = ({
return Promise.reject(Object.assign(new Error(`Upload tool exited with error code: ${result.exitCode}`), result));
}
sendSuccess(
[result.stderr, result.stdout].join('\n\n'),
[result.stdout, result.stderr].join('\n\n'),
100
)();

View File

@@ -25,10 +25,8 @@ export default (state, action) => {
switch (action.type) {
case UPLOAD:
case UPLOAD_TO_ARDUINO: {
if (action.meta.status === 'deleted') {
return hideOnePopup(POPUP_ID.UPLOADING, state);
} else if (action.meta.status !== 'failed') {
return showOnlyPopup(POPUP_ID.UPLOADING, {}, state);
if (action.meta.status === 'started') {
return hideOnePopup(POPUP_ID.UPLOADING_CONFIG, state);
}
return state;
}

View File

@@ -1,144 +0,0 @@
import R from 'ramda';
import React from 'react';
import PropTypes from 'prop-types';
import { STATUS, PopupForm } from 'xod-client';
import { Line as ProgressBar } from 'rc-progress';
class PopupUploadProject extends React.Component {
constructor(props) {
super(props);
this.state = {
isVisible: props.isVisible,
};
this.onClose = this.onClose.bind(this);
}
onClose() {
if (this.canClose()) {
this.props.onClose(this.props.upload.id);
}
}
getTitle() {
switch (this.props.upload.status) {
default:
return 'Uploading project';
}
}
getMessage() {
const preStyle = {
overflow: 'auto',
maxWidth: 'auto',
maxHeight: '300px',
// allow user to select the log for bug reporting etc
userSelect: 'initial',
cursor: 'auto',
};
const message = (this.props.upload.message) ?
(<pre style={preStyle}>{this.props.upload.message.replace(/\r[^\n]/g, '\n')}</pre>) :
null;
const titleMessage = R.cond([
[R.equals(STATUS.SUCCEEDED), R.always(
<p>
The program uploaded successfully.
</p>
)],
[R.equals(STATUS.FAILED), R.always(
<p>
Oops! Error occured.
</p>
)],
[R.T, R.always(
<p>
Your program is uploading onto device.<br />
Do not unplug the device.
</p>
)],
])(this.props.upload.status);
return (
<div>
{titleMessage}
{message}
</div>
);
}
getProgress() {
if (this.isSucceeded()) {
return '100';
}
if (this.props.upload.percentage) {
return this.props.upload.percentage.toString();
}
return '0';
}
isSucceeded() {
return (this.props.upload.status === STATUS.SUCCEEDED);
}
isFailed() {
return (this.props.upload.status === STATUS.FAILED);
}
canClose() {
return (this.isSucceeded() || this.isFailed());
}
render() {
const title = this.getTitle();
const message = this.getMessage();
const progress = this.getProgress();
const color = this.isFailed() ? '#ed5b5b' : '#81c522';
return (
<PopupForm
isVisible={this.props.isVisible}
title={title}
isClosable={this.canClose()}
onClose={this.onClose}
>
<div className="ModalContent">
<ProgressBar
percent={progress}
strokeWidth="5"
strokeColor={color}
strokeLinecap="square"
trailWidth="5"
trailColor="#373737"
className="ProgressBar"
/>
</div>
<div className="ModalContent">
{message}
</div>
</PopupForm>
);
}
}
PopupUploadProject.propTypes = {
upload: PropTypes.object,
isVisible: PropTypes.bool,
onClose: PropTypes.func,
};
PopupUploadProject.defaultProps = {
upload: {
status: STATUS.STARTED,
message: '',
percentage: 0,
},
isVisible: false,
};
export default PopupUploadProject;

View File

@@ -0,0 +1,3 @@
export default {
SUCCESS: 'Uploaded successfully',
};

View File

@@ -27,7 +27,6 @@ import { UPLOAD, UPLOAD_TO_ARDUINO } from '../../upload/actionTypes';
import PopupSetWorkspace from '../../settings/components/PopupSetWorkspace';
import PopupCreateWorkspace from '../../settings/components/PopupCreateWorkspace';
import PopupProjectSelection from '../../projects/components/PopupProjectSelection';
import PopupUploadProject from '../../upload/components/PopupUploadProject';
import PopupUploadConfig from '../../upload/components/PopupUploadConfig';
import { REDUCER_STATUS } from '../../projects/constants';
import { SaveProgressBar } from '../components/SaveProgressBar';
@@ -35,6 +34,7 @@ import { SaveProgressBar } from '../components/SaveProgressBar';
import formatError from '../../shared/errorFormatter';
import * as EVENTS from '../../shared/events';
import * as MESSAGES from '../../shared/messages';
import UPLOAD_MESSAGES from '../../upload/messages';
import { createSystemMessage } from '../../shared/debuggerMessages';
import { subscribeAutoUpdaterEvents } from '../autoupdate';
@@ -83,6 +83,7 @@ class App extends client.App {
this.getSelectedBoard = this.getSelectedBoard.bind(this);
this.onUploadToArduinoClicked = this.onUploadToArduinoClicked.bind(this);
this.onUploadToArduinoAndDebugClicked = this.onUploadToArduinoAndDebugClicked.bind(this);
this.onUploadToArduino = this.onUploadToArduino.bind(this);
this.onArduinoTargetBoardChange = this.onArduinoTargetBoardChange.bind(this);
this.onSerialPortChange = this.onSerialPortChange.bind(this);
@@ -188,6 +189,10 @@ class App extends client.App {
this.props.actions.uploadToArduinoConfig();
}
onUploadToArduinoAndDebugClicked() {
this.props.actions.uploadToArduinoConfig(true);
}
onUploadToArduino(board, port, cloud, debug, processActions = null) {
const proc = (processActions !== null) ? processActions : this.props.actions.uploadToArduino();
const eitherTProject = this.transformProjectForTranspiler(debug);
@@ -219,6 +224,9 @@ class App extends client.App {
}
if (payload.success) {
proc.success(payload.message);
this.props.actions.addConfirmation({ title: UPLOAD_MESSAGES.SUCCESS });
if (debug) {
foldEither(
error => this.props.actions.addError(
@@ -633,15 +641,6 @@ class App extends client.App {
/>
) : null;
}
renderPopupUploadProcess() {
return (this.props.popups.uploadProject) ? (
<PopupUploadProject
isVisible
upload={this.props.upload}
onClose={this.onUploadPopupClose}
/>
) : null;
}
render() {
return (
@@ -654,10 +653,11 @@ class App extends client.App {
<client.Editor
size={this.state.size}
stopDebuggerSession={() => debuggerIPC.sendStopDebuggerSession(ipcRenderer)}
onUploadClick={this.onUploadToArduinoClicked}
onUploadAndDebugClick={this.onUploadToArduinoAndDebugClicked}
/>
{this.renderPopupShowCode()}
{this.renderPopupUploadConfig()}
{this.renderPopupUploadProcess()}
{this.renderPopupProjectPreferences()}
{this.renderPopupPublishProject()}
{this.renderPopupCreateNewProject()}
@@ -762,7 +762,6 @@ const mapStateToProps = R.applySpec({
switchWorkspace: client.getPopupVisibility(client.POPUP_ID.SWITCHING_WORKSPACE),
createWorkspace: client.getPopupVisibility(client.POPUP_ID.CREATING_WORKSPACE),
uploadToArduinoConfig: client.getPopupVisibility(client.POPUP_ID.UPLOADING_CONFIG),
uploadProject: client.getPopupVisibility(client.POPUP_ID.UPLOADING),
showCode: client.getPopupVisibility(client.POPUP_ID.SHOWING_CODE),
projectPreferences: client.getPopupVisibility(
client.POPUP_ID.EDITING_PROJECT_PREFERENCES
@@ -789,6 +788,7 @@ const mapDispatchToProps = dispatch => ({
switchWorkspace: settingsActions.switchWorkspace,
openProject: client.openProject,
saveAll: actions.saveAll,
addConfirmation: client.addConfirmation,
uploadToArduino: uploadActions.uploadToArduino,
uploadToArduinoConfig: uploadActions.uploadToArduinoConfig,
hideUploadConfigPopup: uploadActions.hideUploadConfigPopup,

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="14px"
height="14px" viewBox="0 0 14 14" enable-background="new 0 0 14 14" xml:space="preserve">
<g id="Layer_1">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#AAAAAA" d="M4.066,3.809l1.912,0.259l3.906,3.988L13.814,4l-3.616-3.667
l-0.31-0.317L8.165,0.008L4.066,3.809z"/>
<g>
<path fill="#AAAAAA" d="M5.875,12c-0.264,0-0.512-0.103-0.699-0.283L2.004,8.618c-0.386-0.376-0.386-0.989,0-1.366L9.14,0.283
C9.327,0.101,9.575,0,9.84,0c0.264,0,0.513,0.101,0.698,0.283l3.173,3.097c0.385,0.376,0.385,0.989,0,1.366l-7.136,6.97
C6.388,11.897,6.14,12,5.875,12z M9.84,0.516c-0.124,0-0.24,0.047-0.327,0.131l-7.135,6.97c-0.18,0.175-0.18,0.461,0,0.638
l3.172,3.097c0.087,0.086,0.202,0.132,0.326,0.132s0.24-0.046,0.327-0.132l7.135-6.969c0.18-0.176,0.18-0.462,0-0.637
l-3.17-3.099C10.079,0.563,9.962,0.516,9.84,0.516z"/>
</g>
<g>
<g>
<path fill="#AAAAAA" d="M4.826,11.928c-0.719,0-1.245-0.025-1.524-0.256l-0.02-0.019L0.286,8.615
c-0.382-0.373-0.382-0.986,0.003-1.363l7.137-6.969c0.385-0.377,1.012-0.377,1.398,0l3.17,3.097
c0.388,0.376,0.388,0.989,0,1.366l-0.371-0.364c0.18-0.176,0.18-0.462,0-0.637L8.451,0.647c-0.181-0.175-0.473-0.175-0.652,0
l-7.136,6.97c-0.18,0.175-0.18,0.461,0,0.638l2.987,3.03c0.182,0.132,0.864,0.129,1.468,0.126c0.13,0,0.263-0.002,0.398,0
l0,0.515c-0.135,0-0.267,0-0.396,0.002C5.018,11.928,4.92,11.928,4.826,11.928z"/>
</g>
</g>
</g>
</g>
<g id="Layer_2">
<rect y="12" fill="#AAAAAA" width="14" height="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="14px" height="14px" viewBox="0 0 14 14" enable-background="new 0 0 14 14" xml:space="preserve">
<g>
<g>
<path fill="#AAAAAA" d="M13.833,7.598c-0.11-0.113-0.242-0.17-0.394-0.17h-1.96V4.804l1.514-1.545
c0.111-0.113,0.167-0.247,0.167-0.402s-0.056-0.289-0.167-0.402c-0.11-0.113-0.242-0.169-0.394-0.169s-0.283,0.057-0.394,0.169
L10.692,4H3.308L1.793,2.455C1.683,2.342,1.552,2.286,1.4,2.286c-0.151,0-0.283,0.057-0.394,0.169
C0.896,2.568,0.84,2.702,0.84,2.857s0.056,0.289,0.167,0.402L2.52,4.804v2.625H0.56c-0.152,0-0.283,0.057-0.394,0.17
C0.055,7.711,0,7.845,0,8c0,0.154,0.055,0.289,0.166,0.401c0.111,0.113,0.242,0.17,0.394,0.17h1.96
c0,0.952,0.169,1.769,0.507,2.446L1.26,13.045c-0.1,0.118-0.145,0.257-0.136,0.415c0.009,0.157,0.068,0.29,0.179,0.396
C1.415,13.952,1.54,14,1.68,14c0.163,0,0.303-0.063,0.42-0.188l1.601-1.849l0.131,0.125c0.082,0.078,0.208,0.175,0.381,0.29
c0.172,0.116,0.364,0.232,0.577,0.349s0.467,0.215,0.761,0.295c0.295,0.08,0.591,0.121,0.888,0.121V6.036h1.12v7.107
c0.28,0,0.563-0.039,0.848-0.117c0.286-0.077,0.525-0.163,0.718-0.259c0.192-0.095,0.382-0.203,0.569-0.325
c0.187-0.122,0.309-0.207,0.367-0.255s0.102-0.086,0.131-0.116l1.732,1.759c0.105,0.113,0.236,0.17,0.394,0.17
c0.158,0,0.289-0.057,0.395-0.17c0.11-0.113,0.166-0.247,0.166-0.401c0-0.155-0.056-0.289-0.166-0.402l-1.82-1.865
c0.391-0.709,0.587-1.572,0.587-2.59h1.959c0.152,0,0.283-0.057,0.395-0.169C13.944,8.289,14,8.155,14,8
C14,7.845,13.944,7.711,13.833,7.598z"/>
<path fill="#AAAAAA" d="M8.982,0.835C8.437,0.278,7.776,0,7,0S5.563,0.278,5.018,0.835S4.2,2.065,4.2,2.857h5.6
C9.8,2.066,9.527,1.392,8.982,0.835z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="13px" height="13px" viewBox="0 0 13 13" enable-background="new 0 0 13 13" xml:space="preserve">
<path fill="#AAAAAA" d="M12.945,0.359C12.839,0.12,12.658,0,12.399,0H0.598C0.341,0,0.16,0.12,0.055,0.359
c-0.104,0.253-0.061,0.469,0.129,0.646l4.545,4.551v4.487c0,0.163,0.058,0.299,0.176,0.416l2.358,2.363
C7.377,12.938,7.513,13,7.682,13c0.072,0,0.148-0.016,0.23-0.045c0.237-0.106,0.357-0.287,0.357-0.546V5.557l4.544-4.551
C13.007,0.828,13.048,0.612,12.945,0.359z"/>
</svg>

After

Width:  |  Height:  |  Size: 857 B

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="14px" height="14px" viewBox="-3.5 14.5 14 14" enable-background="new -3.5 14.5 14 14" xml:space="preserve">
<g>
<g>
<polygon fill="#AAAAAA" points="7.5,20.65 4.445,20.65 4.445,14.5 -0.5,22.352 2.556,22.352 2.556,28.5 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 623 B

View File

@@ -32,3 +32,16 @@
.icon-account {
@include icon('../assets/icons/account.svg');
}
.icon-quick-upload {
@include icon('../assets/icons/quick-upload.svg');
}
.icon-debug {
@include icon('../assets/icons/debug.svg');
}
.icon-filter {
@include icon('../assets/icons/filter.svg');
}
.icon-clear-log {
@include icon('../assets/icons/clear-log.svg');
}

View File

@@ -1,7 +1,6 @@
.Breadcrumbs {
z-index: 9;
position: absolute;
top: 30px;
left: 0;
right: 0;

View File

@@ -1,6 +1,5 @@
.CppImplementationEditors {
display: flex;
height: 100%;
width: 100%;
flex-grow: 1;

View File

@@ -1,7 +1,7 @@
.debug-session-stop-button {
z-index: 10;
position: absolute;
top: 33px;
top: 3px;
right: 3px;
padding: 8px 15px 10px 15px;
@@ -13,88 +13,126 @@
}
.Debugger {
position: absolute;
z-index: 10;
bottom: 0;
height: 196px;
height: 220px;
width: 100%;
border-top: 2px solid $chrome-outlines;
color: $sidebar-color-text;
.caption {
position: relative;
display: block;
box-sizing: border-box;
height: 20px;
width: 120px;
margin-left: 2em;
margin-top: -20px;
padding: 4px 8px;
cursor: default;
background: $sidebar-color-bg;
border-top-right-radius: 2px;
border-top-left-radius: 2px;
.close-button {
display: inline-block;
width: 16px;
height: 16px;
padding: 0;
position: absolute;
top: 2px;
right: 2px;
background: none;
border: none;
font-size: 1.4em;
line-height: 0;
color: $sidebar-color-text;
&:hover {
color: $sidebar-color-text-hover;
}
}
.title {
vertical-align: middle;
}
.status {
font-size: $font-size-s;
margin-right: 0.5em;
}
&.isCollapsed {
height: 24px;
}
.clear-log-button {
position: absolute;
top: 2px;
right: 22px;
z-index: 3;
.titlebar {
background-color: $coal-bright;
border: 0;
background: $sidebar-color-bg;
color: $sidebar-color-text;
display: flex;
flex-direction: row;
align-items: stretch;
padding: 2px 4px;
.expander {
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
&:hover {
color: $sidebar-color-text-hover;
background: $sidebar-color-bg-hover;
.title {
margin-left: 7px;
color: $light-grey-bright;
}
.progress {
padding-left: 14px;
.progress-trail {
width: 150px;
height: 7px;
border-radius: 7px;
background-color: $dark;
overflow: hidden;
.progress-line {
height: 7px;
background-color: $green;
}
}
}
}
@mixin deploymentPanelButton() {
box-sizing: border-box;
width: 24px;
height: 24px;
padding: 0;
text-align: center;
vertical-align: middle;
line-height: 1;
background: none;
border-radius: 0;
border: none;
outline: none;
cursor: pointer;
color: #ccc;
&:hover {
color: #ccc;
background: #444;
}
}
.quick-upload-button {
@include deploymentPanelButton();
@extend .icon-quick-upload;
}
.debug-button {
@include deploymentPanelButton();
@extend .icon-debug;
}
.filter-button {
@include deploymentPanelButton();
@extend .icon-filter;
}
.clear-log-button {
@include deploymentPanelButton();
@extend .icon-clear-log;
}
.close-button {
@include deploymentPanelButton();
&::before {
line-height: 24px;
opacity: 0.4;
}
}
}
.container {
display: block;
height: 180px;
padding: 8px;
height: 196px;
background: $sidebar-color-bg;
border-left: 1px solid $color-canvas-background;
}
.log {
max-height: 180px;
max-height: 196px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
opacity: 0;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(white, 0.35);
border-radius: 3px;
}
pre {
margin: 0;
@@ -142,8 +180,6 @@
display: inline-block;
color: $color-comment-text;
font-family: $font-family-mono;
white-space: nowrap;
}
}

View File

@@ -5,6 +5,7 @@
display: flex;
flex-direction: column;
flex-wrap: nowrap;
flex-shrink: 0;
background: $sidebar-color-bg;

View File

@@ -4,3 +4,9 @@
flex-direction: column;
flex-grow: 1;
}
.Workarea-inner {
position: relative;
display: flex;
flex-grow: 1;
}

View File

@@ -1,5 +1,5 @@
export const SHOW_DEBUGGER_PANEL = 'SHOW_DEBUGGER_PANEL';
export const HIDE_DEBUGGER_PANEL = 'HIDE_DEBUGGER_PANEL';
export const UPLOAD = 'UPLOAD';
export const TOGGLE_DEBUGGER_PANEL = 'TOGGLE_DEBUGGER_PANEL';
export const DEBUGGER_LOG_ADD_MESSAGES = 'DEBUGGER_LOG_ADD_MESSAGES';

View File

@@ -1,20 +1,12 @@
import * as AT from './actionTypes';
export const showDebugger = () => ({
type: AT.SHOW_DEBUGGER_PANEL,
});
export const hideDebugger = () => ({
type: AT.HIDE_DEBUGGER_PANEL,
});
export const toggleDebugger = () => ({
type: AT.TOGGLE_DEBUGGER_PANEL,
});
export const addMessagesToDebuggerLog = message => ({
export const addMessagesToDebuggerLog = messages => ({
type: AT.DEBUGGER_LOG_ADD_MESSAGES,
payload: message,
payload: messages,
});
export const clearDebuggerLog = () => ({

View File

@@ -0,0 +1,15 @@
export const UPLOAD_STATUS = {
STARTED: 'started',
PROGRESSED: 'progressed',
SUCCEEDED: 'succeeded',
FAILED: 'failed',
};
export const UPLOAD_MSG_TYPE = {
XOD: 'xod',
LOG: 'log',
ERROR: 'error',
SYSTEM: 'system',
FLASHER: 'flasher',
};

View File

@@ -1,65 +1,180 @@
import R from 'ramda';
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ContextMenuTrigger, ContextMenu, MenuItem } from 'react-contextmenu';
import { Icon } from 'react-fa';
import classNames from 'classnames';
import { foldMaybe } from 'xod-func-tools';
import { isDebugSession } from '../selectors';
import { getUploadProgress, isDebuggerVisible } from '../selectors';
import { UPLOAD_MSG_TYPE } from '../constants';
import * as DA from '../actions';
import Log from './Log';
const Debugger = ({ active, actions }) => {
const cls = classNames('Debugger', {
'is-active': active,
});
const statusIcon = (active) ? 'play' : 'stop';
return (
<div className={cls}>
<div className="caption">
<Icon
key="status"
name={statusIcon}
title="Debugs session is running"
className="status"
/>
<span className="title">
Debugger
</span>
<button
className="close-button"
onClick={actions.hideDebugger}
>
&times;
</button>
</div>
<button
className="clear-log-button"
onClick={actions.clearLog}
>
Clear log
</button>
<div className="container">
<Log />
</div>
</div>
);
const contextMenuAttrs = {
className: 'contextmenu filter-button',
};
const DEPLOYMENT_PANEL_FILTER_CONTEXT_MENU_ID = 'DEPLOYMENT_PANEL_FILTER_CONTEXT_MENU_ID';
const checkmark = active => (active ? <span className="state"></span> : null);
class Debugger extends React.Component {
constructor(props) {
super(props);
this.state = {
messageTypeFilter: {
[UPLOAD_MSG_TYPE.FLASHER]: true,
[UPLOAD_MSG_TYPE.XOD]: false,
},
};
this.toggleDebugMessages = this.toggleMessageType.bind(this, UPLOAD_MSG_TYPE.XOD);
this.toggleUploadMessages = this.toggleMessageType.bind(this, UPLOAD_MSG_TYPE.FLASHER);
}
toggleMessageType(type) {
this.setState(R.over(
R.lensPath(['messageTypeFilter', type]),
R.not
));
}
renderControlsForExpandedState() {
const { isExpanded, actions } = this.props;
if (!isExpanded) return null;
const { messageTypeFilter } = this.state;
return (
<React.Fragment>
<button
className="clear-log-button"
onClick={actions.clearLog}
title="Clear Log"
/>
<ContextMenuTrigger
id={DEPLOYMENT_PANEL_FILTER_CONTEXT_MENU_ID}
key="contextMenuTrigger"
renderTag="button"
attributes={contextMenuAttrs}
holdToDisplay={0}
>
<span />
</ContextMenuTrigger>
<ContextMenu id={DEPLOYMENT_PANEL_FILTER_CONTEXT_MENU_ID}>
<MenuItem onClick={this.toggleUploadMessages}>
{checkmark(messageTypeFilter[UPLOAD_MSG_TYPE.FLASHER])}
Upload Log
</MenuItem>
<MenuItem onClick={this.toggleDebugMessages}>
{checkmark(messageTypeFilter[UPLOAD_MSG_TYPE.XOD])}
Watched Values
</MenuItem>
</ContextMenu>
</React.Fragment>
);
}
render() {
const {
maybeUploadProgress,
actions,
onUploadClick,
onUploadAndDebugClick,
isExpanded,
} = this.props;
const uploadProgress = foldMaybe(
null,
progress => (
<div className="progress-trail">
<div
className="progress-line"
style={{ width: `${progress}%` }}
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={progress}
/>
</div>
),
maybeUploadProgress
);
const { messageTypeFilter } = this.state;
const rejectedMessageTypes = R.compose(
R.keys,
R.filter(R.equals(false))
)(messageTypeFilter);
return (
<div className={cn('Debugger', { isCollapsed: !isExpanded })}>
<div className="titlebar">
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
role="button"
className="expander"
onClick={actions.toggleDebugger}
>
<span className="title">
Deployment
</span>
<div className="progress">
{uploadProgress}
</div>
</div>
{this.renderControlsForExpandedState()}
<button
className="quick-upload-button"
onClick={onUploadClick}
title="Upload to Arduino"
/>
<button
className="debug-button"
onClick={onUploadAndDebugClick}
title="Upload and Debug"
/>
<Icon
Component="button"
className="close-button"
name={isExpanded ? 'chevron-down' : 'chevron-up'}
onClick={actions.toggleDebugger}
/>
</div>
{isExpanded ? (
<div className="container">
<Log rejectedMessageTypes={rejectedMessageTypes} />
</div>
) : null}
</div>
);
}
}
Debugger.propTypes = {
active: PropTypes.bool,
maybeUploadProgress: PropTypes.object.isRequired,
isExpanded: PropTypes.bool.isRequired,
actions: PropTypes.objectOf(PropTypes.func),
onUploadClick: PropTypes.func.isRequired,
onUploadAndDebugClick: PropTypes.func.isRequired,
};
const mapStateToProps = R.applySpec({
active: isDebugSession,
maybeUploadProgress: getUploadProgress,
isExpanded: isDebuggerVisible,
});
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators({
hideDebugger: DA.hideDebugger,
toggleDebugger: DA.toggleDebugger,
clearLog: DA.clearDebuggerLog,
}, dispatch),
});

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Infinite from 'react-infinite';
import { getLog } from '../selectors';
import { getLog, getUploadLog } from '../selectors';
import SystemMessage from '../components/SystemMessage';
import ErrorMessage from '../components/ErrorMessage';
import LogMessage from '../components/LogMessage';
@@ -24,23 +24,31 @@ const renderLogMessage = (messageData, idx) => R.compose(
])
)(messageData);
const Log = ({ log }) => (
const Log = ({ log, uploadLog, rejectedMessageTypes }) => (
<Infinite
className="log"
elementHeight={19}
containerHeight={180}
containerHeight={196}
displayBottomUpwards
>
{log.map(renderLogMessage)}
{
uploadLog
.concat(log)
.filter(msg => !rejectedMessageTypes.includes(msg.type))
.map(renderLogMessage)
}
</Infinite>
);
Log.propTypes = {
log: PropTypes.arrayOf(PropTypes.object),
uploadLog: PropTypes.arrayOf(PropTypes.object),
rejectedMessageTypes: PropTypes.arrayOf(PropTypes.string),
};
const mapStateToProps = R.applySpec({
log: getLog,
uploadLog: getUploadLog,
});
export default connect(mapStateToProps, {})(Log);

View File

@@ -0,0 +1,2 @@
export const TRANSPILING = 'Transpiling...';
export const SUCCES = 'Done!';

View File

@@ -2,8 +2,8 @@ import R from 'ramda';
import { renameKeys, invertMap } from 'xod-func-tools';
import {
SHOW_DEBUGGER_PANEL,
HIDE_DEBUGGER_PANEL,
UPLOAD,
TOGGLE_DEBUGGER_PANEL,
DEBUGGER_LOG_ADD_MESSAGES,
@@ -13,6 +13,9 @@ import {
DEBUG_SESSION_STOPPED,
} from './actionTypes';
import { UPLOAD_STATUS, UPLOAD_MSG_TYPE } from './constants';
import * as MSG from './messages';
import initialState from './state';
const MAX_LOG_MESSAGES = 1000;
@@ -23,10 +26,24 @@ const MAX_LOG_MESSAGES = 1000;
//
// =============================================================================
const addToLog = R.over(R.lensProp('log'));
const createSystemMessage = message => ({
type: UPLOAD_MSG_TYPE.SYSTEM,
message,
});
const createFlasherMessage = message => ({
type: UPLOAD_MSG_TYPE.FLASHER,
message,
});
const createErrorMessage = message => ({
type: UPLOAD_MSG_TYPE.ERROR,
message,
});
const overDebugLog = R.over(R.lensProp('log'));
const overUploadLog = R.over(R.lensProp('uploadLog'));
const addMessageToLog = R.curry(
(message, state) => addToLog(
(message, state) => overDebugLog(
R.compose(
R.takeLast(MAX_LOG_MESSAGES),
R.append(message)
@@ -36,7 +53,7 @@ const addMessageToLog = R.curry(
);
const addMessageListToLog = R.curry(
(messages, state) => addToLog(
(messages, state) => overDebugLog(
R.compose(
R.takeLast(MAX_LOG_MESSAGES),
R.concat(R.__, messages)
@@ -66,7 +83,6 @@ const updateWatchNodeValues = R.curry(
);
const showDebuggerPane = R.assoc('isVisible', true);
const hideDebuggerPane = R.assoc('isVisible', false);
// =============================================================================
//
@@ -76,17 +92,71 @@ const hideDebuggerPane = R.assoc('isVisible', false);
export default (state = initialState, action) => {
switch (action.type) {
case SHOW_DEBUGGER_PANEL:
return showDebuggerPane(state);
case HIDE_DEBUGGER_PANEL:
return hideDebuggerPane(state);
case UPLOAD: {
const {
payload,
meta: { status },
} = action;
if (status === UPLOAD_STATUS.STARTED) {
return R.compose(
R.assoc('uploadProgress', 0),
R.assoc('log', []),
R.assoc('uploadLog', [
createSystemMessage(MSG.TRANSPILING),
])
)(state);
}
if (status === UPLOAD_STATUS.PROGRESSED) {
const { message, percentage } = payload;
return R.compose(
R.assoc('uploadProgress', percentage),
overUploadLog(R.append(createSystemMessage(message))),
)(state);
}
if (status === UPLOAD_STATUS.SUCCEEDED) {
const messages = R.compose(
R.append(createSystemMessage(MSG.SUCCES)),
R.map(createFlasherMessage),
R.reject(R.isEmpty),
R.split('\n')
)(payload.message);
return R.compose(
R.assoc('uploadProgress', null),
overUploadLog(R.concat(R.__, messages)),
)(state);
}
if (status === UPLOAD_STATUS.FAILED) {
const messages = R.compose(
R.map(createErrorMessage),
R.reject(R.isEmpty),
R.split('\n')
)(payload.message);
return R.compose(
R.assoc('uploadProgress', null),
overUploadLog(R.concat(R.__, messages)),
showDebuggerPane
)(state);
}
return state;
}
case TOGGLE_DEBUGGER_PANEL:
return R.over(R.lensProp('isVisible'), R.not, state);
case DEBUGGER_LOG_ADD_MESSAGES:
case DEBUGGER_LOG_ADD_MESSAGES: {
const showPanelOnErrorMessages = R.any(R.propEq('type', UPLOAD_MSG_TYPE.ERROR), action.payload)
? showDebuggerPane
: R.identity;
return R.compose(
showPanelOnErrorMessages,
addMessageListToLog(action.payload),
updateWatchNodeValues(action.payload)
)(state);
}
case DEBUGGER_LOG_CLEAR:
return R.assoc('log', [], state);
case DEBUG_SESSION_STARTED:

View File

@@ -1,4 +1,5 @@
import R from 'ramda';
import { Maybe } from 'ramda-fantasy';
import { createSelector } from 'reselect';
import {
getCurrentTabId,
@@ -24,6 +25,11 @@ export const getLog = R.compose(
getDebuggerState
);
export const getUploadLog = R.compose(
R.prop('uploadLog'),
getDebuggerState
);
export const getDebuggerNodeIdsMap = R.compose(
R.prop('nodeIdsMap'),
getDebuggerState
@@ -34,6 +40,12 @@ export const getWatchNodeValues = R.compose(
getDebuggerState
);
export const getUploadProgress = R.compose(
Maybe,
R.prop('uploadProgress'),
getDebuggerState
);
export const getWatchNodeValuesForCurrentPatch = createSelector(
[getCurrentTabId, getWatchNodeValues, getBreadcrumbChunks, getBreadcrumbActiveIndex],
(tabId, nodeValues, chunks, activeIndex) => {

View File

@@ -2,6 +2,8 @@ export default {
isVisible: false,
isRunning: false,
log: [],
uploadLog: [],
nodeIdsMap: {},
watchNodeValues: {},
uploadProgress: null,
};

View File

@@ -184,7 +184,6 @@ class Editor extends React.Component {
/>
) : null;
const DebuggerContainer = (this.props.isDebuggerVisible) ? <Debugger /> : null;
const BreadcrumbsContainer = R.pathEq(['currentTab', 'id'], DEBUGGER_TAB_ID, this.props)
? <Breadcrumbs />
: null;
@@ -214,12 +213,17 @@ class Editor extends React.Component {
onFocus={this.onWorkareaFocus}
>
<Tabs />
{DebugSessionStopButton}
{this.renderOpenedPatchTab()}
{BreadcrumbsContainer}
{this.renderOpenedImplementationEditorTabs()}
{DebuggerContainer}
<SnackBar />
<div className="Workarea-inner">
{DebugSessionStopButton}
{this.renderOpenedPatchTab()}
{BreadcrumbsContainer}
{this.renderOpenedImplementationEditorTabs()}
<SnackBar />
</div>
<Debugger
onUploadClick={this.props.onUploadClick}
onUploadAndDebugClick={this.props.onUploadAndDebugClick}
/>
</FocusTrap>
<Sidebar
id={SIDEBAR_IDS.RIGHT}
@@ -245,13 +249,14 @@ Editor.propTypes = {
implEditorTabs: PropTypes.array,
patchesIndex: PropTypes.object,
isHelpboxVisible: PropTypes.bool,
isDebuggerVisible: PropTypes.bool,
isDebugSessionRunning: PropTypes.bool,
suggesterIsVisible: PropTypes.bool,
suggesterPlacePosition: PropTypes.object,
isLibSuggesterVisible: PropTypes.bool,
defaultNodePosition: PropTypes.object.isRequired,
stopDebuggerSession: PropTypes.func,
onUploadClick: PropTypes.func.isRequired,
onUploadAndDebugClick: PropTypes.func.isRequired,
actions: PropTypes.shape({
updatePatchImplementation: PropTypes.func.isRequired,
closeImplementationEditor: PropTypes.func.isRequired,
@@ -285,7 +290,6 @@ const mapStateToProps = R.applySpec({
suggesterPlacePosition: EditorSelectors.getSuggesterPlacePosition,
isLibSuggesterVisible: EditorSelectors.isLibSuggesterVisible,
isHelpboxVisible: EditorSelectors.isHelpboxVisible,
isDebuggerVisible: DebuggerSelectors.isDebuggerVisible,
isDebugSessionRunning: DebuggerSelectors.isDebugSession,
defaultNodePosition: EditorSelectors.getDefaultNodePlacePosition,
});

View File

@@ -132,7 +132,7 @@ const rawItems = {
},
toggleDebugger: {
key: 'toggleDebugger',
label: 'Toggle Debugger',
label: 'Toggle Deployment Pane',
command: COMMAND.TOGGLE_DEBUGGER,
},
toggleAccountPane: {

View File

@@ -5,6 +5,7 @@ import {
} from 'redux';
import { Provider, connect } from 'react-redux';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Debugger from '../src/debugger/containers/Debugger';
import DebuggerReducer from '../src/debugger/reducer';
@@ -57,6 +58,51 @@ const addError = (store) => {
}]));
};
const addUploadingLog = (store) => {
store.dispatch({
type: 'UPLOAD',
meta: {
status: 'started',
},
});
store.dispatch({
type: 'UPLOAD',
payload: {
message: 'Project was successfully transpiled. Searching for device...',
percentage: 10,
id: 1,
},
meta: {
status: 'progressed',
},
});
store.dispatch({
type: 'UPLOAD',
payload: {
message: 'Port with connected Arduino was found. Installing toolchains...',
percentage: 15,
id: 1,
},
meta: {
status: 'progressed',
},
});
store.dispatch({
type: 'UPLOAD',
payload: {
message: 'Toolchain is installed. Uploading...',
percentage: 30,
id: 1,
},
meta: {
status: 'progressed',
},
});
};
const startDebugSession = (store) => {
store.dispatch(startDebuggerSession({
type: 'system',
@@ -85,7 +131,75 @@ const idle = () => {
storiesOf('Debugger', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('idle', () => (
<Debugger />
<Debugger
onUploadClick={action('onUploadClick')}
onUploadAndDebugClick={action('onUploadAndDebugClick')}
/>
));
};
const uploading = () => {
const store = createStore();
addUploadingLog(store);
storiesOf('Debugger', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('uploading', () => (
<Debugger
onUploadClick={action('onUploadClick')}
onUploadAndDebugClick={action('onUploadAndDebugClick')}
/>
));
};
const uploadingSuccess = () => {
const store = createStore();
addUploadingLog(store);
store.dispatch({
type: 'UPLOAD',
payload: {
message: '\nConnecting to programmer: .\nFound programmer: Id = "CATERIN"; type = S\n Software Version = 1.0; No Hardware Version given.\nProgrammer supports auto addr increment.\nProgrammer supports buffered memory access with buffersize=128 bytes.\n\nProgrammer supports the following devices:\n Device code: 0x44\n\navrdude: AVR device initialized and ready to accept instructions\n\nReading | ################################################## | 100% 0.00s\n\navrdude: Device signature = 0x1e9587 (probably m32u4)\navrdude: reading input file "/Users/user/Library/Application Support/xod-client-electron/upload-temp/build/xod-arduino-sketch.cpp.hex"\navrdude: writing flash (6884 bytes):\n\nWriting | ################################################## | 100% 0.53s\n\navrdude: 6884 bytes of flash written\navrdude: verifying flash memory against /Users/user/Library/Application Support/xod-client-electron/upload-temp/build/xod-arduino-sketch.cpp.hex:\navrdude: load data flash data from input file /Users/user/Library/Application Support/xod-client-electron/upload-temp/build/xod-arduino-sketch.cpp.hex:\navrdude: input file /Users/user/Library/Application Support/xod-client-electron/upload-temp/build/xod-arduino-sketch.cpp.hex contains 6884 bytes\navrdude: reading on-chip flash data:\n\nReading | ################################################## | 100% 0.07s\n\navrdude: verifying ...\navrdude: 6884 bytes of flash verified\n\navrdude done. Thank you.\n\n\n\n',
id: 1,
},
meta: {
status: 'succeeded',
},
});
storiesOf('Debugger', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('uploading sussess', () => (
<Debugger
onUploadClick={action('onUploadClick')}
onUploadAndDebugClick={action('onUploadAndDebugClick')}
/>
));
};
const uploadingFail = () => {
const store = createStore();
addUploadingLog(store);
store.dispatch({
type: 'UPLOAD',
payload: {
message: 'Error occured during uploading: Some horrible stuff happened',
percentage: 30,
id: 1,
},
meta: {
status: 'failed',
},
});
storiesOf('Debugger', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('uploading fail', () => (
<Debugger
onUploadClick={action('onUploadClick')}
onUploadAndDebugClick={action('onUploadAndDebugClick')}
/>
));
};
@@ -100,7 +214,10 @@ const running = () => {
.add('running', () => (
<div>
<LogLength />
<Debugger />
<Debugger
onUploadClick={action('onUploadClick')}
onUploadAndDebugClick={action('onUploadAndDebugClick')}
/>
</div>
));
};
@@ -112,4 +229,7 @@ const running = () => {
// =============================================================================
idle();
uploading();
uploadingSuccess();
uploadingFail();
running();