From 1442ef86c70c215bc273132011100dc6aeeafcdc Mon Sep 17 00:00:00 2001 From: Mike Cousins Date: Tue, 1 Oct 2019 17:57:12 -0400 Subject: [PATCH] feat(app): show spinner while robot logs are downloading Known limitation: UI will only allow you to be downloading logs from one robot at a time --- app-shell/package.json | 1 + app-shell/src/robot-logs.js | 20 +++++-- .../RobotSettings/AdvancedSettingsCard.js | 13 +++-- app/src/shell/index.js | 30 ++--------- app/src/shell/robot-logs/actions.js | 20 +++++++ app/src/shell/robot-logs/reducer.js | 23 ++++++++ app/src/shell/robot-logs/selectors.js | 6 +++ app/src/shell/robot-logs/types.js | 13 +++++ app/src/shell/types.js | 13 ++--- flow-typed/npm/electron-dl_vx.x.x.js | 33 ++++++++++++ yarn.lock | 54 +++++++++++++++++++ 11 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 app/src/shell/robot-logs/actions.js create mode 100644 app/src/shell/robot-logs/reducer.js create mode 100644 app/src/shell/robot-logs/selectors.js create mode 100644 app/src/shell/robot-logs/types.js create mode 100644 flow-typed/npm/electron-dl_vx.x.x.js diff --git a/app-shell/package.json b/app-shell/package.json index 90d0a05e9ee..6f60fd7ffc7 100644 --- a/app-shell/package.json +++ b/app-shell/package.json @@ -29,6 +29,7 @@ "dateformat": "^3.0.3", "electron-debug": "^3.0.1", "electron-devtools-installer": "^2.2.4", + "electron-dl": "^1.14.0", "electron-store": "^4.0.0", "electron-updater": "^4.1.2", "form-data": "^2.5.0", diff --git a/app-shell/src/robot-logs.js b/app-shell/src/robot-logs.js index 3db0aef8da5..16c4916fa1f 100644 --- a/app-shell/src/robot-logs.js +++ b/app-shell/src/robot-logs.js @@ -1,12 +1,24 @@ // download robot logs manager +import { download } from 'electron-dl' +import createLogger from './log' + +const log = createLogger(__dirname) + export function registerRobotLogs(dispatch, mainWindow) { return function handleIncomingAction(action) { if (action.type === 'shell:DOWNLOAD_LOGS') { - const { - payload: { logUrls }, - } = action - logUrls.forEach(url => mainWindow.webContents.downloadURL(url)) + const { logUrls } = action.payload + + log.debug('Downloading robot logs', { logUrls }) + + logUrls + .reduce( + (result, url) => result.then(() => download(mainWindow, url)), + Promise.resolve() + ) + .catch(error => log.error('Error downloading robot logs', { error })) + .then(() => dispatch({ type: 'shell:DOWNLOAD_LOGS_DONE' })) } } } diff --git a/app/src/components/RobotSettings/AdvancedSettingsCard.js b/app/src/components/RobotSettings/AdvancedSettingsCard.js index bfd033a9c46..e7e21aa6ecb 100644 --- a/app/src/components/RobotSettings/AdvancedSettingsCard.js +++ b/app/src/components/RobotSettings/AdvancedSettingsCard.js @@ -11,13 +11,15 @@ import { } from '../../robot-api' import { CONNECTABLE } from '../../discovery' -import { downloadLogs } from '../../shell' +import { downloadLogs } from '../../shell/robot-logs/actions' +import { getRobotLogsDownloading } from '../../shell/robot-logs/selectors' import { Portal } from '../portal' import { AlertModal, Card, LabeledButton, LabeledToggle, + Icon, } from '@opentrons/components' import type { State, Dispatch } from '../../types' @@ -53,6 +55,7 @@ export default function AdvancedSettingsCard(props: Props) { const settings = useSelector(state => getRobotSettingsState(state, name) ) + const robotLogsDownloading = useSelector(getRobotLogsDownloading) const dispatch = useDispatch() const disabled = status !== CONNECTABLE const logsAvailable = health && health.logs @@ -72,8 +75,12 @@ export default function AdvancedSettingsCard(props: Props) { + ) : ( + 'Download' + ), + disabled: disabled || !logsAvailable || robotLogsDownloading, onClick: () => dispatch(downloadLogs(robot)), }} > diff --git a/app/src/shell/index.js b/app/src/shell/index.js index caafae8218d..3906d3869d4 100644 --- a/app/src/shell/index.js +++ b/app/src/shell/index.js @@ -9,19 +9,10 @@ import createLogger from '../logger' import remote from './remote' import { updateReducer } from './update' import { buildrootReducer, buildrootUpdateEpic } from './buildroot' +import { robotLogsReducer } from './robot-logs/reducer' import type { Reducer } from 'redux' - -import type { - LooseEpic, - ThunkAction, - Action, - ActionLike, - Dispatch, - GetState, -} from '../types' - -import type { ViewableRobot } from '../discovery' +import type { LooseEpic, Action, ActionLike } from '../types' import type { ShellState } from './types' const { ipcRenderer, CURRENT_VERSION, CURRENT_RELEASE_NOTES } = remote @@ -40,6 +31,7 @@ export const shellReducer: Reducer = combineReducers< >({ update: updateReducer, buildroot: buildrootReducer, + robotLogs: robotLogsReducer, }) export const sendActionToShellEpic: LooseEpic = action$ => @@ -71,19 +63,3 @@ export const shellEpic = combineEpics( receiveActionFromShellEpic, buildrootUpdateEpic ) - -export function downloadLogs(robot: ViewableRobot): ThunkAction { - return (dispatch: Dispatch, getState: GetState) => { - const logPaths = robot.health && robot.health.logs - - if (logPaths) { - const logUrls = logPaths.map(p => `http://${robot.ip}:${robot.port}${p}`) - - dispatch({ - type: 'shell:DOWNLOAD_LOGS', - payload: { logUrls }, - meta: { shell: true }, - }) - } - } -} diff --git a/app/src/shell/robot-logs/actions.js b/app/src/shell/robot-logs/actions.js new file mode 100644 index 00000000000..98c2482d5ee --- /dev/null +++ b/app/src/shell/robot-logs/actions.js @@ -0,0 +1,20 @@ +// @flow + +import type { ThunkAction } from '../../types' +import type { ViewableRobot } from '../../discovery/types' + +export function downloadLogs(robot: ViewableRobot): ThunkAction { + return (dispatch, getState) => { + const logPaths = robot.health && robot.health.logs + + if (logPaths) { + const logUrls = logPaths.map(p => `http://${robot.ip}:${robot.port}${p}`) + + dispatch({ + type: 'shell:DOWNLOAD_LOGS', + payload: { logUrls }, + meta: { shell: true }, + }) + } + } +} diff --git a/app/src/shell/robot-logs/reducer.js b/app/src/shell/robot-logs/reducer.js new file mode 100644 index 00000000000..ce04a0f27e5 --- /dev/null +++ b/app/src/shell/robot-logs/reducer.js @@ -0,0 +1,23 @@ +// @flow + +import type { Action } from '../../types' +import type { RobotLogsState } from './types' + +const INITIAL_STATE: RobotLogsState = { downloading: false } + +export function robotLogsReducer( + state: RobotLogsState = INITIAL_STATE, + action: Action +): RobotLogsState { + switch (action.type) { + case 'shell:DOWNLOAD_LOGS': { + return { ...state, downloading: true } + } + + case 'shell:DOWNLOAD_LOGS_DONE': { + return { ...state, downloading: false } + } + } + + return state +} diff --git a/app/src/shell/robot-logs/selectors.js b/app/src/shell/robot-logs/selectors.js new file mode 100644 index 00000000000..f83c0a7d9eb --- /dev/null +++ b/app/src/shell/robot-logs/selectors.js @@ -0,0 +1,6 @@ +// @flow +import type { State } from '../../types' + +export function getRobotLogsDownloading(state: State): boolean { + return state.shell.robotLogs.downloading +} diff --git a/app/src/shell/robot-logs/types.js b/app/src/shell/robot-logs/types.js new file mode 100644 index 00000000000..abb36d53544 --- /dev/null +++ b/app/src/shell/robot-logs/types.js @@ -0,0 +1,13 @@ +// @flow + +export type RobotLogsState = $ReadOnly<{| + downloading: boolean, +|}> + +export type RobotLogsAction = + | {| + type: 'shell:DOWNLOAD_LOGS', + payload: {| logUrls: Array |}, + meta: {| shell: true |}, + |} + | {| type: 'shell:DOWNLOAD_LOGS_DONE' |} diff --git a/app/src/shell/types.js b/app/src/shell/types.js index 3ecd56502f7..c2dc9228c20 100644 --- a/app/src/shell/types.js +++ b/app/src/shell/types.js @@ -2,6 +2,7 @@ import type { Service } from '@opentrons/discovery-client' import type { Config } from '../config/types' import type { BuildrootState, BuildrootAction } from './buildroot/types' +import type { RobotLogsState, RobotLogsAction } from './robot-logs/types' import type { ShellUpdateState, ShellUpdateAction } from './update' export type Remote = {| @@ -15,15 +16,7 @@ export type Remote = {| export type ShellState = {| update: ShellUpdateState, buildroot: BuildrootState, + robotLogs: RobotLogsState, |} -export type ShellLogsDownloadAction = {| - type: 'shell:DOWNLOAD_LOGS', - payload: {| logUrls: Array |}, - meta: {| shell: true |}, -|} - -export type ShellAction = - | ShellUpdateAction - | ShellLogsDownloadAction - | BuildrootAction +export type ShellAction = ShellUpdateAction | BuildrootAction | RobotLogsAction diff --git a/flow-typed/npm/electron-dl_vx.x.x.js b/flow-typed/npm/electron-dl_vx.x.x.js new file mode 100644 index 00000000000..30b21a956dd --- /dev/null +++ b/flow-typed/npm/electron-dl_vx.x.x.js @@ -0,0 +1,33 @@ +// flow-typed signature: 852e7f6cd7dfbd9f03aa1c8f180f4bf1 +// flow-typed version: <>/electron-dl_v1.14.0/flow_v0.106.2 + +/** + * This is an autogenerated libdef stub for: + * + * 'electron-dl' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'electron-dl' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ + + +// Filename aliases +declare module 'electron-dl/index' { + declare module.exports: $Exports<'electron-dl'>; +} +declare module 'electron-dl/index.js' { + declare module.exports: $Exports<'electron-dl'>; +} diff --git a/yarn.lock b/yarn.lock index 5ed2abcd95e..1b8808a8210 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6155,6 +6155,15 @@ electron-devtools-installer@^2.2.4: rimraf "^2.5.2" semver "^5.3.0" +electron-dl@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-1.14.0.tgz#1466f1b945664ca3d784268307c2b935728177bf" + integrity sha512-4okyei42a1mLsvLK7hLrIfd20EQzB18nIlLTwBV992aMSmTGLUEFRTmO1MfSslGNrzD8nuPuy1l/VxO8so4lig== + dependencies: + ext-name "^5.0.0" + pupa "^1.0.0" + unused-filename "^1.0.0" + electron-download@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" @@ -6996,6 +7005,21 @@ express@^4.16.3, express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext-list@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" + integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== + dependencies: + mime-db "^1.28.0" + +ext-name@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" + integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== + dependencies: + ext-list "^2.0.0" + sort-keys-length "^1.0.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -11110,6 +11134,11 @@ mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +mime-db@^1.28.0: + version "1.42.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" + integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== + mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" @@ -11349,6 +11378,11 @@ mkdirp@0.5.0: dependencies: minimist "0.0.8" +modify-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1" + integrity sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE= + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -13769,6 +13803,11 @@ punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" +pupa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-1.0.0.tgz#9a9568a5af7e657b8462a6e9d5328743560ceff6" + integrity sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y= + puppeteer@^1.8.0: version "1.19.0" resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.19.0.tgz#e3b7b448c2c97933517078d7a2c53687361bebea" @@ -15743,6 +15782,13 @@ socks@~2.3.2: ip "^1.1.5" smart-buffer "4.0.2" +sort-keys-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" + integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg= + dependencies: + sort-keys "^1.0.0" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -17212,6 +17258,14 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unused-filename@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-1.0.0.tgz#d340880f71ae2115ebaa1325bef05cc6684469c6" + integrity sha1-00CID3GuIRXrqhMlvvBcxmhEacY= + dependencies: + modify-filename "^1.1.0" + path-exists "^3.0.0" + unzipper@^0.9.3: version "0.9.15" resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.9.15.tgz#97d99203dad17698ee39882483c14e4845c7549c"