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 (
- <>
+
deploy()}>
+ onClick={() => onClick()}>
Deploy Workflow
- >
+ {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
,
this.setState({windowOpenDeploymentOverview: true})}>
+ onClick={() => this.setState({windowOpenDeploymentDeploymentOverview: true})}>
Service 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);
});
});
});