diff --git a/packages/browser-repl/src/components/shell-input.spec.tsx b/packages/browser-repl/src/components/shell-input.spec.tsx index 07b611d83b..3235a5761e 100644 --- a/packages/browser-repl/src/components/shell-input.spec.tsx +++ b/packages/browser-repl/src/components/shell-input.spec.tsx @@ -5,7 +5,7 @@ import { shallow, mount } from '../../testing/enzyme'; import { ShellInput } from './shell-input'; import { Editor } from './editor'; -import Loader from './shell-loader'; +import ShellLoader from './shell-loader'; function changeValue(wrapper, value): void { wrapper.find(Editor).prop('onChange')(value); @@ -117,7 +117,7 @@ describe('', () => { operationInProgress />); - expect(wrapper.find(Loader).exists()).to.equal(true); + expect(wrapper.find(ShellLoader).exists()).to.equal(true); }); it('does not show a loader when operationInProgress is false', () => { @@ -126,7 +126,7 @@ describe('', () => { operationInProgress={false} />); - expect(wrapper.find(Loader).exists()).to.equal(false); + expect(wrapper.find(ShellLoader).exists()).to.equal(false); }); }); diff --git a/packages/browser-repl/src/components/shell-input.tsx b/packages/browser-repl/src/components/shell-input.tsx index aea51f9374..2bc465c0bc 100644 --- a/packages/browser-repl/src/components/shell-input.tsx +++ b/packages/browser-repl/src/components/shell-input.tsx @@ -3,7 +3,7 @@ import { Autocompleter } from '@mongosh/browser-runtime-core'; import classnames from 'classnames'; import React, { Component } from 'react'; import { Editor } from './editor'; -import Loader from './shell-loader'; +import ShellLoader from './shell-loader'; import { LineWithIcon } from './utils/line-with-icon'; const styles = require('./shell-input.less'); @@ -109,9 +109,7 @@ export class ShellInput extends Component { render(): JSX.Element { let prompt: JSX.Element; if (this.props.operationInProgress) { - prompt = (); + prompt = (); } else if (this.props.prompt) { const trimmed = this.props.prompt.trim(); if (trimmed.endsWith('>')) { diff --git a/packages/browser-repl/src/components/shell-loader.less b/packages/browser-repl/src/components/shell-loader.less index 316c291eb7..be74fdde32 100644 --- a/packages/browser-repl/src/components/shell-loader.less +++ b/packages/browser-repl/src/components/shell-loader.less @@ -1,14 +1,15 @@ @import '~@leafygreen-ui/palette/dist/ui-colors.less'; .shell-loader { - border: 2px solid @leafygreen__gray--light-3; - border-top: 2px solid @leafygreen__green--base; + border: 2px solid transparent; + border-top: 2px solid @leafygreen__green--light-2; border-radius: 50%; padding: 0; margin: 0; box-sizing: border-box; + display: inline-block; - animation: shell-loader-spin 500ms linear infinite; + animation: shell-loader-spin 700ms ease infinite; } @keyframes shell-loader-spin { diff --git a/packages/browser-repl/src/components/shell-loader.tsx b/packages/browser-repl/src/components/shell-loader.tsx index 240f52e672..e8cef397a9 100644 --- a/packages/browser-repl/src/components/shell-loader.tsx +++ b/packages/browser-repl/src/components/shell-loader.tsx @@ -4,19 +4,31 @@ import classnames from 'classnames'; const styles = require('./shell-loader.less'); interface ShellLoaderProps { - size: number; + className: string; + size?: string; } export default class ShellLoader extends Component { + static defaultProps = { + className: '', + size: '12px' + }; + render(): JSX.Element { - const { size } = this.props; + const { + className, + size + } = this.props; return (
); diff --git a/packages/browser-repl/src/components/shell.spec.tsx b/packages/browser-repl/src/components/shell.spec.tsx index cc6158546a..99364f6617 100644 --- a/packages/browser-repl/src/components/shell.spec.tsx +++ b/packages/browser-repl/src/components/shell.spec.tsx @@ -17,6 +17,8 @@ const wait: (ms?: number) => Promise = (ms = 10) => { describe('', () => { let onOutputChangedSpy; let onHistoryChangedSpy; + let onOperationStartedSpy; + let onOperationEndSpy; let fakeRuntime; let wrapper: ShallowWrapper | ReactWrapper; let scrollIntoView; @@ -40,11 +42,16 @@ describe('', () => { onOutputChangedSpy = sinon.spy(); onHistoryChangedSpy = sinon.spy(); + onOperationStartedSpy = sinon.spy(); + onOperationEndSpy = sinon.spy(); wrapper = shallow(); + onHistoryChanged={onHistoryChangedSpy} + onOperationStarted={onOperationStartedSpy} + onOperationEnd={onOperationEndSpy} + />); }); afterEach(() => { @@ -215,6 +222,14 @@ describe('', () => { await onInput('db.createUser()'); expect(wrapper.state('history')).to.deep.equal([]); }); + + it('calls onOperationStarted', async() => { + expect(onOperationStartedSpy).to.have.been.calledOnce; + }); + + it('calls onOperationEnd', async() => { + expect(onOperationEndSpy).to.have.been.calledOnce; + }); }); context('when empty input is entered', () => { @@ -316,6 +331,10 @@ describe('', () => { it('calls onHistoryChanged', () => { expect(onHistoryChangedSpy).to.have.been.calledOnceWith(['some code']); }); + + it('calls onOperationEnd', async() => { + expect(onOperationEndSpy).to.have.been.calledOnce; + }); }); it('scrolls the container to the bottom each time the output is updated', () => { diff --git a/packages/browser-repl/src/components/shell.tsx b/packages/browser-repl/src/components/shell.tsx index 7117825c9a..b7a9047ab9 100644 --- a/packages/browser-repl/src/components/shell.tsx +++ b/packages/browser-repl/src/components/shell.tsx @@ -40,6 +40,14 @@ interface ShellProps { */ maxHistoryLength: number; + /* A function called when an operation has begun. + */ + onOperationStarted: () => void; + + /* A function called when an operation has completed (both error and success). + */ + onOperationEnd: () => void; + /* An array of entries to be displayed in the output area. * * Can be used to restore the output between sessions, or to setup @@ -68,9 +76,7 @@ interface ShellState { shellPrompt: string; } -const noop = (): void => { - // -}; +const noop = (): void => { /* */ }; /** * The browser-repl Shell component @@ -78,6 +84,8 @@ const noop = (): void => { export class Shell extends Component { static defaultProps = { onHistoryChanged: noop, + onOperationStarted: noop, + onOperationEnd: noop, onOutputChanged: noop, maxOutputLength: 1000, maxHistoryLength: 1000, @@ -114,6 +122,8 @@ export class Shell extends Component { let outputLine: ShellOutputEntry; try { + this.props.onOperationStarted(); + this.props.runtime.setEvaluationListener(this); const result = await this.props.runtime.evaluate(code); outputLine = { @@ -128,6 +138,7 @@ export class Shell extends Component { }; } finally { await this.updateShellPrompt(); + this.props.onOperationEnd(); } return outputLine; diff --git a/packages/browser-repl/src/index.tsx b/packages/browser-repl/src/index.tsx index 3e8cff0dfd..107ad45d89 100644 --- a/packages/browser-repl/src/index.tsx +++ b/packages/browser-repl/src/index.tsx @@ -1,2 +1,5 @@ +import ShellLoader from './components/shell-loader'; + export { Shell } from './components/shell'; export { IframeRuntime } from './iframe-runtime'; +export { ShellLoader }; diff --git a/packages/compass-shell/src/components/compass-shell/compass-shell.jsx b/packages/compass-shell/src/components/compass-shell/compass-shell.jsx index e8ee3bd1b5..80c456650c 100644 --- a/packages/compass-shell/src/components/compass-shell/compass-shell.jsx +++ b/packages/compass-shell/src/components/compass-shell/compass-shell.jsx @@ -45,7 +45,8 @@ export class CompassShell extends Component { this.state = { initialHistory: this.props.historyStorage ? null : [], - isExpanded: !!this.props.isExpanded + isExpanded: !!this.props.isExpanded, + isOperationInProgress: false }; } @@ -57,6 +58,18 @@ export class CompassShell extends Component { this.shellOutput = output; } + onOperationStarted = () => { + this.setState({ + isOperationInProgress: true + }); + } + + onOperationEnd = () => { + this.setState({ + isOperationInProgress: false + }); + } + lastOpenHeight = defaultShellHeightOpened; resizableRef = null; @@ -123,7 +136,8 @@ export class CompassShell extends Component { */ render() { const { - isExpanded + isExpanded, + isOperationInProgress } = this.state; if (!this.props.runtime || !this.state.initialHistory) { @@ -154,20 +168,25 @@ export class CompassShell extends Component { - {isExpanded && ( -
- -
- )} +
+ +
); diff --git a/packages/compass-shell/src/components/compass-shell/compass-shell.less b/packages/compass-shell/src/components/compass-shell/compass-shell.less index 0b121a8dca..a170dfa410 100644 --- a/packages/compass-shell/src/components/compass-shell/compass-shell.less +++ b/packages/compass-shell/src/components/compass-shell/compass-shell.less @@ -12,8 +12,12 @@ &-shell-container { flex-grow: 1; - display: flex; + display: none; overflow: auto; border-top: 1px solid @leafygreen__gray--dark-2; + + &-visible { + display: flex; + } } } diff --git a/packages/compass-shell/src/components/compass-shell/compass-shell.spec.js b/packages/compass-shell/src/components/compass-shell/compass-shell.spec.js index 31ceacece3..315772f300 100644 --- a/packages/compass-shell/src/components/compass-shell/compass-shell.spec.js +++ b/packages/compass-shell/src/components/compass-shell/compass-shell.spec.js @@ -8,6 +8,7 @@ import { CompassShell } from './compass-shell'; import ResizeHandle from '../resize-handle'; import ShellHeader from '../shell-header'; import InfoModal from '../info-modal'; +import styles from './compass-shell.less'; function updateAndWaitAsync(wrapper) { wrapper.update(); @@ -16,10 +17,13 @@ function updateAndWaitAsync(wrapper) { describe('CompassShell', () => { context('when the prop isExpanded is false', () => { - it('does not render a shell', () => { + it('has the shell display none', () => { const fakeRuntime = {}; - const wrapper = shallow(); - expect(wrapper.find(Shell).exists()).to.equal(false); + const wrapper = shallow(); + expect(wrapper.find(`.${styles['compass-shell-shell-container-visible']}`).exists()).to.equal(false); }); context('when is it expanded', () => { @@ -59,6 +63,7 @@ describe('CompassShell', () => { const fakeRuntime = {}; const wrapper = shallow(); expect(wrapper.find(Shell).prop('runtime')).to.equal(fakeRuntime); + expect(wrapper.find(`.${styles['compass-shell-shell-container-visible']}`).exists()).to.equal(true); }); it('renders the ShellHeader component', () => { diff --git a/packages/compass-shell/src/components/shell-header/shell-header.jsx b/packages/compass-shell/src/components/shell-header/shell-header.jsx index d9be5fde84..22aaaa8b9c 100644 --- a/packages/compass-shell/src/components/shell-header/shell-header.jsx +++ b/packages/compass-shell/src/components/shell-header/shell-header.jsx @@ -1,9 +1,11 @@ import IconButton from '@leafygreen-ui/icon-button'; import Icon from '@leafygreen-ui/icon'; import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { ShellLoader } from '@mongosh/browser-repl'; + import { SET_SHOW_INFO_MODAL } from '../../modules/info-modal'; import styles from './shell-header.less'; @@ -11,6 +13,7 @@ import styles from './shell-header.less'; export class ShellHeader extends Component { static propTypes = { isExpanded: PropTypes.bool.isRequired, + isOperationInProgress: PropTypes.bool.isRequired, onShellToggleClicked: PropTypes.func.isRequired, showInfoModal: PropTypes.func.isRequired }; @@ -23,38 +26,58 @@ export class ShellHeader extends Component { render() { const { isExpanded, + isOperationInProgress, onShellToggleClicked, showInfoModal } = this.props; return (
- +
+ +
{isExpanded && ( - + <> - + - + - + )} {!isExpanded && ( - + )}
diff --git a/packages/compass-shell/src/components/shell-header/shell-header.less b/packages/compass-shell/src/components/shell-header/shell-header.less index 4cb409120d..a6be6e8440 100644 --- a/packages/compass-shell/src/components/shell-header/shell-header.less +++ b/packages/compass-shell/src/components/shell-header/shell-header.less @@ -6,28 +6,52 @@ display: flex; color: @leafygreen__gray--light-1; + &-left { + flex-grow: 1; + } + &-right-actions { - position: absolute; - right: 4px; - top: -2px; - text-align: right; + display: flex; } &-toggle { background: none; border: none; cursor: pointer; - padding: 1px 8px; + padding: 0px 8px; + height: 100%; + display: flex; + vertical-align: middle; + flex-direction: row; + align-items: center; + margin: auto 0; + font-weight: bold; + font-size: 12px; + line-height: 24px; transition: all 200ms; user-select: none; + text-transform: uppercase; + &:hover { color: @leafygreen__gray--light-3; } } - &-info-btn { - margin-right: 8px; + &-operation-in-progress { + color: @leafygreen__green--light-2; + } + + &-loader-icon { + margin: auto; + margin-left: 16px; + margin-right: 6px; + } + + &-btn { + margin-right: 4px; + width: 24px; + height: 24px; } } diff --git a/packages/compass-shell/src/components/shell-header/shell-header.spec.js b/packages/compass-shell/src/components/shell-header/shell-header.spec.js index 99477adac0..bf9e49555e 100644 --- a/packages/compass-shell/src/components/shell-header/shell-header.spec.js +++ b/packages/compass-shell/src/components/shell-header/shell-header.spec.js @@ -3,54 +3,74 @@ import { mount, shallow } from 'enzyme'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; +import { ShellLoader } from '@mongosh/browser-repl'; + import { ShellHeader } from './shell-header'; import styles from './shell-header.less'; describe('ShellHeader', () => { context('when isExpanded prop is true', () => { - it('renders a close chevron button', () => { - const wrapper = mount( { + wrapper = mount( {}} showInfoModal={() => {}} />); + }); + it('renders a close chevron button', () => { expect(wrapper.find(IconButton).exists()).to.equal(true); expect(wrapper.find(Icon).at(1).prop('glyph')).to.equal('ChevronDown'); }); it('renders an info button', () => { - const wrapper = mount( {}} - showInfoModal={() => {}} - />); - expect(wrapper.find(IconButton).exists()).to.equal(true); expect(wrapper.find(Icon).at(0).prop('glyph')).to.equal('InfoWithCircle'); }); it('renders an actions area', () => { - const wrapper = mount( { + expect(wrapper.find(ShellLoader).exists()).to.equal(false); + }); + }); + + context('when isExpanded prop is false', () => { + let wrapper; + beforeEach(() => { + wrapper = mount( {}} showInfoModal={() => {}} />); + }); - expect(wrapper.find(`.${styles['compass-shell-header-right-actions']}`).exists()).to.equal(true); + it('renders an open chevron button', () => { + expect(wrapper.find(IconButton).exists()).to.equal(true); + expect(wrapper.find(Icon).prop('glyph')).to.equal('ChevronUp'); + }); + + it('does not render the loader', () => { + expect(wrapper.find(ShellLoader).exists()).to.equal(false); }); }); - context('when isExpanded prop is false', () => { - it('renders an open chevron button', () => { + context('when isExpanded is false and isOperationInProgress is true', () => { + it('renders the loader', () => { const wrapper = mount( {}} showInfoModal={() => {}} />); - expect(wrapper.find(IconButton).exists()).to.equal(true); - expect(wrapper.find(Icon).prop('glyph')).to.equal('ChevronUp'); + expect(wrapper.find(ShellLoader).exists()).to.equal(true); }); }); @@ -58,6 +78,7 @@ describe('ShellHeader', () => { it('has a button to toggle the container', async() => { const wrapper = shallow( {}} showInfoModal={() => {}} />);