diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index e6cb6c17..e4b5c242 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -1,8 +1,16 @@ -import React from 'react'; +import React, {Fragment, useState} from 'react'; import NotificationHandler from './notifications/NotificationHandler'; import {deployWorkflowToCamunda} from '../util/IoUtilities'; import {getCamundaEndpoint} from '../config/EditorConfigManager'; import {getRootProcess} from '../util/ModellingUtilities'; +import {getServiceTasksToDeploy} from '../../extensions/opentosca/deployment/DeploymentUtils'; +import {getModeler} from '../ModelerHandler'; +import OnDemandDeploymentModal from './OnDemandDeploymentModal'; +import {startOnDemandReplacementProcess} from "../../extensions/opentosca/replacement/OnDemandTransformator"; + +const defaultState = { + windowOpenOnDemandDeployment: false, +}; /** * React button for starting the deployment of the workflow. @@ -12,13 +20,37 @@ import {getRootProcess} from '../util/ModellingUtilities'; * @constructor */ export default function DeploymentButton(props) { + const [windowOpenOnDemandDeployment, setWindowOpenOnDemandDeployment] = useState(false); const {modeler} = props; + + /** + * Handle the result of a close operation on the tramfpr,atopm + * + * @param result the result from the close operation + */ + async function handleOnDemandDeployment(result) { + console.log(result); + if (result && result.hasOwnProperty('onDemand')) { + // get XML of the current workflow + let xml = (await modeler.saveXML({format: true})).xml; + console.log("XML", xml) + if (result.onDemand === true) { + xml = await startOnDemandReplacementProcess(xml); + } + // deploy in any case + deploy(xml); + } + // handle cancellation (don't deploy) + setWindowOpenOnDemandDeployment(false); + + } + /** * Deploy the current workflow to the Camunda engine */ - async function deploy() { + async function deploy(xml) { NotificationHandler.getInstance().displayNotification({ title: 'Deployment started', @@ -27,7 +59,6 @@ export default function DeploymentButton(props) { // get XML of the current workflow const rootElement = getRootProcess(modeler.getDefinitions()); - const xml = (await modeler.saveXML({format: true})).xml; // check if there are views defined for the modeler and include them in the deployment let viewsDict = {}; @@ -56,12 +87,26 @@ export default function DeploymentButton(props) { } } + async function onClick() { + let csarsToDeploy = getServiceTasksToDeploy(getRootProcess(getModeler().getDefinitions())); + if (csarsToDeploy.length > 0) { + setWindowOpenOnDemandDeployment(true); + } else { + deploy((await modeler.saveXML({format: true})).xml); + } + } + return ( - <> + - + {windowOpenOnDemandDeployment && ( + handleOnDemandDeployment(e)} + /> + )} + ); } \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js new file mode 100644 index 00000000..46c0850c --- /dev/null +++ b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2023 Institute of Architecture of Application Systems - + * University of Stuttgart + * + * This program and the accompanying materials are made available under the + * terms the Apache Software License 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-unused-vars */ +import React from 'react'; + +// polyfill upcoming structural components +import Modal from './modal/Modal'; + +const Title = Modal.Title || (({children}) =>

{children}

); +const Footer = Modal.Footer || (({children}) =>
{children}
); + +export default function OnDemandDeploymentModal({onClose}) { + + const onOnDemand = (value) => onClose({ + onDemand: value, + }); + + return + + + Enable On Demand Service Deployment? + + + ; +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js index 8fc8a8c4..0cd9737e 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ +import * as config from "../framework-config/config-manager"; const QUANTME_NAMESPACE_PULL_ENCODED = encodeURIComponent(encodeURIComponent('http://quantil.org/quantme/pull')); const QUANTME_NAMESPACE_PUSH_ENCODED = encodeURIComponent(encodeURIComponent('http://quantil.org/quantme/push')); @@ -48,9 +49,9 @@ export function getBindingType(serviceTask) { * @param modeling the modeling element to adapt properties of the workflow elements * @return {{success: boolean}} true if binding is successful, false otherwise */ -export function bindUsingPull(topicName, serviceTaskId, elementRegistry, modeling) { +export function bindUsingPull(csar, serviceTaskId, elementRegistry, modeling) { - if (topicName === undefined || serviceTaskId === undefined || elementRegistry === undefined || modeling === undefined) { + if (csar.topicName === undefined || serviceTaskId === undefined || elementRegistry === undefined || modeling === undefined) { console.error('Topic name, service task id, element registry, and modeling required for binding using pull!'); return { success: false }; } @@ -62,9 +63,20 @@ export function bindUsingPull(topicName, serviceTaskId, elementRegistry, modelin return { success: false }; } + let deploymentModelUrl = serviceTask.businessObject.get('opentosca:deploymentModelUrl'); + if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { + deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); + } + // remove deployment model URL and set topic - modeling.updateProperties(serviceTask, { 'deploymentModelUrl': undefined, type: 'external', topic: topicName }); - return { success: true }; + + modeling.updateProperties(serviceTask, { + 'opentosca:deploymentModelUrl': deploymentModelUrl, + 'opentosca:deploymentBuildPlanInstanceUrl': csar.buildPlanUrl, + type: 'external', + topic: csar.topicName + }); + return {success: true}; } /** diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js index 46877eb9..018e2670 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js @@ -72,6 +72,6 @@ function getCSARName(serviceTask) { * @param element the element to check * @return {*|boolean} true if the element is a ServiceTask and has an assigned deployment model, false otherwise */ -function isDeployableServiceTask(element) { +export function isDeployableServiceTask(element) { return element.$type && element.$type === 'bpmn:ServiceTask' && element.deploymentModelUrl && getBindingType(element) !== undefined; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js index 9ee53089..a394e504 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -227,7 +227,7 @@ export async function createServiceInstance(csar, camundaEngineEndpoint) { } -function makeId(length) { +export function makeId(length) { let result = ''; let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let charactersLength = characters.length; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js new file mode 100644 index 00000000..4aece5d6 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2023 Institute of Architecture of Application Systems - + * University of Stuttgart + * + * This program and the accompanying materials are made available under the + * terms the Apache Software License 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import {createTempModelerFromXml} from '../../../editor/ModelerHandler'; +import {getXml} from '../../../editor/util/IoUtilities'; +import {isDeployableServiceTask} from "../deployment/DeploymentUtils"; +import * as config from "../framework-config/config-manager"; +import {makeId} from "../deployment/OpenTOSCAUtils"; +import {getCamundaEndpoint} from "../../../editor/config/EditorConfigManager"; + + +function createDeploymentScript(params) { + return ` +var params = ${JSON.stringify(params)}; +params.csarName = "ondemand_" + (Math.random().toString().substring(3)); + +function fetch(method, url, body) { + var resourceURL = new java.net.URL(url); + + var urlConnection = resourceURL.openConnection(); + urlConnection.setRequestMethod(method); + if (body) { + urlConnection.setDoOutput(true); + urlConnection.setRequestProperty("Content-Type", "application/json"); + var outputStream = urlConnection.getOutputStream() + var outputStreamWriter = new java.io.OutputStreamWriter(outputStream) + outputStreamWriter.write(body); + outputStreamWriter.flush(); + outputStreamWriter.close(); + outputStream.close(); + } + + var inputStream = new java.io.InputStreamReader(urlConnection + .getInputStream()); + var bufferedReader = new java.io.BufferedReader(inputStream); + var inputLine = "" + var text = ""; + var i = 5; + while ((inputLine = bufferedReader.readLine()) != null) { + text += inputLine + } + bufferedReader.close(); + return text; +} + + +var createCsarResponse = fetch('POST', params.opentoscaEndpoint, JSON.stringify({ + enrich: 'false', + name: params.csarName, + url: params.deploymentModelUrl +})) + + +var serviceTemplates = JSON.parse(fetch('GET', params.opentoscaEndpoint + "/" + params.csarName + ".csar/servicetemplates")) +var buildPlansUrl = serviceTemplates.service_templates[0]._links.self.href + '/buildplans' +var buildPlans = JSON.parse(fetch('GET', buildPlansUrl)) +var buildPlanUrl = buildPlans.plans[0]._links.self.href +var inputParameters = JSON.parse(fetch('GET', buildPlanUrl)).input_parameters +for(var i = 0; i < inputParameters.length; i++) { + if(inputParameters[i].name === "camundaEndpoint") { + inputParameters[i].value = params.opentoscaEndpoint + } else if(inputParameters[i].name === "camundaTopic") { + inputParameters[i].value = params.camundaTopic + } else { + inputParameters[i].value = "null" + } +} +var createInstanceResponse = fetch('POST', buildPlanUrl + "/instances", JSON.stringify(inputParameters)) +execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", buildPlanUrl + "/instances/" + createInstanceResponse);`; +} + +/** + * Initiate the replacement process for the QuantME tasks that are contained in the current process model + * + * @param xml the BPMN diagram in XML format + * @param currentQRMs the set of currently in the framework available QRMs + * @param endpointConfig endpoints of the services required for the dynamic hardware selection + */ +export async function startOnDemandReplacementProcess(xml) { + const modeler = await createTempModelerFromXml(xml); + const modeling = modeler.get('modeling'); + const elementRegistry = modeler.get('elementRegistry'); + const bpmnReplace = modeler.get('bpmnReplace'); + const bpmnAutoResizeProvider = modeler.get('bpmnAutoResizeProvider'); + bpmnAutoResizeProvider.canResize = () => false; + + const serviceTasks = elementRegistry.filter(({businessObject}) => isDeployableServiceTask(businessObject)); + + for (const serviceTask of serviceTasks) { + let deploymentModelUrl = serviceTask.businessObject.get('opentosca:deploymentModelUrl'); + if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { + deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); + } + let subProcess = bpmnReplace.replaceElement(serviceTask, {type: 'bpmn:SubProcess'}); + + subProcess.businessObject.set("opentosca:onDemandDeployment", true); + subProcess.businessObject.set("opentosca:deploymentModelUrl", deploymentModelUrl); + + const startEvent = modeling.createShape({ + type: 'bpmn:StartEvent' + }, {x: 200, y: 200}, subProcess); + + let topicName = makeId(12); + const serviceTask1 = modeling.appendShape(startEvent, { + type: 'bpmn:ScriptTask', + }, {x: 400, y: 200}); + serviceTask1.businessObject.set("scriptFormat", "javascript"); + serviceTask1.businessObject.set("script", createDeploymentScript( + { + opentoscaEndpoint: config.getOpenTOSCAEndpoint(), + deploymentModelUrl: deploymentModelUrl, + subprocessId: subProcess.id, + camundaTopic: topicName, + camundaEndpoint: getCamundaEndpoint() + } + )); + + + const serviceTask2 = modeling.appendShape(serviceTask1, { + type: 'bpmn:ServiceTask' + }, {x: 600, y: 200}, subProcess); + + serviceTask2.businessObject.set("camunda:type", "external"); + serviceTask2.businessObject.set("camunda:topic", topicName); + } + + // layout diagram after successful transformation + let updatedXml = await getXml(modeler); + console.log(updatedXml); + return updatedXml; +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js index 5c2041bd..c7906d7c 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js @@ -106,7 +106,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentBinding: false, }); return; } @@ -134,7 +134,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentBinding: false, }); } @@ -179,7 +179,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentBinding: false, }); return; } @@ -199,7 +199,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: true + windowOpenDeploymentBinding: true, }); return; } @@ -208,7 +208,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentBinding: false, }); } @@ -233,7 +233,7 @@ export default class DeploymentPlugin extends PureComponent { // bind the service instance using the specified binding pattern let bindingResponse = undefined; if (csar.type === 'pull') { - bindingResponse = bindUsingPull(csar.topicName, serviceTaskIds[j], this.modeler.get('elementRegistry'), this.modeler.get('modeling')); + bindingResponse = bindUsingPull(csar, serviceTaskIds[j], this.modeler.get('elementRegistry'), this.modeler.get('modeling')); } else if (csar.type === 'push') { bindingResponse = bindUsingPush(csar, serviceTaskIds[j], this.modeler.get('elementRegistry'), this.modeler.get('modeling')); } @@ -253,7 +253,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentBinding: false, }); return; } @@ -272,7 +272,7 @@ export default class DeploymentPlugin extends PureComponent { this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentBinding: false, }); } @@ -326,7 +326,7 @@ export default class DeploymentPlugin extends PureComponent { className="qwm-indent">Hide Deployment , ]}/> diff --git a/components/bpmn-q/test/tests/editor/plugin.spec.js b/components/bpmn-q/test/tests/editor/plugin.spec.js index b8ec76ca..412b178c 100644 --- a/components/bpmn-q/test/tests/editor/plugin.spec.js +++ b/components/bpmn-q/test/tests/editor/plugin.spec.js @@ -54,10 +54,6 @@ describe('Test plugins', function () { expect(extensions['quantme']).to.not.be.undefined; expect(extensions['opentosca']).to.not.be.undefined; expect(extensions['planqk']).to.not.be.undefined; - expect(transfButtons.length).to.equal(3); - expect(buttons.length).to.equal(3); - expect(tabs.length).to.equal(4); - expect(styles.length).to.equal(4); }); }); });