Merge pull request #404 from pybricks/dlech

beta 14
This commit is contained in:
David Lechner
2021-04-12 16:50:42 -05:00
committed by GitHub
9 changed files with 102 additions and 21 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pybricks/pybricks-code",
"version": "1.0.0-beta.13",
"version": "1.0.0-beta.14",
"license": "MIT",
"author": "The Pybricks Authors",
"repository": {
+6 -2
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import { connect } from 'react-redux';
import { RootState } from '../reducers';
@@ -9,7 +9,7 @@ import { downloadAndRun } from './actions';
import { HubRuntimeState } from './reducers';
import runIcon from './run.svg';
type StateProps = Pick<ActionButtonProps, 'enabled'>;
type StateProps = Pick<ActionButtonProps, 'enabled' | 'showProgress' | 'progress'>;
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
type OwnProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
@@ -17,6 +17,9 @@ type OwnProps = Pick<ActionButtonProps, 'id'> &
const mapStateToProps = (state: RootState): StateProps => ({
enabled:
state.editor.current !== null && state.hub.runtime === HubRuntimeState.Idle,
showProgress: state.hub.runtime === HubRuntimeState.Loading,
progress:
state.hub.downloadProgress === null ? undefined : state.hub.downloadProgress,
});
const mapDispatchToProps: DispatchProps = {
@@ -29,6 +32,7 @@ const mergeProps = (
ownProps: OwnProps,
): ActionButtonProps => ({
tooltip: TooltipId.Run,
progressTooltip: TooltipId.RunProgress,
icon: runIcon,
...ownProps,
...stateProps,
+10
View File
@@ -32,6 +32,7 @@ export type HubMessageAction = HubChecksumMessageAction;
export enum HubActionType {
DownloadAndRun = 'hub.action.downloadAndRun',
DidStartDownload = 'hub.action.didStartDownload',
DidProgressDownload = 'hub.action.didProgressDownload',
DidFinishDownload = 'hub.action.didFinishDownload',
DidFailToFinishDownload = 'hub.action.didFailToFinishDownload',
Stop = 'hub.action.stop',
@@ -50,6 +51,14 @@ export function didStartDownload(): HubDidStartDownloadAction {
return { type: HubActionType.DidStartDownload };
}
export type HubDidProgressDownloadAction = Action<HubActionType.DidProgressDownload> & {
progress: number;
};
export function didProgressDownload(progress: number): HubDidProgressDownloadAction {
return { type: HubActionType.DidProgressDownload, progress };
}
export type HubDidFinishDownloadAction = Action<HubActionType.DidFinishDownload>;
export function didFinishDownload(): HubDidFinishDownloadAction {
@@ -80,6 +89,7 @@ export function repl(): HubReplAction {
export type HubAction =
| HubDownloadAndRunAction
| HubDidStartDownloadAction
| HubDidProgressDownloadAction
| HubDidFinishDownloadAction
| HubDidFailToFinishDownloadAction
| HubStopAction
+16 -1
View File
@@ -66,4 +66,19 @@ const runtime: Reducer<HubRuntimeState, Action> = (
}
};
export default combineReducers({ runtime });
const downloadProgress: Reducer<number | null, Action> = (state = null, action) => {
switch (action.type) {
case HubActionType.DidStartDownload:
return 0;
case HubActionType.DidProgressDownload:
return action.progress;
case HubActionType.DidFinishDownload:
return 1;
case HubActionType.DidFailToFinishDownload:
return null;
default:
return state;
}
};
export default combineReducers({ runtime, downloadProgress });
+20 -1
View File
@@ -12,7 +12,14 @@ import {
import { BleUartActionType, BleUartWriteAction, didWrite } from '../ble-uart/actions';
import { MpyActionType, didCompile } from '../mpy/actions';
import { createCountFunc } from '../utils/iter';
import { HubActionType, checksum, downloadAndRun, repl, stop } from './actions';
import {
HubActionType,
HubDidProgressDownloadAction,
checksum,
downloadAndRun,
repl,
stop,
} from './actions';
import hub from './sagas';
jest.mock('ace-builds');
@@ -44,6 +51,11 @@ describe('downloadAndRun', () => {
saga.put(didWrite((writeAction as BleUartWriteAction).id));
saga.put(checksum(30));
// then progress is updated
const progressAction = await saga.take();
expect(progressAction.type).toBe(HubActionType.DidProgressDownload);
expect((progressAction as HubDidProgressDownloadAction).progress).toBe(0);
// then the first chunk of 20 bytes
const writeAction2 = await saga.take();
expect(writeAction2.type).toBe(BleUartActionType.Write);
@@ -51,6 +63,13 @@ describe('downloadAndRun', () => {
saga.put(didWrite((writeAction2 as BleUartWriteAction).id));
saga.put(checksum(0));
// then progress is updated
const progress2Action = await saga.take();
expect(progress2Action.type).toBe(HubActionType.DidProgressDownload);
expect((progress2Action as HubDidProgressDownloadAction).progress).toBe(
20 / 30,
);
// then last chunk
const writeAction3 = await saga.take();
expect(writeAction3.type).toBe(BleUartActionType.Write);
+5 -1
View File
@@ -45,6 +45,7 @@ import {
HubStopAction,
didFailToFinishDownload as didFailToFinishDownload,
didFinishDownload,
didProgressDownload,
didStartDownload,
} from './actions';
@@ -91,6 +92,9 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
// let everyone know the runtime is busy loading the program
yield* put(didStartDownload());
// TODO: show compiled size in UI?
console.log(`Downloading ${mpy.data.byteLength} bytes`);
const checksumChannel = yield* actionChannel<HubChecksumMessageAction>(
HubMessageActionType.Checksum,
);
@@ -138,6 +142,7 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
// we can actually only write 20 bytes at a time
for (let j = 0; j < chunk.length; j += SafeTxCharLength) {
yield* put(didProgressDownload((i + j) / mpy.data.byteLength));
const writeAction = yield* put(
write(nextMessageId(), chunk.slice(j, j + SafeTxCharLength)),
);
@@ -147,7 +152,6 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
yield* put(didFailToFinishDownload());
return;
}
// TODO: dispatch progress
}
const { checksumAction, checksumTimeout } = yield* race({
+38 -13
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import {
Button,
@@ -8,6 +8,7 @@ import {
HotkeysTarget,
Intent,
Position,
Spinner,
Tooltip,
} from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
@@ -23,10 +24,16 @@ export interface ActionButtonProps {
readonly keyboardShortcut?: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: TooltipId;
/** Tooltip text that appears when hovering over the button and @showProgress is true. */
readonly progressTooltip?: TooltipId;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
readonly enabled?: boolean;
/** When true, show progress indicator instead of icon. */
readonly showProgress?: boolean;
/** The progress value (0 to 1) or undefined for indeterminate progress. */
readonly progress?: number;
/** Callback that is called when the button is activated (clicked). */
readonly onAction: () => void;
}
@@ -38,10 +45,28 @@ class ActionButton extends React.Component<Props> {
private buttonRef: React.RefObject<Button> = React.createRef();
render(): JSX.Element {
let tooltipText = this.props.i18n.translate(this.props.tooltip);
if (this.props.keyboardShortcut) {
tooltipText += ` (${this.props.keyboardShortcut})`;
}
const {
i18n,
id,
icon,
keyboardShortcut,
enabled,
tooltip,
progressTooltip,
showProgress,
progress,
onAction,
} = this.props;
const tooltipText =
showProgress && progressTooltip
? i18n.translate(progressTooltip, {
percent:
progress === undefined ? '' : i18n.formatPercentage(progress),
})
: i18n.translate(tooltip) +
(keyboardShortcut ? ` (${keyboardShortcut})` : '');
return (
<Tooltip
content={tooltipText}
@@ -52,16 +77,16 @@ class ActionButton extends React.Component<Props> {
ref={this.buttonRef}
intent={Intent.PRIMARY}
onMouseDown={(e) => e.preventDefault()} // prevent focus
onClick={(): void => this.props.onAction()}
disabled={this.props.enabled === false}
onClick={(): void => onAction()}
disabled={enabled === false}
className="no-box-shadow"
style={
this.props.enabled === false
? { pointerEvents: 'none' }
: undefined
}
style={enabled === false ? { pointerEvents: 'none' } : undefined}
>
<img src={this.props.icon} alt={this.props.id} />
{showProgress ? (
<Spinner value={progress} intent={Intent.PRIMARY} />
) : (
<img src={icon} alt={id} />
)}
</Button>
</Tooltip>
);
+4 -1
View File
@@ -2,7 +2,10 @@
"open": { "tooltip": "Open file" },
"saveAs": { "tooltip": "Download file" },
"stop": { "tooltip": "Stop everything" },
"run": { "tooltip": "Download and run this program" },
"run": {
"action": { "tooltip": "Download and run this program" },
"progress": { "tooltip": "Downloading… {percent}" }
},
"repl": { "tooltip": "Start REPL in terminal" },
"bluetooth": {
"connect": { "tooltip": "Connect using Bluetooth" },
+2 -1
View File
@@ -6,7 +6,8 @@
export enum TooltipId {
Open = 'open.tooltip',
SaveAs = 'saveAs.tooltip',
Run = 'run.tooltip',
Run = 'run.action.tooltip',
RunProgress = 'run.progress.tooltip',
Stop = 'stop.tooltip',
Repl = 'repl.tooltip',
Flash = 'flash.action.tooltip',