From 8cffa6bdb675252fa61e667f749deb0e0ab6c702 Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Fri, 23 Nov 2018 13:10:45 +0100 Subject: [PATCH] feat(deploy-toolbar): add modal for deployment * add ModalConductor and modal related methods to App component * add DeployDiagramModal to handle diagram deployment * connect BPMN and DMN editors to deployment feature --- client/src/app/App.js | 30 ++++- .../app/__tests__/DeployDiagramModalSpec.js | 54 +++++++++ .../DeployDiagramModal.js | 84 +++++++++++++ .../app/deploy-diagram-modal/ErrorMessage.js | 12 ++ .../deploy-diagram-modal/ErrorMessage.less | 13 ++ .../src/app/deploy-diagram-modal/Loading.js | 7 ++ .../src/app/deploy-diagram-modal/Loading.less | 14 +++ .../src/app/deploy-diagram-modal/Success.js | 12 ++ .../src/app/deploy-diagram-modal/Success.less | 13 ++ client/src/app/deploy-diagram-modal/View.js | 113 ++++++++++++++++++ client/src/app/deploy-diagram-modal/View.less | 84 +++++++++++++ client/src/app/deploy-diagram-modal/index.js | 1 + client/src/app/modals/ModalConductor.js | 18 +++ client/src/app/modals/index.js | 1 + .../primitives/__tests__/ModalWrapperSpec.js | 62 ++++++++++ client/src/app/primitives/index.js | 3 +- .../src/app/primitives/modal/ModalWrapper.js | 28 +++++ .../app/primitives/modal/ModalWrapper.less | 34 ++++++ client/src/app/primitives/modal/index.js | 1 + client/src/app/tabs/MultiSheetTab.js | 4 +- client/src/app/tabs/bpmn/BpmnEditor.js | 5 +- .../app/tabs/bpmn/__tests__/BpmnEditorSpec.js | 4 +- client/src/app/tabs/dmn/DmnEditor.js | 5 +- .../app/tabs/dmn/__tests__/DmnEditorSpec.js | 4 +- 24 files changed, 593 insertions(+), 13 deletions(-) create mode 100644 client/src/app/__tests__/DeployDiagramModalSpec.js create mode 100644 client/src/app/deploy-diagram-modal/DeployDiagramModal.js create mode 100644 client/src/app/deploy-diagram-modal/ErrorMessage.js create mode 100644 client/src/app/deploy-diagram-modal/ErrorMessage.less create mode 100644 client/src/app/deploy-diagram-modal/Loading.js create mode 100644 client/src/app/deploy-diagram-modal/Loading.less create mode 100644 client/src/app/deploy-diagram-modal/Success.js create mode 100644 client/src/app/deploy-diagram-modal/Success.less create mode 100644 client/src/app/deploy-diagram-modal/View.js create mode 100644 client/src/app/deploy-diagram-modal/View.less create mode 100644 client/src/app/deploy-diagram-modal/index.js create mode 100644 client/src/app/modals/ModalConductor.js create mode 100644 client/src/app/modals/index.js create mode 100644 client/src/app/primitives/__tests__/ModalWrapperSpec.js create mode 100644 client/src/app/primitives/modal/ModalWrapper.js create mode 100644 client/src/app/primitives/modal/ModalWrapper.less create mode 100644 client/src/app/primitives/modal/index.js diff --git a/client/src/app/App.js b/client/src/app/App.js index a9e9320e9b..fe34d980d6 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -19,6 +19,8 @@ import Log from './Log'; import debug from 'debug'; +import { ModalConductor } from './modals'; + import { Button, DropdownButton, @@ -57,7 +59,8 @@ const INITIAL_STATE = { layout: {}, tabs: [], tabState: {}, - logEntries: [] + logEntries: [], + currentModal: null }; @@ -1202,6 +1205,14 @@ export class App extends Component { return this.showDialog(options); } + if (action === 'open-modal') { + return this.setModal(options); + } + + if (action === 'close-modal') { + return this.setModal(null); + } + if (action === 'open-external-url') { this.openExternalUrl(options); } @@ -1219,6 +1230,16 @@ export class App extends Component { this.props.globals.backend.send('external:open-url', options); } + openModal = modal => this.triggerAction('open-modal', modal); + + closeModal = () => this.triggerAction('close-modal'); + + setModal = currentModal => this.setState({ currentModal }); + + handleDeploy = options => { + return this.props.globals.backend.send('deploy', { ...options, file: this.state.activeTab.file }); + }; + quit() { return true; } @@ -1356,6 +1377,7 @@ export class App extends Component { onLayoutChanged={ this.handleLayoutChanged } onContextMenu={ this.openTabMenu } onAction={ this.triggerAction } + onModal={ this.openModal } ref={ this.tabRef } /> } @@ -1369,6 +1391,12 @@ export class App extends Component { onClear={ this.clearLog } /> + + ); } diff --git a/client/src/app/__tests__/DeployDiagramModalSpec.js b/client/src/app/__tests__/DeployDiagramModalSpec.js new file mode 100644 index 0000000000..d805119584 --- /dev/null +++ b/client/src/app/__tests__/DeployDiagramModalSpec.js @@ -0,0 +1,54 @@ +/* global sinon */ + +import React from 'react'; + +import { + shallow +} from 'enzyme'; + +import { DeployDiagramModal } from '../deploy-diagram-modal'; + + +describe('', function() { + + it('should render', function() { + shallow(); + }); + + + it('should set state.error to error message when onDeploy throws error', async function() { + // given + const errorMessage = 'error'; + + const onDeployStub = sinon.stub().throws({ message: errorMessage }); + + const wrapper = shallow(); + const instance = wrapper.instance(); + + // when + await instance.handleDeploy(new Event('click')); + + // expect + expect(instance.state.error).to.be.equal(errorMessage); + }); + + + it('should set state.success to success message when onDeploy resolves', async function() { + // given + const endpointUrl = 'http://example.com'; + const successMessage = `Successfully deployed diagram to ${endpointUrl}`; + + const onDeployStub = sinon.stub().resolves(); + + const wrapper = shallow(); + const instance = wrapper.instance(); + instance.state.endpointUrl = endpointUrl; + + // when + await instance.handleDeploy(new Event('click')); + + // expect + expect(instance.state.success).to.be.equal(successMessage); + }); + +}); diff --git a/client/src/app/deploy-diagram-modal/DeployDiagramModal.js b/client/src/app/deploy-diagram-modal/DeployDiagramModal.js new file mode 100644 index 0000000000..821c0eeb4e --- /dev/null +++ b/client/src/app/deploy-diagram-modal/DeployDiagramModal.js @@ -0,0 +1,84 @@ +import React from 'react'; + +import View from './View'; + + +const defaultState = { + isLoading: false, + success: '', + error: '', + endpointUrl: '', + tenantId: '', + deploymentName: '' +}; + +class DeployDiagramModal extends React.Component { + constructor() { + super(); + + this.state = defaultState; + } + + handleDeploy = async (event) => { + event.preventDefault(); + + this.setState({ + success: '', + error: '', + isLoading: true + }); + + const payload = this.getPayloadFromState(); + + try { + await this.props.onDeploy(payload); + + this.setState({ + isLoading: false, + success: `Successfully deployed diagram to ${payload.endpointUrl}`, + error: '' + }); + } catch (error) { + this.setState({ + isLoading: false, + success: '', + error: error.message + }); + } + } + + handleEndpointUrlChange = event => this.setState({ endpointUrl: event.target.value }); + + handleTenantIdChange = event => this.setState({ tenantId: event.target.value }); + + handleDeploymentNameChange = event => this.setState({ deploymentName: event.target.value }); + + render() { + return ; + } + + getPayloadFromState() { + const payload = { + endpointUrl: this.state.endpointUrl, + deploymentName: this.state.deploymentName, + tenantId: this.state.tenantId + }; + + return payload; + } +} + +export default DeployDiagramModal; diff --git a/client/src/app/deploy-diagram-modal/ErrorMessage.js b/client/src/app/deploy-diagram-modal/ErrorMessage.js new file mode 100644 index 0000000000..18e39c7f09 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/ErrorMessage.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import css from './ErrorMessage.less'; + + +const ErrorMessage = ({ message }) => ( +
+ Error: { message } +
+); + +export default ErrorMessage; diff --git a/client/src/app/deploy-diagram-modal/ErrorMessage.less b/client/src/app/deploy-diagram-modal/ErrorMessage.less new file mode 100644 index 0000000000..51763f5a82 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/ErrorMessage.less @@ -0,0 +1,13 @@ +:local(.ErrorMessage) { + border: solid 1px; + border-radius: 4px; + padding: 8px 10px; + + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +:local(.ErrorContent) { + user-select: text; +} diff --git a/client/src/app/deploy-diagram-modal/Loading.js b/client/src/app/deploy-diagram-modal/Loading.js new file mode 100644 index 0000000000..59fa7599f8 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/Loading.js @@ -0,0 +1,7 @@ +import React from 'react'; + +import { Loading as style } from './Loading.less'; + +import { Icon } from '../primitives'; + +export default () => ; diff --git a/client/src/app/deploy-diagram-modal/Loading.less b/client/src/app/deploy-diagram-modal/Loading.less new file mode 100644 index 0000000000..2ed6154077 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/Loading.less @@ -0,0 +1,14 @@ +:local(.Loading) { + animation: spin 2s infinite linear; + display: inline-block; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(359deg); + } + } +} diff --git a/client/src/app/deploy-diagram-modal/Success.js b/client/src/app/deploy-diagram-modal/Success.js new file mode 100644 index 0000000000..2853d73ac2 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/Success.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import css from './Success.less'; + + +const Success = ({ message }) => ( +
+ Success: { message } +
+); + +export default Success; diff --git a/client/src/app/deploy-diagram-modal/Success.less b/client/src/app/deploy-diagram-modal/Success.less new file mode 100644 index 0000000000..3ed7b7b95e --- /dev/null +++ b/client/src/app/deploy-diagram-modal/Success.less @@ -0,0 +1,13 @@ +:local(.Success) { + border: solid 1px; + border-radius: 4px; + padding: 8px 10px; + + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +:local(.SuccessContent) { + user-select: text; +} diff --git a/client/src/app/deploy-diagram-modal/View.js b/client/src/app/deploy-diagram-modal/View.js new file mode 100644 index 0000000000..4c7c09d5d8 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/View.js @@ -0,0 +1,113 @@ +import React from 'react'; + +import { ModalWrapper } from '../primitives'; + +import ErrorMessage from './ErrorMessage'; +import Loading from './Loading'; +import Success from './Success'; + +import { View as style } from './View.less'; + + +const View = (props) => { + const { + error, + isLoading, + success + } = props; + + return ( + +

Deploy Diagram

+ +

+ Specify deployment details and deploy this diagram to Camunda. +

+ + { isLoading && } + + { success && } + + { error && } + + + +
+ +
+ +
+ +
+ + +
+ This should point to the /deployment/create endpoint + inside your Camunda REST API. +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ ); +}; + +export default View; diff --git a/client/src/app/deploy-diagram-modal/View.less b/client/src/app/deploy-diagram-modal/View.less new file mode 100644 index 0000000000..3b0c125162 --- /dev/null +++ b/client/src/app/deploy-diagram-modal/View.less @@ -0,0 +1,84 @@ +:local(.View) { + user-select: none; + + .intro { + margin-bottom: 30px; + } + + h2 { + font-weight: normal; + } + + .ca-form { + + font-size: 13.3333px; + margin: 15px 0; + + display: grid; + grid-template-columns: max-content auto; + + grid-column-gap: 10px; + grid-row-gap: 15px; + + align-items: baseline; + + + .form-row { + margin: 15px; + } + + input[type="text"] { + width: 100%; + padding: 6px; + + font-size: inherit; + } + + label { + font-weight: bolder; + line-height: 30px; + text-align: right; + width: 100%; + display: inline-block; + } + + input[type="text"] { + border: 1px solid rgb(204, 204, 204); + + &:focus, + &:hover:not(:disabled) { + outline: none; + border-color: rgb(82, 178, 21); + } + + &:focus { + box-shadow: 0 0 1px 2px rgba(82, 178, 21, 0.2); + } + } + + button + button { + margin-left: 10px; + } + + button { + padding: 6px 10px; + } + + button:disabled, + input:disabled { + color: #808080; + } + + input[type="text"]:invalid:not(:focus) { + border-color: rgb(255, 0, 0); + } + + + .hint { + margin-top: 10px; + color: rgb(102, 102, 102); + } + + } +} + diff --git a/client/src/app/deploy-diagram-modal/index.js b/client/src/app/deploy-diagram-modal/index.js new file mode 100644 index 0000000000..4fdd8fc3de --- /dev/null +++ b/client/src/app/deploy-diagram-modal/index.js @@ -0,0 +1 @@ +export { default as DeployDiagramModal } from './DeployDiagramModal'; diff --git a/client/src/app/modals/ModalConductor.js b/client/src/app/modals/ModalConductor.js new file mode 100644 index 0000000000..258e2aa34d --- /dev/null +++ b/client/src/app/modals/ModalConductor.js @@ -0,0 +1,18 @@ +import React from 'react'; + +import { DeployDiagramModal } from '../deploy-diagram-modal'; + + +const DEPLOY_DIAGRAM = 'DEPLOY_DIAGRAM'; + + +const ModalConductor = props => { + switch (props.currentModal) { + case DEPLOY_DIAGRAM: + return ; + default: + return null; + } +}; + +export default ModalConductor; diff --git a/client/src/app/modals/index.js b/client/src/app/modals/index.js new file mode 100644 index 0000000000..6237fab91a --- /dev/null +++ b/client/src/app/modals/index.js @@ -0,0 +1 @@ +export { default as ModalConductor } from './ModalConductor'; diff --git a/client/src/app/primitives/__tests__/ModalWrapperSpec.js b/client/src/app/primitives/__tests__/ModalWrapperSpec.js new file mode 100644 index 0000000000..4f80e79e33 --- /dev/null +++ b/client/src/app/primitives/__tests__/ModalWrapperSpec.js @@ -0,0 +1,62 @@ +/* global sinon */ + +import React from 'react'; + +import { + mount, + shallow +} from 'enzyme'; + +import { ModalWrapper } from '..'; + + +describe('', function() { + + it('should render', function() { + shallow(); + }); + + + it('should render children', function() { + const wrapper = shallow(( + +
+ { 'Test' } +
+
+ )); + + expect(wrapper.contains(
{ 'Test' }
)).to.be.true; + }); + + + it('should invoke passed onClose prop for background click', function() { + // given + const onCloseSpy = sinon.spy(); + const wrapper = mount(); + + // when + wrapper.first().simulate('click'); + + // then + expect(onCloseSpy).to.be.called; + + wrapper.unmount(); + }); + + + it('should NOT invoke passed onClose prop for click on modal container', function() { + // given + const onCloseSpy = sinon.spy(); + const wrapper = mount(); + + // when + wrapper.find('div div').first().simulate('click'); + + // then + expect(onCloseSpy).to.not.be.called; + + wrapper.unmount(); + }); + +}); diff --git a/client/src/app/primitives/index.js b/client/src/app/primitives/index.js index 7c7efc0cda..c66fed8696 100644 --- a/client/src/app/primitives/index.js +++ b/client/src/app/primitives/index.js @@ -4,4 +4,5 @@ export { Icon } from './icon'; export { default as Loader } from './Loader'; export { default as TabLinks } from './TabLinks'; export { default as TabContainer } from './TabContainer'; -export { default as Tab } from './Tab'; \ No newline at end of file +export { default as Tab } from './Tab'; +export { ModalWrapper } from './modal'; diff --git a/client/src/app/primitives/modal/ModalWrapper.js b/client/src/app/primitives/modal/ModalWrapper.js new file mode 100644 index 0000000000..422fd98cab --- /dev/null +++ b/client/src/app/primitives/modal/ModalWrapper.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import css from './ModalWrapper.less'; + + +const ModalWrapper = props => { + const handleBackgroundClick = event => { + if (event.target === event.currentTarget) { + props.onClose(); + } + }; + + return ( +
+
+ { props.children } +
+
+ ); +}; + +ModalWrapper.defaultProps = { + onClose: () => {} +}; + +export default ModalWrapper; diff --git a/client/src/app/primitives/modal/ModalWrapper.less b/client/src/app/primitives/modal/ModalWrapper.less new file mode 100644 index 0000000000..e234e4dd4a --- /dev/null +++ b/client/src/app/primitives/modal/ModalWrapper.less @@ -0,0 +1,34 @@ +:local(.ModalOverlay) { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + + &:before { + content: ''; + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + background: #666; + opacity: .2; + z-index: 1001; + } +} + +:local(.ModalContainer) { + position: fixed; + margin: 10% auto; + left: 0; + right: 0; + max-width: 600px; + height: auto; + background: white; + box-shadow: 0 1px 4px rgba(0,0,0,0.3); + border-radius: 2px; + padding: 20px; + z-index: 1001; +} \ No newline at end of file diff --git a/client/src/app/primitives/modal/index.js b/client/src/app/primitives/modal/index.js new file mode 100644 index 0000000000..2cd50b39be --- /dev/null +++ b/client/src/app/primitives/modal/index.js @@ -0,0 +1 @@ +export { default as ModalWrapper } from './ModalWrapper'; diff --git a/client/src/app/tabs/MultiSheetTab.js b/client/src/app/tabs/MultiSheetTab.js index 2acf10daab..61383eaee0 100644 --- a/client/src/app/tabs/MultiSheetTab.js +++ b/client/src/app/tabs/MultiSheetTab.js @@ -319,7 +319,9 @@ export class MultiSheetTab extends CachedComponent { onContentUpdated={ onContentUpdated } onError={ this.handleError } onImport={ this.handleImport } - onLayoutChanged={ this.handleLayoutChanged } /> + onLayoutChanged={ this.handleLayoutChanged } + onModal={ this.props.onModal } + /> console.log('Deploy Current Diagram') - }, { - text: 'Configure Deployment Endpoint', - onClick: () => console.log('Configure Deployment Endpoint') + onClick: this.props.onModal.bind(null, 'DEPLOY_DIAGRAM') }] }> diff --git a/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js b/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js index 48e6b7d9a3..bba0a2c413 100644 --- a/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js +++ b/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js @@ -517,7 +517,8 @@ function renderEditor(xml, options = {}) { onContentUpdated, onError, onImport, - onLayoutChanged + onLayoutChanged, + onModal } = options; const slotFillRoot = mount( @@ -531,6 +532,7 @@ function renderEditor(xml, options = {}) { onImport={ onImport || noop } onLayoutChanged={ onLayoutChanged || noop } onContentUpdated={ onContentUpdated || noop } + onModal={ onModal || noop } cache={ options.cache || new Cache() } layout={ layout || { minimap: { diff --git a/client/src/app/tabs/dmn/DmnEditor.js b/client/src/app/tabs/dmn/DmnEditor.js index 21c606f8b0..c7ecc9c17c 100644 --- a/client/src/app/tabs/dmn/DmnEditor.js +++ b/client/src/app/tabs/dmn/DmnEditor.js @@ -482,10 +482,7 @@ export class DmnEditor extends CachedComponent { console.log('Deploy Current Diagram') - }, { - text: 'Configure Deployment Endpoint', - onClick: () => console.log('Configure Deployment Endpoint') + onClick: this.props.onModal.bind(null, 'DEPLOY_DIAGRAM') }] }> diff --git a/client/src/app/tabs/dmn/__tests__/DmnEditorSpec.js b/client/src/app/tabs/dmn/__tests__/DmnEditorSpec.js index e8f31b40c7..4def50de0f 100644 --- a/client/src/app/tabs/dmn/__tests__/DmnEditorSpec.js +++ b/client/src/app/tabs/dmn/__tests__/DmnEditorSpec.js @@ -405,7 +405,8 @@ function renderEditor(xml, options = {}) { onChanged, onError, onImport, - onLayoutChanged + onLayoutChanged, + onModal } = options; const slotFillRoot = mount( @@ -417,6 +418,7 @@ function renderEditor(xml, options = {}) { onError={ onError || noop } onImport={ onImport || noop } onLayoutChanged={ onLayoutChanged || noop } + onModal={ onModal || noop } cache={ options.cache || new Cache() } layout={ layout || { minimap: {