From 39dd32bb9241faba605bf0fd2b9c9a2b48a72584 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 12 Jun 2023 10:30:50 +0200 Subject: [PATCH 01/33] Refactor deployment processing in own plugin refs: #2 refs: #14 --- components/bpmn-q/karma.conf.js | 4 +- .../editor/plugin/PluginHandler.js | 4 + .../palette/DataFlowPaletteProvider.js | 1 - .../extensions/opentosca/OpenToscaPlugin.js | 27 ++ .../configTabs/OpenToscaTab.js | 0 .../deployment/BindingUtils.js | 0 .../deployment/DeploymentUtils.js | 0 .../opentosca/deployment/OpenTOSCAUtils.js | 236 ++++++++++++++++++ .../framework-config/config-manager.js | 73 ++++++ .../opentosca/framework-config/config.js | 17 ++ .../opentosca/framework-config/index.js | 14 ++ .../extensions/opentosca/modeling/index.js | 16 ++ .../properties-provider}/Deployment.js | 19 +- .../DmnImplementationProps.js | 2 +- .../ImplementationProps.js | 6 +- .../ImplementationTypeProps.js | 12 +- .../ServiceTaskPropertiesProvider.js | 81 ++++++ .../opentosca/resources/QuantME_Logo.svg | 1 + .../opentosca/resources/config-icon.png | Bin 0 -> 572 bytes .../extensions/opentosca/resources/info.png | Bin 0 -> 443 bytes .../opentosca/resources/opentosca4bpmn.json | 23 ++ .../resources/service-deployment-icon.png | Bin .../resources/service-deployment-icon.svg | 0 .../opentosca/styling/opentosca.css | 106 ++++++++ .../deployment/services/DeploymentPlugin.js | 0 .../services/ServiceDeploymentBindingModal.js | 0 .../services/ServiceDeploymentInputModal.js | 0 .../ServiceDeploymentOverviewModal.js | 0 .../opentosca/utilities/Utilities.js | 33 +++ .../extensions/quantme/QuantMEPlugin.js | 6 - .../quantme/deployment/OpenTOSCAUtils.js | 211 ---------------- .../framework-config/config-manager.js | 50 ---- .../quantme/framework-config/config.js | 2 - .../QuantMEPropertiesProvider.js | 37 --- .../quantme/resources/quantum4bpmn.json | 11 - .../extensions/quantme/styling/quantme.css | 11 - .../quantme/ui/QuantMEPluginButton.js | 4 +- .../quantme/ui/control/QuantMEController.js | 107 +------- .../ImplementationTypeHelperExtension.js | 2 +- components/bpmn-q/public/index.html | 3 + .../bpmn-q/test/tests/editor/plugin.spec.js | 18 +- .../test/tests/helpers/DiagramHelper.js | 2 +- .../tests/opentosca/deployment-utils.spec.js | 32 +++ .../tests/opentosca/opentosca-config.spec.js | 32 +++ .../test/tests/quantme/quantme-config.spec.js | 4 - components/bpmn-q/webpack.config.js | 3 +- 46 files changed, 730 insertions(+), 480 deletions(-) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/configTabs/OpenToscaTab.js (100%) rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/deployment/BindingUtils.js (100%) rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/deployment/DeploymentUtils.js (100%) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js rename components/bpmn-q/modeler-component/extensions/{quantme/modeling/properties-provider/service-task => opentosca/modeling/properties-provider}/Deployment.js (77%) rename components/bpmn-q/modeler-component/extensions/{quantme/modeling/properties-provider/service-task => opentosca/modeling/properties-provider}/DmnImplementationProps.js (98%) rename components/bpmn-q/modeler-component/extensions/{quantme/modeling/properties-provider/service-task => opentosca/modeling/properties-provider}/ImplementationProps.js (96%) rename components/bpmn-q/modeler-component/extensions/{quantme/modeling/properties-provider/service-task => opentosca/modeling/properties-provider}/ImplementationTypeProps.js (94%) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/QuantME_Logo.svg create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/config-icon.png create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/info.png create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/resources/service-deployment-icon.png (100%) rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/resources/service-deployment-icon.svg (100%) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/ui/deployment/services/DeploymentPlugin.js (100%) rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/ui/deployment/services/ServiceDeploymentBindingModal.js (100%) rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/ui/deployment/services/ServiceDeploymentInputModal.js (100%) rename components/bpmn-q/modeler-component/extensions/{quantme => opentosca}/ui/deployment/services/ServiceDeploymentOverviewModal.js (100%) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js create mode 100644 components/bpmn-q/test/tests/opentosca/deployment-utils.spec.js create mode 100644 components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js diff --git a/components/bpmn-q/karma.conf.js b/components/bpmn-q/karma.conf.js index eb1c219d..459dd532 100644 --- a/components/bpmn-q/karma.conf.js +++ b/components/bpmn-q/karma.conf.js @@ -17,12 +17,14 @@ module.exports = function (config) { 'test/tests/editor/editor.spec.js', 'test/tests/editor/plugin.spec.js', 'test/tests/planqk/planqk-transformation.spec.js', - 'test/tests/quantme/quantme-transformation.spec.js', 'test/tests/editor/utils/modelling-util.spec.js', 'test/tests/qhana/qhana-plugin-config.spec.js', 'test/tests/qhana/qhana-service-configs.spec.js', + 'test/tests/quantme/quantme-transformation.spec.js', 'test/tests/quantme/data-object-configs.spec.js', 'test/tests/quantme/quantme-config.spec.js', + 'test/tests/opentosca/opentosca-config.spec.js', + 'test/tests/opentosca/deployment-utils.spec.js', 'test/tests/dataflow/data-flow-transformation.spec.js', 'test/tests/dataflow/data-flow-plugin-config.spec.js', 'test/tests/dataflow/data-flow-configurations-endpoint.spec.js', diff --git a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js index 2b45efb1..af20f6fd 100644 --- a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js +++ b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js @@ -1,5 +1,6 @@ import PlanQKPlugin from "../../extensions/planqk/PlanQKPlugin"; import QuantMEPlugin from "../../extensions/quantme/QuantMEPlugin"; +import OpenToscaPlugin from "../../extensions/opentosca/OpenToscaPlugin"; import DataFlowPlugin from '../../extensions/data-extension/DataFlowPlugin'; import QHAnaPlugin from '../../extensions/qhana/QHAnaPlugin'; import {getAllConfigs} from "./PluginConfigHandler"; @@ -16,6 +17,7 @@ const PLUGINS = [ QHAnaPlugin, PlanQKPlugin, QuantMEPlugin, + OpenToscaPlugin ]; // list of currently active plugins in the current running instance of the modeler, defined based on the plugin configuration @@ -63,6 +65,8 @@ export function checkEnabledStatus(pluginName) { return process.env.ENABLE_QHANA_PLUGIN; case 'quantme': return process.env.ENABLE_QUANTME_PLUGIN; + case 'opentosca': + return process.env.ENABLE_OPENTOSCA_PLUGIN; } } /** diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js b/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js index 64e22b3d..dcbac638 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js @@ -7,7 +7,6 @@ import * as consts from '../Constants'; export default class DataFlowPaletteProvider { constructor(bpmnFactory, create, elementFactory, palette, translate) { - this.bpmnFactory = bpmnFactory; this.create = create; this.elementFactory = elementFactory; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js new file mode 100644 index 00000000..5b37ef8e --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js @@ -0,0 +1,27 @@ +import React from "react"; + +import OpenToscaTab from "./configTabs/OpenToscaTab"; + +import opentoscaStyles from './styling/opentosca.css'; +import DeploymentPlugin from "./ui/deployment/services/DeploymentPlugin"; +import OpenToscaExtensionModule from "./modeling"; +let openToscaModdleExtension = require('./resources/opentosca4bpmn.json'); + + +/** + * Plugin Object of the OpenTOSCA extension. Used to register the plugin in the plugin handler of the modeler. + */ +export default { + buttons: [], + configTabs: [ + { + tabId: 'OpenTOSCAEndpointTab', + tabTitle: 'OpenTOSCA', + configTab: OpenToscaTab, + } + ], + extensionModule: OpenToscaExtensionModule, + moddleDescription: openToscaModdleExtension, + name: 'opentosca', + styling: [opentoscaStyles] +}; \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/quantme/configTabs/OpenToscaTab.js b/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenToscaTab.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/configTabs/OpenToscaTab.js rename to components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenToscaTab.js diff --git a/components/bpmn-q/modeler-component/extensions/quantme/deployment/BindingUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/deployment/BindingUtils.js rename to components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js diff --git a/components/bpmn-q/modeler-component/extensions/quantme/deployment/DeploymentUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/deployment/DeploymentUtils.js rename to components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js new file mode 100644 index 00000000..38dd157c --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2021 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 {fetch} from 'whatwg-fetch'; +import {performAjax} from '../utilities/Utilities'; + +/** + * Upload the CSAR located at the given URL to the connected OpenTOSCA Container and return the corresponding URL and required input parameters + * + * @param opentoscaEndpoint the endpoint of the OpenTOSCA Container + * @param csarName the name of the CSAR to upload + * @param url the URL pointing to the CSAR + * @param wineryEndpoint the endpoint of the Winery containing the CSAR to upload + */ +export async function uploadCSARToContainer(opentoscaEndpoint, csarName, url, wineryEndpoint) { + + if (opentoscaEndpoint === undefined) { + console.error('OpenTOSCA endpoint is undefined. Unable to upload CSARs...'); + return {success: false}; + } + + try { + if (url.startsWith('{{ wineryEndpoint }}')) { + url = url.replace('{{ wineryEndpoint }}', wineryEndpoint); + } + console.log('Checking if CSAR at following URL is already uploaded to the OpenTOSCA Container: ', url); + + // check if CSAR is already uploaded + let getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); + + if (!getCSARResult.success) { + console.log('CSAR is not yet uploaded. Uploading...'); + + let body = { + enrich: 'false', + name: csarName, + url: url + }; + + // upload the CSAR + await fetch(opentoscaEndpoint, { + method: 'POST', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json'} + }); + + // check successful upload and retrieve corresponding url + getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); + } + + if (!getCSARResult.success) { + console.error('Uploading CSAR failed!'); + return {success: false}; + } + + // retrieve input parameters for the build plan + let buildPlanResult = await fetch(getCSARResult.url); + let buildPlanResultJson = await buildPlanResult.json(); + + return {success: true, url: getCSARResult.url, inputParameters: buildPlanResultJson.input_parameters}; + } catch (e) { + console.error('Error while uploading CSAR: ' + e); + return {success: false}; + } +} + +/** + * Get the link to the build plan of the CSAR with the given name if it is uploaded to the OpenTOSCA Container + * + * @param opentoscaEndpoint the endpoint of the OpenTOSCA Container + * @param csarName the name of the csar + * @return the status whether the given CSAR is uploaded and the corresponding build plan link if available + */ +async function getBuildPlanForCSAR(opentoscaEndpoint, csarName) { + + // get all currently deployed CSARs + let response = await fetch(opentoscaEndpoint); + let responseJson = await response.json(); + + let deployedCSARs = responseJson.csars; + if (deployedCSARs === undefined) { + + // no CSARs available + return {success: false}; + } + + for (let i = 0; i < deployedCSARs.length; i++) { + let deployedCSAR = deployedCSARs[i]; + if (deployedCSAR.id === csarName) { + console.log('Found uploaded CSAR with id: %s', csarName); + let url = deployedCSAR._links.self.href; + + // retrieve the URl to the build plan required to get the input parameters and to instantiate the CSAR + return getBuildPlanUrl(url); + } + } + + // unable to find CSAR + return {success: false}; +} + +/** + * Get the URL to the build plan of the given CSAR + * + * @param csarUrl the URL to a CSAR + * @return the URL to the build plan for the given CSAR + */ +async function getBuildPlanUrl(csarUrl) { + + let response = await fetch(csarUrl + '/servicetemplates'); + let responseJson = await response.json(); + + if (!responseJson.service_templates || responseJson.service_templates.length !== 1) { + console.error('Unable to find service template in CSAR at URL: %s', csarUrl); + return {success: false}; + } + + let buildPlansUrl = responseJson.service_templates[0]._links.self.href + '/buildplans'; + response = await fetch(buildPlansUrl); + responseJson = await response.json(); + + if (!responseJson.plans || responseJson.plans.length !== 1) { + console.error('Unable to find build plan at URL: %s', buildPlansUrl); + return {success: false}; + } + + return {success: true, url: responseJson.plans[0]._links.self.href}; +} + +/** + * Create an instance of the ServiceTemplate contained in the given CSAR + * + * @param csar the details about the CSAR to create an instance from the contained ServiceTemplate + * @param camundaEngineEndpoint the endpoint of the Camunda engine to bind services using the pulling pattern + * @return the result of the instance creation (success, endpoint, topic on which the service listens, ...) + */ +export async function createServiceInstance(csar, camundaEngineEndpoint) { + + let result = {success: false}; + + let inputParameters = csar.inputParameters; + if (csar.type === 'pull') { + + // get special parameters that are required to bind services using external tasks / the pulling pattern + let camundaTopicParam = inputParameters.find((param) => param.name === 'camundaTopic'); + let camundaEndpointParam = inputParameters.find((param) => param.name === 'camundaEndpoint'); + + // abort if parameters are not available + if (camundaTopicParam === undefined || camundaEndpointParam === undefined) { + console.error('Unable to pass topic to poll to service instance creation. Service binding will fail!'); + return result; + } + + // generate topic for the binding + let topicName = makeId(12); + + camundaTopicParam.value = topicName; + camundaEndpointParam.value = camundaEngineEndpoint; + result.topicName = topicName; + } + + // trigger instance creation + let instanceCreationResponse = await fetch(csar.buildPlanUrl + '/instances', { + method: 'POST', + body: JSON.stringify(inputParameters), + headers: {'Content-Type': 'application/json'} + }); + let instanceCreationResponseJson = await instanceCreationResponse.json(); + + // wait for the service instance to be created + await new Promise(r => setTimeout(r, 5000)); + + // get service template instance to poll for completness + let buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); + let buildPlanResponseJson = await buildPlanResponse.json(); + + // retry polling 10 times, creation of the build time takes some time + for (let retry = 0; retry < 10; retry++) { + + // stop retries in case of correct response + if (buildPlanResponseJson._links) { + break; + } + + await new Promise(r => setTimeout(r, 5000)); + + console.log('Retry fetching build plan'); + + buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); + buildPlanResponseJson = await buildPlanResponse.json(); + } + + if (!buildPlanResponseJson._links) { + console.log('Unable to fetch build plans for ' + csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); + result.success = false; + return result; + } + + let pollingUrl = buildPlanResponseJson._links.service_template_instance.href; + + let state = 'CREATING'; + console.log('Polling for finished service instance at URL: %s', pollingUrl); + while (!(state === 'CREATED' || state === 'FAILED')) { + + // wait 5 seconds for next poll + await new Promise(r => setTimeout(r, 5000)); + // poll for current state + let pollingResponse = await fetch(pollingUrl); + let pollingResponseJson = await pollingResponse.json(); + console.log('Polling response: ', pollingResponseJson); + + state = pollingResponseJson.state; + } + + result.success = true; + return result; +} + + +function makeId(length) { + let result = ''; + let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js new file mode 100644 index 00000000..849efc1c --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2021 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 defaultConfig from "./config"; +import {getPluginConfig} from '../../../editor/plugin/PluginConfigHandler'; + +let config = {}; + +/** + * Get the endpoint of the configured OpenTOSCA container + * + * @return {string} the currently specified endpoint of the OpenTOSCA container + */ +export function getOpenTOSCAEndpoint() { + if (config.opentoscaEndpoint === undefined) { + setOpenTOSCAEndpoint( + getPluginConfig('opentosca').opentoscaEndpoint + || defaultConfig.opentoscaEndpoint); + } + return config.opentoscaEndpoint; +} + +/** + * Set the endpoint of the OpenTOSCA container + * + * @param opentoscaEndpoint the endpoint of the OpenTOSCA container + */ +export function setOpenTOSCAEndpoint(opentoscaEndpoint) { + if (opentoscaEndpoint !== null && opentoscaEndpoint !== undefined) { + config.opentoscaEndpoint = opentoscaEndpoint.replace(/\/$/, ''); + } +} + +/** + * Get the endpoint of the configured Winery + * + * @return {string} the currently specified endpoint of the Winery + */ +export function getWineryEndpoint() { + if (config.wineryEndpoint === undefined) { + setWineryEndpoint( + getPluginConfig('opentosca').wineryEndpoint + || defaultConfig.wineryEndpoint); + } + return config.wineryEndpoint; +} + +/** + * Set the endpoint of the Winery + * + * @param wineryEndpoint the endpoint of the Winery + */ +export function setWineryEndpoint(wineryEndpoint) { + if (wineryEndpoint !== null && wineryEndpoint !== undefined) { + config.wineryEndpoint = wineryEndpoint.replace(/\/$/, ''); + } +} + +/** + * Reset all saved endpoints and configuration values back to default or the value of the respective plugin config + * by setting this.comfig to an empty js object. + */ +export function resetConfig() { + config = {}; +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js new file mode 100644 index 00000000..3adab154 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2021 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 + */ + +// takes either the environment variables or the default values definded in webpack.config +const defaultConfig = { + opentoscaEndpoint: process.env.OPENTOSCA_ENDPOINT, + wineryEndpoint: process.env.WINERY_ENDPOINT, +}; +export default defaultConfig; \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js new file mode 100644 index 00000000..c9a1d7e1 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2021 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 * as configManager from './config-manager'; + +const config = configManager; +export default config; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js new file mode 100644 index 00000000..bd77aff5 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2021 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 ServiceTaskPropertiesProvider from "./properties-provider/ServiceTaskPropertiesProvider"; + +export default { + __init__: ['customPropertiesProvider'], + customPropertiesProvider: ['type', ServiceTaskPropertiesProvider] +}; diff --git a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/Deployment.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js similarity index 77% rename from components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/Deployment.js rename to components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js index 6f86bca3..45ff745d 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/Deployment.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js @@ -1,8 +1,7 @@ import {SelectEntry} from "@bpmn-io/properties-panel"; import React from "@bpmn-io/properties-panel/preact/compat"; import {useService} from "bpmn-js-properties-panel"; -import {getServiceTaskLikeBusinessObject} from "../../../../../editor/util/camunda-utils/ImplementationTypeUtils"; -import {getImplementationType} from "../../../utilities/ImplementationTypeHelperExtension"; +import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; /** * Copyright (c) 2023 Institute of Architecture of Application Systems - @@ -35,24 +34,12 @@ export function Deployment({element, translate, wineryEndpoint}) { url: wineryEndpoint + '/servicetemplates/?grouped', method: 'GET', success: function (result) { - let checks = 0; for (let i = 0; i < result.length; i++) { - if (result[i].text === QUANTME_NAMESPACE_PULL) { + if (result[i].text === QUANTME_NAMESPACE_PULL || result[i].text === QUANTME_NAMESPACE_PUSH) { result[i].children.forEach(element => arrValues.push({ label: element.text, value: concatenateCsarEndpoint('{{ wineryEndpoint }}', result[i].id, element.text) })); - checks++; - } - if (result[i].text === QUANTME_NAMESPACE_PUSH) { - result[i].children.forEach(element => arrValues.push({ - label: element.text, - value: concatenateCsarEndpoint('{{ wineryEndpoint }}', result[i].id, element.text) - })); - checks++; - } - if (checks === 2) { - break; } } }, @@ -65,7 +52,7 @@ export function Deployment({element, translate, wineryEndpoint}) { }; const get = function () { - return element.businessObject.get('quantme:deploymentModelUrl'); + return element.businessObject.get('opentosca:deploymentModelUrl'); }; const setValue = function (value) { diff --git a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/DmnImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js similarity index 98% rename from components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/DmnImplementationProps.js rename to components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js index faa8ce7f..35be18ab 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/DmnImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js @@ -8,8 +8,8 @@ import { SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; -import {getImplementationType} from "../../../utilities/ImplementationTypeHelperExtension"; import {useService} from "bpmn-js-properties-panel"; +import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; export function DmnImplementationProps(props) { const { diff --git a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/ImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js similarity index 96% rename from components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/ImplementationProps.js rename to components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js index 5bfc5857..9df4e5b8 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/ImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js @@ -3,12 +3,12 @@ import {DmnImplementationProps} from './DmnImplementationProps'; import {ImplementationTypeProps} from './ImplementationTypeProps'; import {useService} from "bpmn-js-properties-panel"; -import {getImplementationType} from "../../../utilities/ImplementationTypeHelperExtension"; import { getServiceTaskLikeBusinessObject, -} from "../../../../../editor/util/camunda-utils/ImplementationTypeUtils"; -import {getExtensionElementsList} from "../../../../../editor/util/camunda-utils/ExtensionElementsUtil"; +} from "../../../../editor/util/camunda-utils/ImplementationTypeUtils"; +import {getExtensionElementsList} from "../../../../editor/util/camunda-utils/ExtensionElementsUtil"; import {Deployment} from "./Deployment"; +import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; /** * Properties group for service tasks. Extends the original implementation by adding a new selection option to the diff --git a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/ImplementationTypeProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js similarity index 94% rename from components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/ImplementationTypeProps.js rename to components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js index cc7ea824..80cafc4c 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/service-task/ImplementationTypeProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js @@ -4,14 +4,14 @@ import { import {SelectEntry, isSelectEntryEdited} from '@bpmn-io/properties-panel'; import {useService} from "bpmn-js-properties-panel"; -import {getImplementationType} from "../../../utilities/ImplementationTypeHelperExtension"; -import {createElement} from "../../../../../editor/util/camunda-utils/ElementUtil" +import {createElement} from "../../../../editor/util/camunda-utils/ElementUtil"; import { getServiceTaskLikeBusinessObject, isDeploymentCapable, isDmnCapable, isExternalCapable, isServiceTaskLike -} from "../../../../../editor/util/camunda-utils/ImplementationTypeUtils"; -import {getExtensionElementsList} from "../../../../../editor/util/camunda-utils/ExtensionElementsUtil"; +} from "../../../../editor/util/camunda-utils/ImplementationTypeUtils"; +import {getExtensionElementsList} from "../../../../editor/util/camunda-utils/ExtensionElementsUtil"; +import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; const DELEGATE_PROPS = { @@ -177,13 +177,13 @@ function ImplementationType(props) { if (isDeploymentCapable(businessObject)) { updatedProperties = { ...updatedProperties, - 'quantme:deploymentModelUrl': undefined + 'opentosca:deploymentModelUrl': undefined }; if (value === 'deploymentModel') { updatedProperties = { ...updatedProperties, - 'quantme:deploymentModelUrl': '' + 'opentosca:deploymentModelUrl': '' }; } } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js new file mode 100644 index 00000000..0fa95dff --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js @@ -0,0 +1,81 @@ +import {ImplementationProps} from "./ImplementationProps"; +import {Group} from "@bpmn-io/properties-panel"; +import {getWineryEndpoint} from '../../framework-config/config-manager'; + +const LOW_PRIORITY = 500; + +/** + * A provider with a `#getGroups(element)` method that exposes groups for a diagram element. + * + * @param propertiesPanel + * @param injector + * @param {Function} translate + * @param eventBus + */ +export default function ServiceTaskPropertiesProvider(propertiesPanel, injector, translate, eventBus) { + // subscribe to config updates to retrieve the currently defined Winery endpoint + const self = this; + let wineryEndpoint; + eventBus.on('config.updated', function (config) { + wineryEndpoint = config.wineryEndpoint; + }); + + /** + * Return the groups provided for the given element. + * + * @param element + * + * @return {(Object[]) => (Object[])} groups middleware + */ + this.getGroups = function (element) { + + /** + * We return a middleware that modifies + * the existing groups. + * + * @param {Object[]} groups + * + * @return {Object[]} modified groups + */ + return function (groups) { + // update ServiceTasks with the deployment extension + if (element.type && element.type === 'bpmn:ServiceTask') { + groups[2] = ImplementationGroup(element, injector, getWineryEndpoint()); + } + return groups; + }; + }; + + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +ServiceTaskPropertiesProvider.$inject = ['propertiesPanel', 'injector', 'translate', 'eventBus']; + + +/** + * Properties group to show customized implementation options entry for service tasks. + * + * @param element The element to show the properties for. + * @param injector The injector of the bpmn-js modeler + * @param wineryEndpoint The winery endpoint of the QuantME plugin + * @return {null|{component: ((function(*): preact.VNode)|*), entries: *[], label, id: string}} + * @constructor + */ +function ImplementationGroup(element, injector, wineryEndpoint) { + const translate = injector.get('translate'); + + const group = { + label: translate('Implementation'), + id: 'CamundaPlatform__Implementation', + component: Group, + entries: [ + ...ImplementationProps({element, wineryEndpoint, translate}) + ] + }; + + if (group.entries.length) { + return group; + } + + return null; +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/QuantME_Logo.svg b/components/bpmn-q/modeler-component/extensions/opentosca/resources/QuantME_Logo.svg new file mode 100644 index 00000000..8d04e004 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/resources/QuantME_Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/config-icon.png b/components/bpmn-q/modeler-component/extensions/opentosca/resources/config-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c139553946ba0b4458666cd3646f82fa6e423634 GIT binary patch literal 572 zcmV-C0>k}@P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf06}y`Sad^gZEa<4 zbO1wAML|?gQaT`KWG!lNWoICEF)lD5D0OpbZf77#N=G0{P(?=|b~rF8EFf`pVQgu1 zc_2L?IWP)+RNCDD00D$aL_t(IPi0fR%ECYpJT*2JHrfQNEEN%3TSWw`*eO^jDI?+o z*r!Q4-$E3810O&T1m7c;YTS>R&prGZIQBBTv%9xYoN~hD2$Kyc`z#c-^btRL@h%rWv$Dc2AD?1`NCa0000< KMNUMnLSTa3*XLjW literal 0 HcmV?d00001 diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/info.png b/components/bpmn-q/modeler-component/extensions/opentosca/resources/info.png new file mode 100644 index 0000000000000000000000000000000000000000..3da2d8ffa69a68ea30d280cd5bd2b3314c218c7f GIT binary patch literal 443 zcmeAS@N?(olHy`uVBq!ia0vp^{2!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a5n0T@z;^_M8K-LV zNdpC$lDyqr82-2SpV<%Ov6p!Iy0Smw5ET+uX};A%+%KhL%>wX4(b@Rt5&k*(Y8>(U6;;l9^VCTf?;|%?zN1b)GJcAr}6< zLAiXb3OuvtG1MP)-0}ZvO}1}ge!>>di=}tjIm>3vojL7B)NC7tP2~@CmVTIB8Ys2n zVZlW+89p}t6Z2$`Pj_;t{2BRXwH|kBR|9|DqHA40-s|PJ$d_jDB_6sRI^p>(Z5vU} zz2#;Rerz-L)TtM2dT$r@hwDe=^SSH0gZBwauPeH6Z0_e0m7^Umk~i|~bZqQ4&ZHL(cT)De?YPP|`Q({8r@Ps!avWyu1-hKU)78&qol`;+01IW7oB#j- literal 0 HcmV?d00001 diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json b/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json new file mode 100644 index 00000000..47c975f6 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json @@ -0,0 +1,23 @@ +{ + "name": "OpenTOSCA4BPMN", + "uri": "https://github.com/UST-QuAntiL/OpenTOSCA", + "prefix": "opentosca", + "xml": { + "tagAlias": "lowerCase" + }, + "types": [ + { + "name": "ServiceTask", + "extends": [ "bpmn:ServiceTask" ], + "properties": [ + { + "name": "deploymentModelUrl", + "isAttr": true, + "type": "String" + } + ] + } + ], + "enumerations": [], + "associations": [] +} diff --git a/components/bpmn-q/modeler-component/extensions/quantme/resources/service-deployment-icon.png b/components/bpmn-q/modeler-component/extensions/opentosca/resources/service-deployment-icon.png similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/resources/service-deployment-icon.png rename to components/bpmn-q/modeler-component/extensions/opentosca/resources/service-deployment-icon.png diff --git a/components/bpmn-q/modeler-component/extensions/quantme/resources/service-deployment-icon.svg b/components/bpmn-q/modeler-component/extensions/opentosca/resources/service-deployment-icon.svg similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/resources/service-deployment-icon.svg rename to components/bpmn-q/modeler-component/extensions/opentosca/resources/service-deployment-icon.svg diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css b/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css new file mode 100644 index 00000000..d584e8e6 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css @@ -0,0 +1,106 @@ +@import url('~bpmn-font/dist/css/bpmn-embedded.css'); + +.qwm .quantme-tasks-icon:before { + content: ""; + width: 20px; + height: 20px; + background-size: contain; + background-image: url("../resources/QuantME_Logo.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; +} + +.qwm .config:before { + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../resources/config-icon.png") no-repeat center center; + float: left; +} + +.qwm .app-icon-service-deployment:before { + content: ""; + width: 15px; + height: 15px; + background-size: contain; + background-image: url("../resources/service-deployment-icon.png"); + background-repeat: no-repeat; + display: inline-block; + float: left; +} + +.qwm .indent { + margin-left: 5px; +} + +.qwm .spaceUnder { + padding-bottom: 1em; +} + +.qwm .spaceUnderSmall { + padding-bottom: 0.3em; +} + +.qwm .spaceAbove { + padding-top: 1em; +} + +.qwm .hidden { + display: none; +} + +.qwm .djs-label { + font-family: 'Arial', sans-serif; +} + +.qwm .adaptation-tab { + overflow: hidden; + border: 1px solid #ccc; + background-color: #f1f1f1; +} + +/* Style the buttons that are used to open the tab content */ +.qwm .adaptation-tab-button { + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + transition: 0.3s; + padding: 14px 16px; +} + +/* Change background color of buttons on hover */ +.qwm .adaptation-tab-button:hover { + background-color: #ddd; +} + +/* Create an active/current tablink class */ +.qwm .adaptation-tab-button.active { + background-color: #ccc; +} + +.qwm .rewrite-failed-button:after { + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../resources/info.png") no-repeat center center; + float: right; + padding-left: 20px; +} + +.qwm .rewrite-failed-button:disabled{ + background-color: #f44336; + color: #000000; +} + +.qwm .rewrite-successful-button:disabled{ + background-color: #008000; + color: #000000; +} diff --git a/components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/DeploymentPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/DeploymentPlugin.js rename to components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js diff --git a/components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/ServiceDeploymentBindingModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/ServiceDeploymentBindingModal.js rename to components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js diff --git a/components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/ServiceDeploymentInputModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/ServiceDeploymentInputModal.js rename to components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js diff --git a/components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/ServiceDeploymentOverviewModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentOverviewModal.js similarity index 100% rename from components/bpmn-q/modeler-component/extensions/quantme/ui/deployment/services/ServiceDeploymentOverviewModal.js rename to components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentOverviewModal.js diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js b/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js new file mode 100644 index 00000000..fc2ff59e --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 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 $ from 'jquery'; + + +export function performAjax(targetUrl, dataToSend) { + return new Promise(function (resolve, reject) { + $.ajax({ + type: 'POST', + url: targetUrl, + data: dataToSend, + processData: false, + contentType: false, + beforeSend: function () { + }, + success: function (data) { + resolve(data); + }, + error: function (err) { + reject(err); + } + }); + }); +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/quantme/QuantMEPlugin.js b/components/bpmn-q/modeler-component/extensions/quantme/QuantMEPlugin.js index 226698da..863f8d67 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/QuantMEPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/QuantMEPlugin.js @@ -2,7 +2,6 @@ import React from "react"; import QuantMEExtensionModule from "./modeling"; import BPMNConfigTab from "./configTabs/BPMNConfigTab"; -import OpenToscaTab from "./configTabs/OpenToscaTab"; import NisqAnalyzerTab from "./configTabs/NisqAnalyzerTab"; import QrmDataTab from "./configTabs/QrmDataTab"; import HybridRuntimeTab from "./configTabs/HybridRuntimeTab"; @@ -29,11 +28,6 @@ export default { tabTitle: 'QuantME Data', configTab: DataObjectConfigurationsTab, }, - { - tabId: 'OpenTOSCAEndpointTab', - tabTitle: 'OpenTOSCA', - configTab: OpenToscaTab, - }, { tabId: 'BPMNTab', tabTitle: 'Workflow', diff --git a/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js b/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js index e4bddb21..769111f4 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js @@ -12,217 +12,6 @@ import {fetch} from 'whatwg-fetch'; import {performAjax} from '../utilities/Utilities'; -/** - * Upload the CSAR located at the given URL to the connected OpenTOSCA Container and return the corresponding URL and required input parameters - * - * @param opentoscaEndpoint the endpoint of the OpenTOSCA Container - * @param csarName the name of the CSAR to upload - * @param url the URL pointing to the CSAR - * @param wineryEndpoint the endpoint of the Winery containing the CSAR to upload - */ -export async function uploadCSARToContainer(opentoscaEndpoint, csarName, url, wineryEndpoint) { - - if (opentoscaEndpoint === undefined) { - console.error('OpenTOSCA endpoint is undefined. Unable to upload CSARs...'); - return {success: false}; - } - - try { - if (url.startsWith('{{ wineryEndpoint }}')) { - url = url.replace('{{ wineryEndpoint }}', wineryEndpoint); - } - console.log('Checking if CSAR at following URL is already uploaded to the OpenTOSCA Container: ', url); - - // check if CSAR is already uploaded - let getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); - - if (!getCSARResult.success) { - console.log('CSAR is not yet uploaded. Uploading...'); - - let body = { - enrich: 'false', - name: csarName, - url: url - }; - - // upload the CSAR - await fetch(opentoscaEndpoint, { - method: 'POST', - body: JSON.stringify(body), - headers: {'Content-Type': 'application/json'} - }); - - // check successful upload and retrieve corresponding url - getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); - } - - if (!getCSARResult.success) { - console.error('Uploading CSAR failed!'); - return {success: false}; - } - - // retrieve input parameters for the build plan - let buildPlanResult = await fetch(getCSARResult.url); - let buildPlanResultJson = await buildPlanResult.json(); - - return {success: true, url: getCSARResult.url, inputParameters: buildPlanResultJson.input_parameters}; - } catch (e) { - console.error('Error while uploading CSAR: ' + e); - return {success: false}; - } -} - -/** - * Get the link to the build plan of the CSAR with the given name if it is uploaded to the OpenTOSCA Container - * - * @param opentoscaEndpoint the endpoint of the OpenTOSCA Container - * @param csarName the name of the csar - * @return the status whether the given CSAR is uploaded and the corresponding build plan link if available - */ -async function getBuildPlanForCSAR(opentoscaEndpoint, csarName) { - - // get all currently deployed CSARs - let response = await fetch(opentoscaEndpoint); - let responseJson = await response.json(); - - let deployedCSARs = responseJson.csars; - if (deployedCSARs === undefined) { - - // no CSARs available - return {success: false}; - } - - for (let i = 0; i < deployedCSARs.length; i++) { - let deployedCSAR = deployedCSARs[i]; - if (deployedCSAR.id === csarName) { - console.log('Found uploaded CSAR with id: %s', csarName); - let url = deployedCSAR._links.self.href; - - // retrieve the URl to the build plan required to get the input parameters and to instantiate the CSAR - return getBuildPlanUrl(url); - } - } - - // unable to find CSAR - return {success: false}; -} - -/** - * Get the URL to the build plan of the given CSAR - * - * @param csarUrl the URL to a CSAR - * @return the URL to the build plan for the given CSAR - */ -async function getBuildPlanUrl(csarUrl) { - - let response = await fetch(csarUrl + '/servicetemplates'); - let responseJson = await response.json(); - - if (!responseJson.service_templates || responseJson.service_templates.length !== 1) { - console.error('Unable to find service template in CSAR at URL: %s', csarUrl); - return {success: false}; - } - - let buildPlansUrl = responseJson.service_templates[0]._links.self.href + '/buildplans'; - response = await fetch(buildPlansUrl); - responseJson = await response.json(); - - if (!responseJson.plans || responseJson.plans.length !== 1) { - console.error('Unable to find build plan at URL: %s', buildPlansUrl); - return {success: false}; - } - - return {success: true, url: responseJson.plans[0]._links.self.href}; -} - -/** - * Create an instance of the ServiceTemplate contained in the given CSAR - * - * @param csar the details about the CSAR to create an instance from the contained ServiceTemplate - * @param camundaEngineEndpoint the endpoint of the Camunda engine to bind services using the pulling pattern - * @return the result of the instance creation (success, endpoint, topic on which the service listens, ...) - */ -export async function createServiceInstance(csar, camundaEngineEndpoint) { - - let result = {success: false}; - - let inputParameters = csar.inputParameters; - if (csar.type === 'pull') { - - // get special parameters that are required to bind services using external tasks / the pulling pattern - let camundaTopicParam = inputParameters.find((param) => param.name === 'camundaTopic'); - let camundaEndpointParam = inputParameters.find((param) => param.name === 'camundaEndpoint'); - - // abort if parameters are not available - if (camundaTopicParam === undefined || camundaEndpointParam === undefined) { - console.error('Unable to pass topic to poll to service instance creation. Service binding will fail!'); - return result; - } - - // generate topic for the binding - let topicName = makeId(12); - - camundaTopicParam.value = topicName; - camundaEndpointParam.value = camundaEngineEndpoint; - result.topicName = topicName; - } - - // trigger instance creation - let instanceCreationResponse = await fetch(csar.buildPlanUrl + '/instances', { - method: 'POST', - body: JSON.stringify(inputParameters), - headers: {'Content-Type': 'application/json'} - }); - let instanceCreationResponseJson = await instanceCreationResponse.json(); - - // wait for the service instance to be created - await new Promise(r => setTimeout(r, 5000)); - - // get service template instance to poll for completness - let buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - let buildPlanResponseJson = await buildPlanResponse.json(); - - // retry polling 10 times, creation of the build time takes some time - for (let retry = 0; retry < 10; retry++) { - - // stop retries in case of correct response - if (buildPlanResponseJson._links) { - break; - } - - await new Promise(r => setTimeout(r, 5000)); - - console.log('Retry fetching build plan'); - - buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - buildPlanResponseJson = await buildPlanResponse.json(); - } - - if (!buildPlanResponseJson._links) { - console.log('Unable to fetch build plans for ' + csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - result.success = false; - return result; - } - - let pollingUrl = buildPlanResponseJson._links.service_template_instance.href; - - let state = 'CREATING'; - console.log('Polling for finished service instance at URL: %s', pollingUrl); - while (!(state === 'CREATED' || state === 'FAILED')) { - - // wait 5 seconds for next poll - await new Promise(r => setTimeout(r, 5000)); - // poll for current state - let pollingResponse = await fetch(pollingUrl); - let pollingResponseJson = await pollingResponse.json(); - console.log('Polling response: ', pollingResponseJson); - - state = pollingResponseJson.state; - } - - result.success = true; - return result; -} /** * Create a new ArtifactTemplate of the given type and add the given blob as file diff --git a/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config-manager.js b/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config-manager.js index 11f1a52a..ec8e9b80 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config-manager.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config-manager.js @@ -77,56 +77,6 @@ export function setTransformationFrameworkEndpoint(transformationFrameworkEndpoi } } -/** - * Get the endpoint of the configured OpenTOSCA container - * - * @return {string} the currently specified endpoint of the OpenTOSCA container - */ -export function getOpenTOSCAEndpoint() { - if (config.opentoscaEndpoint === undefined) { - setOpenTOSCAEndpoint( - getPluginConfig('quantme').opentoscaEndpoint - || defaultConfig.opentoscaEndpoint); - } - return config.opentoscaEndpoint; -} - -/** - * Set the endpoint of the OpenTOSCA container - * - * @param opentoscaEndpoint the endpoint of the OpenTOSCA container - */ -export function setOpenTOSCAEndpoint(opentoscaEndpoint) { - if (opentoscaEndpoint !== null && opentoscaEndpoint !== undefined) { - config.opentoscaEndpoint = opentoscaEndpoint.replace(/\/$/, ''); - } -} - -/** - * Get the endpoint of the configured Winery - * - * @return {string} the currently specified endpoint of the Winery - */ -export function getWineryEndpoint() { - if (config.wineryEndpoint === undefined) { - setWineryEndpoint( - getPluginConfig('quantme').wineryEndpoint - || defaultConfig.wineryEndpoint); - } - return config.wineryEndpoint; -} - -/** - * Set the endpoint of the Winery - * - * @param wineryEndpoint the endpoint of the Winery - */ -export function setWineryEndpoint(wineryEndpoint) { - if (wineryEndpoint !== null && wineryEndpoint !== undefined) { - config.wineryEndpoint = wineryEndpoint.replace(/\/$/, ''); - } -} - /** * Get the local path to the folder in the repository containing the QRMs * diff --git a/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config.js b/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config.js index 05ead47b..c2373d7f 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/framework-config/config.js @@ -12,8 +12,6 @@ // takes either the environment variables or the default values definded in webpack.config const defaultConfig = { quantmeDataConfigurationsEndpoint: process.env.DATA_CONFIG, - opentoscaEndpoint: process.env.OPENTOSCA_ENDPOINT, - wineryEndpoint: process.env.WINERY_ENDPOINT, nisqAnalyzerEndpoint: process.env.NISQ_ANALYZER_ENDPOINT, transformationFrameworkEndpoint: process.env.TRANSFORMATION_FRAMEWORK_ENDPOINT, qiskitRuntimeHandlerEndpoint: process.env.QISKIT_RUNTIME_HANDLER_ENDPOINT, diff --git a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/QuantMEPropertiesProvider.js b/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/QuantMEPropertiesProvider.js index 12c08f73..ef7bc7d6 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/QuantMEPropertiesProvider.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/modeling/properties-provider/QuantMEPropertiesProvider.js @@ -15,9 +15,6 @@ import { VariationalQuantumAlgorithmTaskEntries, WarmStartingTaskEntries } from "./QuantMETaskProperties"; -import { ImplementationProps } from "./service-task/ImplementationProps"; -import { Group } from "@bpmn-io/properties-panel"; -import { getWineryEndpoint } from '../../framework-config/config-manager'; import * as configConsts from '../../../../editor/configurations/Constants'; import { instance as dataObjectConfigs } from '../../configurations/DataObjectConfigurations'; import ConfigurationsProperties from '../../../../editor/configurations/ConfigurationsProperties'; @@ -34,7 +31,6 @@ const LOW_PRIORITY = 500; * @param bpmnFactory */ export default function QuantMEPropertiesProvider(propertiesPanel, injector, translate, eventBus, bpmnFactory) { - // subscribe to config updates to retrieve the currently defined Winery endpoint const self = this; let wineryEndpoint; @@ -66,11 +62,6 @@ export default function QuantMEPropertiesProvider(propertiesPanel, injector, tra groups.unshift(createQuantMEGroup(element, translate)); } - // update ServiceTasks with the deployment extension - if (element.type && element.type === 'bpmn:ServiceTask') { - groups[2] = ImplementationGroup(element, injector, getWineryEndpoint()); - } - // add properties group for displaying the properties defined by the configurations if a configuration // is applied to the current element if (is(element, dataConsts.DATA_MAP_OBJECT)) { @@ -107,34 +98,6 @@ function createQuantMEGroup(element, translate) { }; } -/** - * Properties group to show customized implementation options entry for service tasks. - * - * @param element The element to show the properties for. - * @param injector The injector of the bpmn-js modeler - * @param wineryEndpoint The winery endpoint of the QuantME plugin - * @return {null|{component: ((function(*): preact.VNode)|*), entries: *[], label, id: string}} - * @constructor - */ -function ImplementationGroup(element, injector, wineryEndpoint) { - const translate = injector.get('translate'); - - const group = { - label: translate('Implementation'), - id: 'CamundaPlatform__Implementation', - component: Group, - entries: [ - ...ImplementationProps({ element, wineryEndpoint, translate }) - ] - }; - - if (group.entries.length) { - return group; - } - - return null; -} - /** * Add the property entries for the QuantME attributes to the given group based on the type of the QuantME element * diff --git a/components/bpmn-q/modeler-component/extensions/quantme/resources/quantum4bpmn.json b/components/bpmn-q/modeler-component/extensions/quantme/resources/quantum4bpmn.json index 284e0923..09cbfcc6 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/resources/quantum4bpmn.json +++ b/components/bpmn-q/modeler-component/extensions/quantme/resources/quantum4bpmn.json @@ -376,17 +376,6 @@ } ] }, - { - "name": "ServiceTask", - "extends": [ "bpmn:ServiceTask" ], - "properties": [ - { - "name": "deploymentModelUrl", - "isAttr": true, - "type": "String" - } - ] - }, { "name": "Task", "extends": [ "bpmn:Task" ], diff --git a/components/bpmn-q/modeler-component/extensions/quantme/styling/quantme.css b/components/bpmn-q/modeler-component/extensions/quantme/styling/quantme.css index 59a5cb71..93c27d01 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/styling/quantme.css +++ b/components/bpmn-q/modeler-component/extensions/quantme/styling/quantme.css @@ -73,17 +73,6 @@ float: left; } -.qwm .app-icon-service-deployment:before { - content: ""; - width: 15px; - height: 15px; - background-size: contain; - background-image: url("../resources/service-deployment-icon.png"); - background-repeat: no-repeat; - display: inline-block; - float: left; -} - .qwm .indent { margin-left: 5px; } diff --git a/components/bpmn-q/modeler-component/extensions/quantme/ui/QuantMEPluginButton.js b/components/bpmn-q/modeler-component/extensions/quantme/ui/QuantMEPluginButton.js index 793d36e2..ee6f39a8 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/ui/QuantMEPluginButton.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/ui/QuantMEPluginButton.js @@ -2,7 +2,6 @@ import React from 'react'; import AdaptationPlugin from "./adaptation/AdaptationPlugin"; import QuantMEController from "./control/QuantMEController"; import UpdateDataObjectConfigurationsButton from "../configurations/UpdateDataObjectConfigurationsButton"; -import DeploymentPlugin from "./deployment/services/DeploymentPlugin"; import ExtensibleButton from "../../../editor/ui/ExtensibleButton"; import NotificationHandler from "../../../editor/ui/notifications/NotificationHandler"; import {updateQRMs} from "../qrm-manager"; @@ -22,8 +21,7 @@ export default function QuantMEPluginButton() { }); return , , , - ]} + subButtons={[, , ]} title="QuantME" styleClass="quantme-logo" description="Show buttons of the QuantME plugin"/>; diff --git a/components/bpmn-q/modeler-component/extensions/quantme/ui/control/QuantMEController.js b/components/bpmn-q/modeler-component/extensions/quantme/ui/control/QuantMEController.js index 39d12d9f..22800b43 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/ui/control/QuantMEController.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/ui/control/QuantMEController.js @@ -13,16 +13,10 @@ import React, {PureComponent} from 'react'; import {startQuantmeReplacementProcess} from '../../replacement/QuantMETransformator'; -import {configureBasedOnHardwareSelection} from '../../replacement/hardware-selection/QuantMEHardwareSelectionHandler'; -import {getServiceTasksToDeploy} from '../../deployment/DeploymentUtils'; -import {createServiceInstance, uploadCSARToContainer} from '../../deployment/OpenTOSCAUtils'; -import {bindUsingPull, bindUsingPush} from '../../deployment/BindingUtils'; -import {createTempModelerFromXml, getModeler} from "../../../../editor/ModelerHandler"; +import {getModeler} from "../../../../editor/ModelerHandler"; import NotificationHandler from "../../../../editor/ui/notifications/NotificationHandler"; import {getQRMs, updateQRMs} from "../../qrm-manager"; -import {getXml} from "../../../../editor/util/IoUtilities"; import config from "../../framework-config/config"; -import {getRootProcess} from '../../../../editor/util/ModellingUtilities'; /** * React component which contains a button which updates the QRMs by reloading them from the sepcified GitHub repository. @@ -63,105 +57,6 @@ export default class QuantMEController extends PureComponent { } }); } - - if (!this.editorActions._actions.hasOwnProperty('transformAndDeployWorkflow')) { - // transform and deploy the workflow for the dynamic hardware selection - this.editorActions.register({ - transformAndDeployWorkflow: async function (params) { - console.log('Transforming and deploying workflow for hardware selection!'); - let currentQRMs = getQRMs(); - - // configure the workflow fragment with the given parameters - console.log('Configuring workflow to transform using provider "%s", QPU "%s", and circuit language "%s"!', - params.provider, params.qpu, params.circuitLanguage); - let configurationResult = await configureBasedOnHardwareSelection(params.xml, params.provider, params.qpu, params.circuitLanguage); - - // forward error to API if configuration fails - if (configurationResult.status === 'failed') { - console.log('Configuration of given workflow fragment and parameters failed!'); - self.api.sendResult(params.returnPath, params.id, { - status: configurationResult.status, - xml: configurationResult.xml - }); - return; - } - - // transform to native BPMN - let result = await startQuantmeReplacementProcess(configurationResult.xml, currentQRMs, - { - nisqAnalyzerEndpoint: self.modeler.config.nisqAnalyzerEndpoint, - transformationFrameworkEndpoint: self.modeler.config.transformationFrameworkEndpoint, - camundaEndpoint: self.modeler.config.camundaEndpoint - }); - if (result.status === 'failed') { - console.log('Transformation process failed with cause: ', result.cause); - self.api.sendResult(params.returnPath, params.id, {status: 'failed'}); - return; - } - - // get all ServiceTasks that require a service deployment - let modeler = await createTempModelerFromXml(result.xml); - let csarsToDeploy = getServiceTasksToDeploy(getRootProcess(modeler.getDefinitions())); - console.log('Found %i CSARs associated with ServiceTasks: ', csarsToDeploy.length, csarsToDeploy); - - // upload the CSARs to the OpenTOSCA Container - for (let i = 0; i < csarsToDeploy.length; i++) { - let csar = csarsToDeploy[i]; - let uploadResult = await uploadCSARToContainer(config.opentoscaEndpoint, csar.csarName, csar.url, config.wineryEndpoint); - console.log('Uploaded CSAR \'%s\' to OpenTOSCA container with result: ', csar.csarName, uploadResult); - - // abort if upload is not successful - if (uploadResult.success === false) { - self.api.sendResult(params.returnPath, params.id, {status: 'failed'}); - return; - } - csar.buildPlanUrl = uploadResult.url; - csar.inputParameters = uploadResult.inputParameters; - - // create a service instance of the CSAR - console.log('Successfully uploaded CSAR to OpenTOSCA Container. Creating service instance...'); - let instanceCreationResponse = await createServiceInstance(csar, config.camundaEndpoint); - console.log('Creation of service instance of CSAR \'%s\' returned result: ', csar.csarName, instanceCreationResponse); - - // bind the service instance using the specified binding pattern - let serviceTaskIds = csar.serviceTaskIds; - for (let j = 0; j < serviceTaskIds.length; j++) { - let bindingResponse = undefined; - if (csar.type === 'pull') { - bindingResponse = bindUsingPull(instanceCreationResponse.topicName, serviceTaskIds[j], modeler.get('elementRegistry'), modeler.get('modeling')); - } else if (csar.type === 'push') { - bindingResponse = bindUsingPush(csar, serviceTaskIds[j], modeler.get('elementRegistry')); - } - - if (bindingResponse === undefined || bindingResponse.success === false) { - console.error('Failed to bind service instance to ServiceTask with Id: ', serviceTaskIds[j]); - self.api.sendResult(params.returnPath, params.id, {status: 'failed'}); - return; - } - } - } - console.log('Successfully deployed and bound all required service instances!'); - - // deploy the transformed and bound workflow to the Camunda engine - const rootElement = getRootProcess(modeler.getDefinitions()); - let boundWorkflowXml = await getXml(modeler); - let workflowDeploymentResult = await self.backend.send('deployment:deploy-workflow', rootElement.id, boundWorkflowXml, {}); - if (workflowDeploymentResult === undefined || workflowDeploymentResult.status !== 'deployed') { - console.error('Failed to deploy workflow: ', workflowDeploymentResult); - self.api.sendResult(params.returnPath, params.id, {status: 'failed'}); - return; - } - - // return result to the API - console.log('Workflow deployment successfully. Returning to API...'); - self.api.sendResult(params.returnPath, params.id, { - status: workflowDeploymentResult.status, - deployedProcessDefinition: workflowDeploymentResult.deployedProcessDefinition, - xml: boundWorkflowXml - }); - } - }); - } } updateQRMs() { diff --git a/components/bpmn-q/modeler-component/extensions/quantme/utilities/ImplementationTypeHelperExtension.js b/components/bpmn-q/modeler-component/extensions/quantme/utilities/ImplementationTypeHelperExtension.js index 2cdc032f..48e44faf 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/utilities/ImplementationTypeHelperExtension.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/utilities/ImplementationTypeHelperExtension.js @@ -65,7 +65,7 @@ export function getImplementationType(element) { return 'delegateExpression'; } - const deploymentModelUrl = businessObject.get('quantme:deploymentModelUrl'); + const deploymentModelUrl = businessObject.get('opentosca:deploymentModelUrl'); if (typeof deploymentModelUrl !== 'undefined') { return 'deploymentModel'; } diff --git a/components/bpmn-q/public/index.html b/components/bpmn-q/public/index.html index ad1f22cb..3c824082 100644 --- a/components/bpmn-q/public/index.html +++ b/components/bpmn-q/public/index.html @@ -128,6 +128,9 @@ { name: 'qhana', }, + { + name: 'opentosca', + }, { name: 'planqk', config: { diff --git a/components/bpmn-q/test/tests/editor/plugin.spec.js b/components/bpmn-q/test/tests/editor/plugin.spec.js index adf87c37..743f6cfd 100644 --- a/components/bpmn-q/test/tests/editor/plugin.spec.js +++ b/components/bpmn-q/test/tests/editor/plugin.spec.js @@ -24,22 +24,23 @@ describe('Test plugins', function () { expect(getActivePlugins().length).to.equal(0); }); - it('Should find 3 active plugins', function () { - setPluginConfig([{name: 'dataflow'}, {name: 'quantme'}, {name: 'planqk'}]); + it('Should find 4 active plugins', function () { + setPluginConfig([{name: 'dataflow'}, {name: 'quantme'}, {name: 'opentosca'}, {name: 'planqk'}]); const plugins = getActivePlugins(); - expect(plugins.length).to.equal(3); + expect(plugins.length).to.equal(4); expect(plugins[0].name).to.equal('dataflow'); expect(plugins[1].name).to.equal('quantme'); - expect(plugins[2].name).to.equal('planqk'); + expect(plugins[2].name).to.equal('opentosca'); + expect(plugins[3].name).to.equal('planqk'); }); }); describe('Test getter for plugin attributes', function () { it('Should get correct plugin entries for active plugins', function () { - setPluginConfig([{name: 'dataflow'}, {name: 'quantme'}, {name: 'planqk'}]); + setPluginConfig([{name: 'dataflow'}, {name: 'quantme'}, {name: 'opentosca'}, {name: 'planqk'}]); const modules = getAdditionalModules(); const extensions = getModdleExtension(); @@ -48,14 +49,15 @@ describe('Test plugins', function () { const tabs = getConfigTabs(); const styles = getStyles(); - expect(modules.length).to.equal(3); + expect(modules.length).to.equal(4); expect(extensions['dataflow']).to.not.be.undefined; 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(2); + expect(buttons.length).to.equal(3); expect(tabs.length).to.equal(8); - expect(styles.length).to.equal(3); + expect(styles.length).to.equal(4); }); }); }); diff --git a/components/bpmn-q/test/tests/helpers/DiagramHelper.js b/components/bpmn-q/test/tests/helpers/DiagramHelper.js index 0cb1845e..410ff88a 100644 --- a/components/bpmn-q/test/tests/helpers/DiagramHelper.js +++ b/components/bpmn-q/test/tests/helpers/DiagramHelper.js @@ -282,7 +282,7 @@ const transformedValidPlanqkDiagram = '\n' ' \n' + '\n'; -export const validQuantMEDiagram = ' SequenceFlow_0kum1kc SequenceFlow_0kum1kc SequenceFlow_0gw15u7 SequenceFlow_0gw15u7 SequenceFlow_00gjpgx SequenceFlow_0s6m835 SequenceFlow_0fxi83k SequenceFlow_16yvlag SequenceFlow_0591a3g ${clusteringConverged == \'false\'} ${clusteringConverged == \'true\'} SequenceFlow_1wsvjv1 SequenceFlow_0ncbyt5 SequenceFlow_08ni26o SequenceFlow_09l09is SequenceFlow_0ncbyt5 SequenceFlow_0vmb89t ${classificationConverged == \'false\'} ${classificationConverged == \'true\'} SequenceFlow_03zrxe7 SequenceFlow_1csno8e SequenceFlow_1csno8e SequenceFlow_15qw95r SequenceFlow_0fxi83k SequenceFlow_16yvlag SequenceFlow_00gjpgx SequenceFlow_0591a3g SequenceFlow_1wsvjv1 SequenceFlow_08ed0ea SequenceFlow_09l09is SequenceFlow_0vmb89t SequenceFlow_03zrxe7 SequenceFlow_08ni26o SequenceFlow_08ed0ea SequenceFlow_0s6m835 SequenceFlow_15qw95r '; +export const validQuantMEDiagram = ' SequenceFlow_0kum1kc SequenceFlow_0kum1kc SequenceFlow_0gw15u7 SequenceFlow_0gw15u7 SequenceFlow_00gjpgx SequenceFlow_0s6m835 SequenceFlow_0fxi83k SequenceFlow_16yvlag SequenceFlow_0591a3g ${clusteringConverged == \'false\'} ${clusteringConverged == \'true\'} SequenceFlow_1wsvjv1 SequenceFlow_0ncbyt5 SequenceFlow_08ni26o SequenceFlow_09l09is SequenceFlow_0ncbyt5 SequenceFlow_0vmb89t ${classificationConverged == \'false\'} ${classificationConverged == \'true\'} SequenceFlow_03zrxe7 SequenceFlow_1csno8e SequenceFlow_1csno8e SequenceFlow_15qw95r SequenceFlow_0fxi83k SequenceFlow_16yvlag SequenceFlow_00gjpgx SequenceFlow_0591a3g SequenceFlow_1wsvjv1 SequenceFlow_08ed0ea SequenceFlow_09l09is SequenceFlow_0vmb89t SequenceFlow_03zrxe7 SequenceFlow_08ni26o SequenceFlow_08ed0ea SequenceFlow_0s6m835 SequenceFlow_15qw95r '; export const validDataFlowDiagram = ' Flow_1wgvxmm Flow_1wr8t0y Flow_1wgvxmm Flow_1wr8t0y DataStoreMap_0louwgh Property_0ohays4 DataMapObject_19we4h2 '; diff --git a/components/bpmn-q/test/tests/opentosca/deployment-utils.spec.js b/components/bpmn-q/test/tests/opentosca/deployment-utils.spec.js new file mode 100644 index 00000000..0ae1cc0a --- /dev/null +++ b/components/bpmn-q/test/tests/opentosca/deployment-utils.spec.js @@ -0,0 +1,32 @@ +import {validQuantMEDiagram} from "../helpers/DiagramHelper"; +import {createTempModelerFromXml} from "../../../modeler-component/editor/ModelerHandler"; +import {getServiceTasksToDeploy} from "../../../modeler-component/extensions/opentosca/deployment/DeploymentUtils"; +import {getRootProcess} from "../../../modeler-component/editor/util/ModellingUtilities"; +import {setPluginConfig} from "../../../modeler-component/editor/plugin/PluginConfigHandler"; +import {expect} from 'chai'; + +describe('Test the CSAR extraction.', function () { + + + it('should get service tasks to deploy from model', async function () { + setPluginConfig([{ + name: 'dataflow', + config: {} + }, { + name: 'quantme', + config: { + test: 'test', + } + }, { + name: 'opentosca', + config: { + test: 'test', + } + }]); + const modeler = await createTempModelerFromXml(validQuantMEDiagram); + let csarsToDeploy = getServiceTasksToDeploy(getRootProcess(modeler.getDefinitions())); + expect(csarsToDeploy.length).to.equal(1); + expect(csarsToDeploy[0].url).to.equal('{{ wineryEndpoint }}/servicetemplates/http%253A%252F%252Fquantil.org%252Fquantme%252Fpull/KMeansInitializerService/?csar'); + expect(csarsToDeploy[0].csarName).to.equal('KMeansInitializerService.csar'); + }); +}); \ No newline at end of file diff --git a/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js b/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js new file mode 100644 index 00000000..865dae6a --- /dev/null +++ b/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js @@ -0,0 +1,32 @@ +import {setPluginConfig} from '../../../modeler-component/editor/plugin/PluginConfigHandler'; +import {expect} from 'chai'; +import * as opentoscaConfig from '../../../modeler-component/extensions/opentosca/framework-config/config-manager'; + +describe('Test OpenTosca ConfigManager', function () { + + describe('Test OpenTosca endpoint', function () { + + before('Reset OpenTosca configuration', function () { + opentoscaConfig.resetConfig(); + }); + + afterEach('Reset OpenTosca configuration', function () { + opentoscaConfig.resetConfig(); + }); + + it('Should configure OpenTosca endpoints', function () { + setPluginConfig([ + { + name: 'opentosca', + config: { + opentoscaEndpoint: 'http://test:1337/csars', + wineryEndpoint: 'http://test:8093/winery', + } + }] + ); + + expect(opentoscaConfig.getOpenTOSCAEndpoint()).to.equal('http://test:1337/csars'); + expect(opentoscaConfig.getWineryEndpoint()).to.equal('http://test:8093/winery'); + }); + }); +}); diff --git a/components/bpmn-q/test/tests/quantme/quantme-config.spec.js b/components/bpmn-q/test/tests/quantme/quantme-config.spec.js index b54dfbc5..46847fe5 100644 --- a/components/bpmn-q/test/tests/quantme/quantme-config.spec.js +++ b/components/bpmn-q/test/tests/quantme/quantme-config.spec.js @@ -20,8 +20,6 @@ describe('Test QuantME ConfigManager', function () { name: 'quantme', config: { quantmeDataConfigurationsEndpoint: 'http://test:8100/data-objects', - opentoscaEndpoint: 'http://test:1337/csars', - wineryEndpoint: 'http://test:8093/winery', nisqAnalyzerEndpoint: 'http://test:8098/nisq-analyzer', transformationFrameworkEndpoint: 'http://test:8888', qiskitRuntimeHandlerEndpoint: 'http://test:8889', @@ -37,8 +35,6 @@ describe('Test QuantME ConfigManager', function () { ); expect(quantmeConfig.getQuantMEDataConfigurationsEndpoint()).to.equal('http://test:8100/data-objects'); - expect(quantmeConfig.getOpenTOSCAEndpoint()).to.equal('http://test:1337/csars'); - expect(quantmeConfig.getWineryEndpoint()).to.equal('http://test:8093/winery'); expect(quantmeConfig.getNisqAnalyzerEndpoint()).to.equal('http://test:8098/nisq-analyzer'); expect(quantmeConfig.getTransformationFrameworkEndpoint()).to.equal('http://test:8888'); expect(quantmeConfig.getQiskitRuntimeHandlerEndpoint()).to.equal('http://test:8889'); diff --git a/components/bpmn-q/webpack.config.js b/components/bpmn-q/webpack.config.js index 960dcdc4..f9ac3a0a 100644 --- a/components/bpmn-q/webpack.config.js +++ b/components/bpmn-q/webpack.config.js @@ -69,8 +69,9 @@ module.exports = { ENABLE_PLANQK_PLUGIN: true, ENABLE_QHANA_PLUGIN: true, ENABLE_QUANTME_PLUGIN: true, - GITHUB_TOKEN: '', + ENABLE_OPENTOSCA_PLUGIN: true, OPENTOSCA_ENDPOINT: 'http://localhost:1337/csars', + GITHUB_TOKEN: '', NISQ_ANALYZER_ENDPOINT: 'http://localhost:8098/nisq-analyzer', QISKIT_RUNTIME_HANDLER_ENDPOINT: 'http://localhost:8889', QHANA_GET_PLUGIN_URL: 'http://localhost:5006/api/plugins/', From 4218c77e21b0e81de7a8c1d916dbf9799dd8a2ae Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Wed, 10 May 2023 12:04:02 +0200 Subject: [PATCH 02/33] feat(ci): dockerize Refs: #6 --- .github/workflows/docker-publish.yaml | 24 ++++++++++++++++++++++++ Dockerfile | 20 ++++++++++---------- 2 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/docker-publish.yaml diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml new file mode 100644 index 00000000..6906f8fc --- /dev/null +++ b/.github/workflows/docker-publish.yaml @@ -0,0 +1,24 @@ + +on: + push: + #branches: + # - 'master' + +jobs: + docker-publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v4 + with: + push: true + tags: ghcr.io/sequenc-consortium/workflow-modeler:latest diff --git a/Dockerfile b/Dockerfile index 0e251fff..19bd171d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM node:18-alpine - +FROM node:18-alpine as builder LABEL maintainer = "Martin Beisel " -COPY "components/bpmn-q" /tmp -WORKDIR /tmp - -RUN npm install - -EXPOSE 8080 - -CMD npm run dev +COPY "components/bpmn-q" /app +WORKDIR /app +RUN npm ci +RUN npm run build -- --mode=production +FROM nginxinc/nginx-unprivileged:alpine +USER root +RUN rm -rf /usr/share/nginx/html +COPY --from=builder /app/public /usr/share/nginx/html +USER 101 From f8ac265be44136a0e1af4f690b5f89e027a621fb Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 15 May 2023 12:28:44 +0200 Subject: [PATCH 03/33] docs: serve from docker container --- .github/workflows/docker-publish.yaml | 4 ++-- README.md | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index 6906f8fc..820f1934 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -1,8 +1,8 @@ on: push: - #branches: - # - 'master' + branches: + - 'master' jobs: docker-publish: diff --git a/README.md b/README.md index c424cb65..2d48cc81 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,14 @@ npm run dev ``` to start the modeler in a simple html website which runs on localhost:8080. +## Execution in Docker +To serve the application from a Docker container execute: +``` +docker run --name workflow-modeler -p 8080:8080 ghcr.io/sequenc-consortium/workflow-modeler +``` +One can add environment variables with the `-e =` flag. + + ## How to use this Library To use the Quantum Workflow Modeler component in your application you have to install its npm package which is published From 47c218dfd44d0befe2de8df9ac55b5452e046363 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Wed, 17 May 2023 09:43:31 +0200 Subject: [PATCH 04/33] docs(ci): extend documentation of docker build --- .dockerignore | 3 +++ Dockerfile | 24 ++++++++++++++++++- README.md | 8 ++++++- .../editor/plugin/PluginHandler.js | 10 ++++---- components/bpmn-q/package-lock.json | 4 ++-- 5 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ced1c8cf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +components/bpmn-q/node_modules/ +npm-debug.log +yarn-error.log diff --git a/Dockerfile b/Dockerfile index 19bd171d..aaf60721 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,30 @@ FROM node:18-alpine as builder LABEL maintainer = "Martin Beisel " COPY "components/bpmn-q" /app WORKDIR /app -RUN npm ci +RUN npm ci + +ARG DATA_CONFIG +ARG OPENTOSCA_ENDPOINT +ARG WINERY_ENDPOINT +ARG NISQ_ANALYZER_ENDPOINT +ARG TRANSFORMATION_FRAMEWORK_ENDPOINT +ARG QISKIT_RUNTIME_HANDLER_ENDPOINT +ARG AWS_RUNTIME_HANDLER_ENDPOINT +ARG SCRIPT_SPLITTER_ENDPOINT +ARG SCRIPT_SPLITTER_THRESHOLD +ARG QRM_REPONAME +ARG QRM_USERNAME +ARG QRM_REPOPATH +ARG PROVENANCE_COLLECTION +ARG ENABLE_DATA_FLOW_PLUGIN +ARG ENABLE_PLANQK_PLUGIN +ARG ENABLE_QHANA_PLUGIN +ARG ENABLE_QUANTME_PLUGIN +ARG ENABLE_OPENTOSCA_PLUGIN +RUN env RUN npm run build -- --mode=production + + FROM nginxinc/nginx-unprivileged:alpine USER root RUN rm -rf /usr/share/nginx/html diff --git a/README.md b/README.md index 2d48cc81..d34c402a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,13 @@ To serve the application from a Docker container execute: ``` docker run --name workflow-modeler -p 8080:8080 ghcr.io/sequenc-consortium/workflow-modeler ``` -One can add environment variables with the `-e =` flag. +Afterwards the application is available in a browser on localhost:8080 + +To build and run an own image execute: +``` +docker build -t workflow-modeler [--build-arg =] . +docker run --name workflow-modeler -p 8080:8080 workflow-modeler +``` ## How to use this Library diff --git a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js index af20f6fd..7037a1ba 100644 --- a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js +++ b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js @@ -58,15 +58,15 @@ export function getActivePlugins() { export function checkEnabledStatus(pluginName) { switch(pluginName) { case 'dataflow': - return process.env.ENABLE_DATA_FLOW_PLUGIN; + return process.env.ENABLE_DATA_FLOW_PLUGIN !== "false"; case 'planqk': - return process.env.ENABLE_PLANQK_PLUGIN; + return process.env.ENABLE_PLANQK_PLUGIN !== "false"; case 'qhana': - return process.env.ENABLE_QHANA_PLUGIN; + return process.env.ENABLE_QHANA_PLUGIN !== "false"; case 'quantme': - return process.env.ENABLE_QUANTME_PLUGIN; + return process.env.ENABLE_QUANTME_PLUGIN !== "false"; case 'opentosca': - return process.env.ENABLE_OPENTOSCA_PLUGIN; + return process.env.ENABLE_OPENTOSCA_PLUGIN !== "false"; } } /** diff --git a/components/bpmn-q/package-lock.json b/components/bpmn-q/package-lock.json index b3723a14..dae6feaf 100644 --- a/components/bpmn-q/package-lock.json +++ b/components/bpmn-q/package-lock.json @@ -1,12 +1,12 @@ { "name": "@planqk/quantum-workflow-modeler", - "version": "0.1.7", + "version": "0.1.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@planqk/quantum-workflow-modeler", - "version": "0.1.7", + "version": "0.1.8", "license": "Apache-2.0", "dependencies": { "@bpmn-io/properties-panel": "^1.3.1", From 4cbc677410ef9d2dc0ec878b3ead5f5fee3a637d Mon Sep 17 00:00:00 2001 From: Maximilian Kuhn Date: Wed, 14 Jun 2023 08:52:31 +0200 Subject: [PATCH 05/33] Add feature issue template (#15) * Add Issue Templates for Features Add feature issue templates similar to the ones of the qunicorn repository but adjusted to the style of the workflow modeler. --- .github/ISSUE_TEMPLATE/feature_request.md | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..657f5cc8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,30 @@ +--- +name: Feature request +about: Describe the feature +title: '' +labels: '' +assignees: '' + +--- + +**Problem Statement:** + + +**Sketch (Optional):** + + +**References (Optional):** + + +**Expected Behavior:** + + +**Tasks:** + + +- [ ] Task 1 +- [ ] Task 2 +- [ ] Task 3 + +**Other Information:** + From ffa438ecb440008f79613b1c165f62337407250c Mon Sep 17 00:00:00 2001 From: Maximilian Kuhn Date: Wed, 14 Jun 2023 14:53:08 +0200 Subject: [PATCH 06/33] Implement attachment of deployment artifacts Ref: #1 --- .../QuantumWorkflowModeler.js | 1 + .../editor/resources/styling/editor-ui.css | 14 +++ .../properties-provider/ArtifactUpload.js | 48 ++++++++ .../ArtifactWizardModal.js | 104 ++++++++++++++++++ .../ImplementationProps.js | 6 + .../properties-provider/artifact-modal.css | 64 +++++++++++ 6 files changed, 237 insertions(+) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css diff --git a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js index daec9ec6..f12f9353 100644 --- a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js +++ b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js @@ -83,6 +83,7 @@ export class QuantumWorkflowModeler extends HTMLElement {
+
`; diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css b/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css index 205a93c4..08a81ab9 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css @@ -13,6 +13,20 @@ color: #636363; } +.qwm-properties-btn { + box-sizing: border-box; + outline: none; + background-color: transparent; + border: solid; + border-radius: 6px; + font-size: 15px; + font-family: Arial, serif; + font-weight: bold; + margin: 2px; + padding: 8px; + color: #636363; +} + .qwm-shortcuts { right: 0px; position: absolute; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js new file mode 100644 index 00000000..e735656e --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js @@ -0,0 +1,48 @@ +import {HeaderButton} from '@bpmn-io/properties-panel'; +import {useService} from 'bpmn-js-properties-panel'; +import React from 'react'; +import ArtifactWizardModal from './ArtifactWizardModal'; +import {createRoot} from 'react-dom/client'; +import {useState} from 'react'; +import './artifact-modal.css'; + +/** + * Entry to display the button which opens the Artifact Wizard, a dialog which allows to upload + */ +export function ArtifactUpload(props) { + const {element} = props; + const translate = useService('translate'); + + const onClick = () => { + // render config button and pop-up menu + console.log("Button Clicked"); + const root = createRoot(document.getElementById("wizardDiv")); + root.render(); + }; + + return HeaderButton({ + element, + id: 'deployment-data-button', + text: translate('Deployment Data'), + description: 'ArtifactWizard', + className: "wizard-button", + children: 'Artifact Wizard', + onClick, + }); +} + +function Modal() { + const [showModal, setShowModal] = useState(true); + + function handleWizardClosed() { + setShowModal(false); + } + + return ( +
+ {showModal && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js new file mode 100644 index 00000000..21eb0c3d --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js @@ -0,0 +1,104 @@ +/** + * 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, {useState} from 'react'; +import Modal from '../../../../editor/ui/modal/Modal'; +import './artifact-modal.css'; +import '../../../../editor/config/config-modal.css'; + + +// polyfill upcoming structural components +const Title = Modal.Title; +const Body = Modal.Body; +const Footer = Modal.Footer; + +/** + * Configuration modal of the editor which displays a set of given configTabs. used to display customized tabs of the + * plugins to allow them the configurations of their plugin configurations during runtime. + * + * @param onClose Function called when the modal is closed. + * @returns {JSX.Element} The modal as React component + * @constructor + */ +export default function ArtifactWizardModal({onClose}) { + const [uploadFile, setUploadFile] = useState(null); + const [textInput, setTextInput] = useState(''); + const [selectedTab, setSelectedTab] = useState("docker"); + + const onSubmit = () => { + // Process the uploaded file or text input here + console.log('Uploaded file:', uploadFile); + console.log('Text input:', textInput); + + // Call close callback + onClose(); + }; + + return ( + + Artifact Wizard + + +
+
+
setSelectedTab("artifact")} + > + Local File +
+
setSelectedTab("docker")} + > + Docker Image +
+
+ + {selectedTab === "artifact" && ( +
+ + setUploadFile(e.target.files[0])} + /> +
+ )} + + {selectedTab === "docker" && ( +
+ + setTextInput(e.target.value)} + /> +
+ )} +
+ + +
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js index 9df4e5b8..7e3b8186 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js @@ -8,6 +8,7 @@ import { } from "../../../../editor/util/camunda-utils/ImplementationTypeUtils"; import {getExtensionElementsList} from "../../../../editor/util/camunda-utils/ExtensionElementsUtil"; import {Deployment} from "./Deployment"; +import { ArtifactUpload } from './ArtifactUpload'; import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; /** @@ -93,6 +94,11 @@ export function ImplementationProps(props) { component: Deployment, isEdited: isTextFieldEntryEdited }); + entries.push({ + id: 'artifactUpload', + component: ArtifactUpload, + isEdited: isTextFieldEntryEdited + }) } return entries; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css new file mode 100644 index 00000000..42422073 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css @@ -0,0 +1,64 @@ +.tab { + flex: 1; + padding: 10px; + border-radius: 3px; + text-align: center; + cursor: pointer; + font-weight: bold; + font-stretch: normal; + font-style: normal; + line-height: normal; + letter-spacing: normal; + color: #fdfdfe; + margin: 1px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2); + border: solid 1px var(--blue-darken-62); + background-color: var(--blue-base-65); +} + +.tab.active { + background-color: var(--blue-darken-55); +} + +.tab:hover { + border: solid 1px var(--blue-darken-55); + background-color: var(--blue-darken-62); +} + +.tabButtonsContainer { + display: flex; + flex-direction: row; + align-items: center; + overflow: auto; + min-width: 120px; + max-height: 263px; + direction: ltr; +} + + +.wizard-button { + box-sizing: border-box; + outline: none; + background-color: transparent; + border: solid; + border-radius: 6px; + font-size: 15px; + font-family: Arial, serif; + font-weight: bold; + color: #636363; + padding: 3px 6px 2px; + margin: 2px 32px 6px 12px; +} + +.wizard-tab-content { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + padding: 3px 6px 2px; + column-gap: 10px; +} + +.dockerimage-input { + flex-grow: 2; +} \ No newline at end of file From fec13ce37d46b0f59a9a75e698ac13f293b8db1c Mon Sep 17 00:00:00 2001 From: LaviniaStiliadou Date: Fri, 23 Jun 2023 17:48:50 +0200 Subject: [PATCH 07/33] Add xml viewer (#70) * add xml viewer * remove ace * add border to resize editor * enable live update on diagram change * fix tests * increase initial size of xml viewer & limit max height * change size of modeler when xml viewer is enabled * remove jump after first resize * remove button from bottom left, add button to toolbar * add icon --- components/bpmn-q/.gitignore | 3 +- .../QuantumWorkflowModeler.js | 72 +++++++++++++++--- .../resources/icons/xml-viewer-icon.png | Bin 0 -> 4518 bytes .../editor/resources/styling/editor-ui.css | 25 +++++- .../editor/ui/ButtonToolbar.js | 3 +- .../editor/ui/XMLViewerButton.js | 63 +++++++++++++++ .../editor/util/IoUtilities.js | 23 +++--- .../rules/DataFlowRulesProvider.js | 20 ++++- .../bpmn-q/modeler-component/modeler.css | 35 +++++++++ components/bpmn-q/package-lock.json | 11 +++ components/bpmn-q/package.json | 17 +++-- components/bpmn-q/public/index.html | 18 ++++- 12 files changed, 255 insertions(+), 35 deletions(-) create mode 100644 components/bpmn-q/modeler-component/editor/resources/icons/xml-viewer-icon.png create mode 100644 components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js diff --git a/components/bpmn-q/.gitignore b/components/bpmn-q/.gitignore index c98920ae..010d43cb 100644 --- a/components/bpmn-q/.gitignore +++ b/components/bpmn-q/.gitignore @@ -1,4 +1,5 @@ node_modules/* public/* !public/index.html -bpmnlint-plugin-custom/node_modules/* \ No newline at end of file +!public/src +bpmnlint-plugin-custom/node_modules/* diff --git a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js index f12f9353..6fd80d63 100644 --- a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js +++ b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js @@ -22,6 +22,7 @@ import { getPluginButtons, getTransformationButtons } from "./editor/plugin/Plug import { getPluginConfig, setPluginConfig } from "./editor/plugin/PluginConfigHandler"; import * as editorConfig from './editor/config/EditorConfigManager'; import { initEditorEventHandler } from './editor/events/EditorEventHandler'; +import $ from 'jquery'; /** * The Quantum Workflow modeler HTML web component which contains the bpmn-js modeler to model BPMN diagrams, an editor @@ -80,7 +81,7 @@ export class QuantumWorkflowModeler extends HTMLElement {

-
+
@@ -95,14 +96,14 @@ export class QuantumWorkflowModeler extends HTMLElement { let startX; let startWidth; let width = panel.style.width; - var propertiesElement = document.getElementById("properties"); + let propertiesElement = document.getElementById("properties"); propertiesElement.addEventListener("mousemove", function (e) { - var rect = this.getBoundingClientRect(); - var x = e.clientX - rect.left; - var y = e.clientY - rect.top; + let rect = this.getBoundingClientRect(); + let x = e.clientX - rect.left; + let y = e.clientY - rect.top; - var borderSize = 5; + let borderSize = 5; if ( x < borderSize || @@ -133,14 +134,14 @@ export class QuantumWorkflowModeler extends HTMLElement { // Mouse down handler function handleMouseDown(event) { - var rect = panel.getBoundingClientRect(); - var x = event.clientX - rect.left; + let rect = panel.getBoundingClientRect(); + let x = event.clientX - rect.left; - var borderSize = 5; + let borderSize = 5; if ( x < borderSize || - x > rect.width - borderSize + x > rect.width - borderSize ) { isResizing = true; @@ -193,6 +194,52 @@ export class QuantumWorkflowModeler extends HTMLElement { isCollapsed = !isCollapsed; }); + + let editor = document.getElementById('editor'); + let dragging = false; + let aceEditor = ace.edit(editor); + + + $("#editor_dragbar").mousedown(function (e) { + e.preventDefault(); + dragging = true; + + let editorElement = $("#editor"); + let editor_wrap = $("#editor_wrap"); + let dragbar = $("#editor_dragbar"); + let startY = e.pageY; + let startTop = parseInt(editorElement.css("top")); + let startHeight = editor_wrap.height(); + + $(document).mousemove(function (e) { + if (!dragging) return; + + let actualY = e.pageY; + let deltaY = startY - actualY; + let newTop = startTop - deltaY; + let newHeight = startHeight + deltaY; + const viewportHeight = window.innerHeight; + const heightInVh = (newHeight / viewportHeight) * 100; + + // since we move the editor element up we need to add the actual height of the + // wrapper element + const editorHeight = 2 * newHeight; + if (newHeight >= 75 && heightInVh <= 89) { + editorElement.css("top", newTop + "px"); + editor_wrap.css("height", newHeight + "px"); + editorElement.css("height", editorHeight + "px"); + dragbar.css("top", newTop - dragbar.height() + "px"); + aceEditor.resize(); + } + }); + }); + + $(document).mouseup(function (e) { + if (dragging) { + dragging = false; + $(document).unbind("mousemove"); + } + }); } /** @@ -234,6 +281,11 @@ export class QuantumWorkflowModeler extends HTMLElement { // load initial workflow this.workflowModel = this.workflowModel || getPluginConfig('editor').defaultWorkflow; + getModeler().on('commandStack.changed', function () { + getModeler().saveXML({ format: true }).then(function (result) { + modeler.xml = result; + }) + }); if (this.workflowModel) { loadDiagram(this.workflowModel, getModeler()).then(); } else { diff --git a/components/bpmn-q/modeler-component/editor/resources/icons/xml-viewer-icon.png b/components/bpmn-q/modeler-component/editor/resources/icons/xml-viewer-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..95f342f3538850d13a71fc8bdc172d45a2a3f73b GIT binary patch literal 4518 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 literal 0 HcmV?d00001 diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css b/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css index 5652af37..a6ff1594 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css @@ -209,6 +209,16 @@ background: url("../icons/save-outline-icon.png") no-repeat center center; } +#editor { + position: absolute; + width: 100%; + height: 100%; +} + +.ace_print-margin { + display: none; +} + .qwm-icon-upload:before { content: ""; width: 15px; @@ -219,4 +229,17 @@ background-repeat: no-repeat; display: inline-block; float: left; -} \ No newline at end of file +} + +.qwm-icon-xml-viewer:before { + content: ""; + width: 20px; + height: 18px; + margin-top: 0px; + background-size: contain; + background-image: url("../icons/xml-viewer-icon.png"); + background-repeat: no-repeat; + display: inline-block; + float: left; +} + diff --git a/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js b/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js index 9e6da41e..2e2ac26b 100644 --- a/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js +++ b/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js @@ -7,7 +7,7 @@ import ConfigPlugin from "../config/ConfigPlugin"; import TransformationToolbarButton from "./TransformationToolbarButton"; import UploadButton from "./UploadButton"; import ShortcutPlugin from "../shortcut/ShortcutPlugin"; - +import XMLViewerButton from "./XMLViewerButton"; /** * React component which displays the toolbar of the modeler @@ -34,6 +34,7 @@ export default function ButtonToolbar(props) { +

diff --git a/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js b/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js new file mode 100644 index 00000000..aa59adfd --- /dev/null +++ b/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import { getModeler } from "../ModelerHandler"; +import ace from "ace-builds"; +import { loadDiagram } from "../util/IoUtilities"; + +/** + * React button which enables the XML Viewer. + * + * @returns {JSX.Element} + * @constructor + */ +export default function XMLViewerButton() { + + const [enabledXMLView, setEnabledXMLView] = useState(false); + function enableXMLViewer(enabledXMLView) { + let modelerContainer = document.getElementById('modeler-container'); + let editor = document.getElementById('editor'); + let editorWrap = document.getElementById('editor_wrap'); + let panel = document.getElementById("properties"); + let aceEditor = ace.edit(editor); + if (!enabledXMLView) { + modelerContainer.style.height = '93vh'; + editor.style.display = 'block'; + editor.style.height = '93vh'; + panel.style.display = 'none'; + editorWrap.style.display = 'block'; + + // Dynamically set the value of the editor + let xml = getModeler().xml; + if (xml.xml != undefined) { + xml = xml.xml; + } + aceEditor.setValue(xml); + } else { + modelerContainer.style.height = '98vh'; + editor.style.display = 'none'; + panel.style.display = 'block'; + editorWrap.style.display = 'none'; + + + aceEditor.getSession().on('change', function () { + update(); + }); + + function update() { + let xml = aceEditor.getSession().getValue(); + loadDiagram(xml, getModeler()); + } + } + + setEnabledXMLView(!enabledXMLView) + + } + + return ( + + ); +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/editor/util/IoUtilities.js b/components/bpmn-q/modeler-component/editor/util/IoUtilities.js index 9df49a7d..db34386b 100644 --- a/components/bpmn-q/modeler-component/editor/util/IoUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/IoUtilities.js @@ -30,7 +30,6 @@ const NEW_DIAGRAM_XML = '\n' + * @returns {Promise} */ export async function saveXmlAsLocalFile(xml, fileName = editorConfig.getFileName()) { - console.log(fileName); const bpmnFile = await new File([xml], fileName, { type: 'text/xml' }); const link = document.createElement('a'); @@ -82,19 +81,21 @@ export async function getXml(modeler) { * @returns {Promise} Undefined, if an error occurred during import. */ export async function loadDiagram(xml, modeler, dispatchEvent = true) { + if (xml !== '') { + try { + const result = await modeler.importXML(xml); + modeler.xml = xml; - try { - const result = await modeler.importXML(xml); - - if (dispatchEvent) { - dispatchWorkflowEvent(workflowEventTypes.LOADED, xml, editorConfig.getFileName()); - } + if (dispatchEvent) { + dispatchWorkflowEvent(workflowEventTypes.LOADED, xml, editorConfig.getFileName()); + } - return result; - } catch (err) { - console.error(err); + return result; + } catch (err) { + console.error(err); - return { error: err }; + return { error: err }; + } } } diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js b/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js index 65205bb1..63cb439a 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js @@ -5,6 +5,8 @@ import { } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; import * as consts from '../Constants'; import { isConnectedWith } from '../../../editor/util/ModellingUtilities'; +import { getModeler } from '../../../editor/ModelerHandler'; +import ace from 'ace-builds'; /** * Custom rules provider for the DataFlow elements. Extends the BpmnRules. @@ -22,7 +24,7 @@ export default class CustomRulesProvider extends BpmnRules { // copy took place eventBus.on('copyPaste.elementsCopied', event => { const { tree } = event; - + // persist in local storage, encoded as json localStorage.setItem('bpmnClipboard', JSON.stringify(tree)); }); @@ -65,6 +67,22 @@ export default class CustomRulesProvider extends BpmnRules { context.position ); }); + + // update xml viewer on diagram change + eventBus.on("commandStack.changed", function () { + let editor = document.getElementById('editor'); + let aceEditor = ace.edit(editor); + let modeler = getModeler(); + if (modeler) { + modeler.saveXML({ format: true }).then(function (result) { + if (result.xml != undefined) { + result = result.xml; + } + aceEditor.setValue(result); + }) + } + }); + } /** diff --git a/components/bpmn-q/modeler-component/modeler.css b/components/bpmn-q/modeler-component/modeler.css index 6495073e..15488b09 100644 --- a/components/bpmn-q/modeler-component/modeler.css +++ b/components/bpmn-q/modeler-component/modeler.css @@ -4,6 +4,23 @@ right: 0; } +.xml-viewer-button { + position: absolute; + bottom: 45px; + left: 20px; + z-index: 2; +} + +.ace_editor.ace-tm { + width: 99%; + top: 0%; + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 1; +} + #properties { cursor: default; } @@ -16,4 +33,22 @@ body { overflow-y: hidden; +} + +#editor_wrap { + position: relative; + border-bottom: 1px solid #222222; +} + +#editor_dragbar { + position: absolute; + top: 200; + /* Adjust the initial top position as needed */ + left: 0; + right: 0; + background-color: #222222; + height: 3px; + cursor: row-resize; + opacity: 1; + z-index: 3; } \ No newline at end of file diff --git a/components/bpmn-q/package-lock.json b/components/bpmn-q/package-lock.json index dae6feaf..41f5694e 100644 --- a/components/bpmn-q/package-lock.json +++ b/components/bpmn-q/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@bpmn-io/properties-panel": "^1.3.1", + "ace-builds": "^1.23.0", "autoprefixer": "^10.4.13", "bpmn-font": "^0.12.0", "bpmn-js": "^11.1.1", @@ -3209,6 +3210,11 @@ "node": ">= 0.6" } }, + "node_modules/ace-builds": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.23.0.tgz", + "integrity": "sha512-PKRuQaQkjIhg1zeqJPW1QFtJO2fx13Nlv7N/VLhQS/kIQ/2aGAgWvYVcE9X64gbSiLzPdY3jcxv5wJJpLj1gLw==" + }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -18457,6 +18463,11 @@ "negotiator": "0.6.3" } }, + "ace-builds": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.23.0.tgz", + "integrity": "sha512-PKRuQaQkjIhg1zeqJPW1QFtJO2fx13Nlv7N/VLhQS/kIQ/2aGAgWvYVcE9X64gbSiLzPdY3jcxv5wJJpLj1gLw==" + }, "acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", diff --git a/components/bpmn-q/package.json b/components/bpmn-q/package.json index 388a747f..1d7f909d 100644 --- a/components/bpmn-q/package.json +++ b/components/bpmn-q/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@bpmn-io/properties-panel": "^1.3.1", + "ace-builds": "^1.23.0", "autoprefixer": "^10.4.13", "bpmn-font": "^0.12.0", "bpmn-js": "^11.1.1", @@ -43,8 +44,8 @@ "min-dash": "^4.0.0", "min-dom": "^4.1.0", "node-fetch": "^2.1.2", - "process": "^0.11.10", "prefix-css-loader": "^1.0.0", + "process": "^0.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", "stream": "^0.0.2", @@ -66,12 +67,18 @@ "babel-loader": "^9.1.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babelify": "^10.0.0", + "bpmn-js-bpmnlint": "^0.14.0", + "bpmnlint": "^6.3.0", + "bpmnlint-loader": "^0.1.4", + "bpmnlint-plugin-camunda": "^0.4.2", + "bpmnlint-plugin-custom": "file:bpmnlint-plugin-custom", "browserify": "^14.5.0", "browserify-css": "^0.15.0", "canvg-browser": "^1.0.0", "chai": "^4.3.7", "chai-xml": "^0.4.0", "clean-webpack-plugin": "^4.0.0", + "client": "file:client", "copy-webpack-plugin": "^4.6.0", "css-loader": "^6.7.2", "cssify": "^1.0.3", @@ -111,12 +118,6 @@ "webpack": "^5.75.0", "webpack-cli": "^5.0.0", "webpack-dev-server": "^4.11.1", - "xmlbuilder": "^15.1.1", - "bpmn-js-bpmnlint": "^0.14.0", - "bpmnlint": "^6.3.0", - "bpmnlint-loader": "^0.1.4", - "bpmnlint-plugin-camunda": "^0.4.2", - "bpmnlint-plugin-custom": "file:bpmnlint-plugin-custom", - "client": "file:client" + "xmlbuilder": "^15.1.1" } } diff --git a/components/bpmn-q/public/index.html b/components/bpmn-q/public/index.html index 3c824082..e41729a9 100644 --- a/components/bpmn-q/public/index.html +++ b/components/bpmn-q/public/index.html @@ -1,16 +1,29 @@ + Workflow Modeler +
- + + + + + + \ No newline at end of file From bfabdf603aa47757554481042ae5402650063d91 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Fri, 30 Jun 2023 00:43:32 +0200 Subject: [PATCH 08/33] Feature/9 visualize deployment model (#17) * Add deployment model visualization Ref: #9 * Add deployment model visualization Ref: #9 * Orientate at top-node * refactor stroke style * add test * add test * add test * fix top level node detection behavior and error handling * Fix crash on hide --------- Co-authored-by: Maximilian Kuhn --- components/bpmn-q/karma.conf.js | 1 + .../editor/util/RenderUtilities.js | 18 +- .../opentosca/deployment/OpenTOSCAUtils.js | 48 +++- .../opentosca/modeling/OpenToscaRenderer.js | 226 ++++++++++++++++++ .../extensions/opentosca/modeling/index.js | 8 +- .../resources/show-deployment-button.svg | 1 + components/bpmn-q/package-lock.json | 98 ++++---- components/bpmn-q/package.json | 1 - .../bpmn-q/test/tests/editor/plugin.spec.js | 4 +- .../deployment-model-renderer.spec.js | 29 +++ components/bpmn-q/webpack.config.js | 10 +- 11 files changed, 379 insertions(+), 65 deletions(-) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/show-deployment-button.svg create mode 100644 components/bpmn-q/test/tests/opentosca/deployment-model-renderer.spec.js diff --git a/components/bpmn-q/karma.conf.js b/components/bpmn-q/karma.conf.js index 459dd532..ebf95197 100644 --- a/components/bpmn-q/karma.conf.js +++ b/components/bpmn-q/karma.conf.js @@ -25,6 +25,7 @@ module.exports = function (config) { 'test/tests/quantme/quantme-config.spec.js', 'test/tests/opentosca/opentosca-config.spec.js', 'test/tests/opentosca/deployment-utils.spec.js', + 'test/tests/opentosca/deployment-model-renderer.spec.js', 'test/tests/dataflow/data-flow-transformation.spec.js', 'test/tests/dataflow/data-flow-plugin-config.spec.js', 'test/tests/dataflow/data-flow-configurations-endpoint.spec.js', diff --git a/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js b/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js index 9a9b52fa..5395db0b 100644 --- a/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js @@ -61,8 +61,10 @@ function drawRect(parentNode, width, height, borderRadius, color) { * @param parentGfx The parent element the SVG is drawn in * @param importSVG The SVG * @param svgAttributes Attributes for the SVG + * @param foreground true if SVG should be above Task + * @returns The created svg element */ -export function drawTaskSVG(parentGfx, importSVG, svgAttributes) { +export function drawTaskSVG(parentGfx, importSVG, svgAttributes, foreground) { const innerSvgStr = importSVG.svg, transformDef = importSVG.transform; @@ -70,15 +72,21 @@ export function drawTaskSVG(parentGfx, importSVG, svgAttributes) { svgAttr(groupDef, {transform: transformDef}); innerSVG(groupDef, innerSvgStr); - // set task box opacity to 0 such that icon can be in the background - svgAttr(svgSelect(parentGfx, 'rect'), {'fill-opacity': 0}); + if(!foreground) { + // set task box opacity to 0 such that icon can be in the background + svgAttr(svgSelect(parentGfx, 'rect'), { 'fill-opacity': 0 }); + } if (svgAttributes) { svgAttr(groupDef, svgAttributes); } - // draw svg in the background - parentGfx.prepend(groupDef); + if(foreground) { + parentGfx.append(groupDef); + } else { + parentGfx.prepend(groupDef); + } + return groupDef; } /** 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 38dd157c..00737290 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -10,7 +10,7 @@ */ import {fetch} from 'whatwg-fetch'; -import {performAjax} from '../utilities/Utilities'; +import * as config from "../framework-config/config-manager"; /** * Upload the CSAR located at the given URL to the connected OpenTOSCA Container and return the corresponding URL and required input parameters @@ -224,6 +224,52 @@ export async function createServiceInstance(csar, camundaEngineEndpoint) { return result; } +export async function loadTopology(deploymentModelUrl) { + console.log(config.getWineryEndpoint()) + if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { + deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); + } + let topology; + let tags; + try { + topology = await fetch(deploymentModelUrl.replace('?csar', 'topologytemplate')) + .then(res => res.json()); + tags = await fetch(deploymentModelUrl.replace('?csar', 'tags')) + .then(res => res.json()); + + } catch (e) { + throw new Error('An unexpected error occurred during loading the deployments models topology.'); + } + let topNode; + const topNodeTag = tags.find(tag => tag.name === "top-node"); + if (topNodeTag) { + const topNodeId = topNodeTag.value; + topNode = topology.nodeTemplates.find(nodeTemplate => nodeTemplate.id === topNodeId); + if (!topNode) { + throw new Error(`Top level node "${topNodeId}" not found.`); + } + } else { + let nodes = new Map(topology.nodeTemplates.map(nodeTemplate => [nodeTemplate.id, nodeTemplate])); + for(let relationship of topology.relationshipTemplates) { + if(relationship.name === "HostedOn") { + nodes.delete(relationship.targetElement.ref); + } + } + if(nodes.size === 1) { + topNode = nodes.values().next().value; + } + } + if (!topNode) { + throw new Error("No top level node found."); + } + + return { + topNode, + nodeTemplates: topology.nodeTemplates, + relationshipTemplates: topology.relationshipTemplates + }; +} + function makeId(length) { let result = ''; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js new file mode 100644 index 00000000..fe46a320 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js @@ -0,0 +1,226 @@ +import { + connectRectangles +} from 'diagram-js/lib/layout/ManhattanLayout'; + +import { + createLine, +} from 'diagram-js/lib/util/RenderUtil'; + +import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer'; + +import buttonIcon from '../resources/show-deployment-button.svg?raw'; +import {drawTaskSVG} from '../../../editor/util/RenderUtilities'; +import * as config from '../framework-config/config-manager'; +import NotificationHandler from '../../../editor/ui/notifications/NotificationHandler'; +import {append as svgAppend, attr as svgAttr, create as svgCreate, select, prepend as svgPrepend} from 'tiny-svg'; +import {query as domQuery} from 'min-dom'; +import {loadTopology} from "../deployment/OpenTOSCAUtils"; + +const HIGH_PRIORITY = 14001; +const SERVICE_TASK_TYPE = 'bpmn:ServiceTask'; +const DEPLOYMENT_GROUP_ID = 'deployment'; +const DEPLOYMENT_REL_MARKER_ID = 'deployment-rel'; + +const NODE_WIDTH = 100; +const NODE_HEIGHT = 60; +const STROKE_STYLE = { + strokeLinecap: 'round', + strokeLinejoin: 'round', + stroke: '#777777', + strokeWidth: 2, + strokeDasharray: 4, +}; + +export default class OpenToscaRenderer extends BpmnRenderer { + constructor(config, eventBus, styles, pathMap, canvas, textRenderer) { + super(config, eventBus, styles, pathMap, canvas, textRenderer, HIGH_PRIORITY); + this.styles = styles; + this.textRenderer = textRenderer; + + this.openToscaHandlers = { + [SERVICE_TASK_TYPE]: function (self, parentGfx, element) { + const task = self.renderer('bpmn:ServiceTask')(parentGfx, element); + self.maybeAddShowDeploymentModelButton(parentGfx, element); + return task; + } + }; + this.addMarkerDefinition(canvas); + } + + addMarkerDefinition(canvas) { + const marker = svgCreate('marker', { + id: DEPLOYMENT_REL_MARKER_ID, + viewBox: '0 0 8 8', + refX: 8, + refY: 4, + markerWidth: 8, + markerHeight: 8, + orient: 'auto' + }); + svgAppend(marker, svgCreate('path', { + d: 'M 0 0 L 8 4 L 0 8', + ...this.styles.computeStyle({}, ['no-fill'], { + ...STROKE_STYLE, + strokeWidth: 1, + strokeDasharray: 2 + }) + })); + + let defs = domQuery('defs', canvas._svg); + if (!defs) { + defs = svgCreate('defs'); + + svgPrepend(canvas._svg, defs); + } + svgAppend(defs, marker); + } + + maybeAddShowDeploymentModelButton(parentGfx, element) { + let deploymentModelUrl = element.businessObject.get('opentosca:deploymentModelUrl'); + if (!deploymentModelUrl) return; + + const button = drawTaskSVG(parentGfx, { + transform: 'matrix(0.3, 0, 0, 0.3, 85, 65)', + svg: buttonIcon + }, null, true); + button.style['pointer-events'] = 'all'; + button.style['cursor'] = 'pointer'; + button.addEventListener('click', (e) => { + element.showDeploymentModel = !element.showDeploymentModel; + e.preventDefault(); + if (element.showDeploymentModel) { + this.showDeploymentModel(parentGfx, element, deploymentModelUrl); + } else { + this.removeDeploymentModel(parentGfx); + } + }); + if (element.showDeploymentModel) { + this.showDeploymentModel(parentGfx, element, deploymentModelUrl); + } + } + + async showDeploymentModel(parentGfx, element, deploymentModelUrl) { + if (!element.deploymentModelTopology || element.loadedDeploymentModelUrl !== deploymentModelUrl) { + try { + const topology = await loadTopology(deploymentModelUrl); + element.loadedDeploymentModelUrl = deploymentModelUrl; + element.deploymentModelTopology = topology; + } catch (e) { + element.showDeploymentModel = false; + element.loadedDeploymentModelUrl = null; + element.deploymentModelTopology = null; + this.removeDeploymentModel(parentGfx); + console.error(e); + NotificationHandler.getInstance().displayNotification({ + type: 'warning', + title: 'Could not load topology', + content: e.message, + duration: 2000 + }); + } + } + const groupDef = svgCreate('g', {id: DEPLOYMENT_GROUP_ID}); + parentGfx.append(groupDef); + + const {nodeTemplates, relationshipTemplates, topNode} = element.deploymentModelTopology; + + let ySubtract = parseInt(topNode.y); + let xSubtract = parseInt(topNode.x); + + const positions = new Map(); + for (let nodeTemplate of nodeTemplates) { + const position = { + x: (parseInt(nodeTemplate.x) - xSubtract) / 1.4, + y: (parseInt(nodeTemplate.y) - ySubtract) / 1.4, + }; + + positions.set(nodeTemplate.id, position); + if (nodeTemplate.id !== topNode.id) { + this.drawNodeTemplate(groupDef, nodeTemplate, position); + } + } + + for (let relationshipTemplate of relationshipTemplates) { + const start = positions.get(relationshipTemplate.sourceElement.ref); + const end = positions.get(relationshipTemplate.targetElement.ref); + this.drawRelationship(groupDef, + start, relationshipTemplate.sourceElement.ref === topNode.id, + end, relationshipTemplate.targetElement.ref === topNode.id); + } + } + + removeDeploymentModel(parentGfx) { + const group = select(parentGfx, '#' + DEPLOYMENT_GROUP_ID) + if (group) { + group.remove(); + } + } + + drawRelationship(parentGfx, start, startIsToplevel, end, endIsToplevel) { + const line = createLine(connectRectangles({ + width: NODE_WIDTH, + height: startIsToplevel ? 80 : NODE_HEIGHT, + ...start + }, { + width: NODE_WIDTH, + height: endIsToplevel ? 80 : NODE_HEIGHT, + ...end + }), this.styles.computeStyle({}, ['no-fill'], { + ...STROKE_STYLE, + markerEnd: `url(#${DEPLOYMENT_REL_MARKER_ID})` + }), 5); + parentGfx.prepend(line); + } + + drawNodeTemplate(parentGfx, nodeTemplate, position) { + const groupDef = svgCreate('g'); + svgAttr(groupDef, {transform: `matrix(1, 0, 0, 1, ${position.x.toFixed(2)}, ${position.y.toFixed(2)})`}); + const rect = svgCreate('rect', { + width: NODE_WIDTH, + height: NODE_HEIGHT, + fill: '#DDDDDD', + ...STROKE_STYLE + }); + + svgAppend(groupDef, rect); + + const text = this.textRenderer.createText(nodeTemplate.name, { + box: { + width: NODE_WIDTH, + height: NODE_HEIGHT, + }, + align: 'center-middle' + }); + svgAppend(groupDef, text); + parentGfx.append(groupDef); + } + + renderer(type) { + return this.handlers[type]; + } + + canRender(element) { + // only return true if handler for rendering is registered + return this.openToscaHandlers[element.type]; + } + + drawShape(parentNode, element) { + if (element.type in this.openToscaHandlers) { + const h = this.openToscaHandlers[element.type]; + return h(this, parentNode, element); + } + return super.drawShape(parentNode, element); + } +} + +OpenToscaRenderer.$inject = [ + 'config', + 'eventBus', + 'styles', + 'pathMap', + 'canvas', + 'textRenderer' +]; + + + diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js index bd77aff5..5c43a562 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js @@ -8,9 +8,11 @@ * * SPDX-License-Identifier: Apache-2.0 */ -import ServiceTaskPropertiesProvider from "./properties-provider/ServiceTaskPropertiesProvider"; +import ServiceTaskPropertiesProvider from './properties-provider/ServiceTaskPropertiesProvider' +import OpenToscaRenderer from './OpenToscaRenderer' export default { - __init__: ['customPropertiesProvider'], - customPropertiesProvider: ['type', ServiceTaskPropertiesProvider] + __init__: ['openToscaRenderer', 'customPropertiesProvider'], + openToscaRenderer: ['type', OpenToscaRenderer], + customPropertiesProvider: ['type', ServiceTaskPropertiesProvider], }; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/show-deployment-button.svg b/components/bpmn-q/modeler-component/extensions/opentosca/resources/show-deployment-button.svg new file mode 100644 index 00000000..8734f9a8 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/resources/show-deployment-button.svg @@ -0,0 +1 @@ +D \ No newline at end of file diff --git a/components/bpmn-q/package-lock.json b/components/bpmn-q/package-lock.json index 41f5694e..582df31b 100644 --- a/components/bpmn-q/package-lock.json +++ b/components/bpmn-q/package-lock.json @@ -62,7 +62,7 @@ "browserify-css": "^0.15.0", "canvg-browser": "^1.0.0", "chai": "^4.3.7", - "chai-xml": "^0.4.0", + "chai-xml": "^0.4.1", "clean-webpack-plugin": "^4.0.0", "client": "file:client", "copy-webpack-plugin": "^4.6.0", @@ -4719,12 +4719,12 @@ } }, "node_modules/chai-xml": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chai-xml/-/chai-xml-0.4.0.tgz", - "integrity": "sha512-VjFPW64Hcp9CuuZbAC26cBWi+DPhyWOW8yxNpfQX3W+jQLPJxN/sm5FAaW+FOKTzsNeIFQpt5yhGbZA5s/pEyg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/chai-xml/-/chai-xml-0.4.1.tgz", + "integrity": "sha512-VUf5Ol4ifOAsgz+lN4tfWENgQtrKxHPWsmpL5wdbqQdkpblZkcDlaT2aFvsPQH219Yvl8vc4064yFErgBIn9bw==", "dev": true, "dependencies": { - "xml2js": "^0.4.23" + "xml2js": "^0.5.0" }, "engines": { "node": ">= 0.8.0" @@ -4733,28 +4733,6 @@ "chai": ">=1.10.0 " } }, - "node_modules/chai-xml/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/chai-xml/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -16174,6 +16152,28 @@ "node": ">=0.10.0" } }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -19919,30 +19919,12 @@ } }, "chai-xml": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chai-xml/-/chai-xml-0.4.0.tgz", - "integrity": "sha512-VjFPW64Hcp9CuuZbAC26cBWi+DPhyWOW8yxNpfQX3W+jQLPJxN/sm5FAaW+FOKTzsNeIFQpt5yhGbZA5s/pEyg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/chai-xml/-/chai-xml-0.4.1.tgz", + "integrity": "sha512-VUf5Ol4ifOAsgz+lN4tfWENgQtrKxHPWsmpL5wdbqQdkpblZkcDlaT2aFvsPQH219Yvl8vc4064yFErgBIn9bw==", "dev": true, "requires": { - "xml2js": "^0.4.23" - }, - "dependencies": { - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true - } + "xml2js": "^0.5.0" } }, "chalk": { @@ -28821,6 +28803,24 @@ "integrity": "sha512-dTaaRwm4ccF8UF15/PLT3pNNlZP04qko/FUcr0QBppYLk8+J7xA9gg2vI2X4Kr1PcJAVxwI9NdADex29FX2QVQ==", "dev": true }, + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "dependencies": { + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + } + } + }, "xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/components/bpmn-q/package.json b/components/bpmn-q/package.json index 1d7f909d..b6e016f3 100644 --- a/components/bpmn-q/package.json +++ b/components/bpmn-q/package.json @@ -76,7 +76,6 @@ "browserify-css": "^0.15.0", "canvg-browser": "^1.0.0", "chai": "^4.3.7", - "chai-xml": "^0.4.0", "clean-webpack-plugin": "^4.0.0", "client": "file:client", "copy-webpack-plugin": "^4.6.0", diff --git a/components/bpmn-q/test/tests/editor/plugin.spec.js b/components/bpmn-q/test/tests/editor/plugin.spec.js index bf255c3a..ca1ccfc7 100644 --- a/components/bpmn-q/test/tests/editor/plugin.spec.js +++ b/components/bpmn-q/test/tests/editor/plugin.spec.js @@ -55,9 +55,9 @@ describe('Test plugins', function () { 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(2); + expect(buttons.length).to.equal(3); expect(tabs.length).to.equal(9); - expect(styles.length).to.equal(3); + expect(styles.length).to.equal(4); }); }); }); diff --git a/components/bpmn-q/test/tests/opentosca/deployment-model-renderer.spec.js b/components/bpmn-q/test/tests/opentosca/deployment-model-renderer.spec.js new file mode 100644 index 00000000..e458bba0 --- /dev/null +++ b/components/bpmn-q/test/tests/opentosca/deployment-model-renderer.spec.js @@ -0,0 +1,29 @@ +import {createTempModeler} from "../../../modeler-component/editor/ModelerHandler"; +import {setPluginConfig} from "../../../modeler-component/editor/plugin/PluginConfigHandler"; +import {create as svgCreate} from "tiny-svg"; +import {expect} from 'chai'; + +const deploymentModelTopology = { "topNode": { "id": "RealWorld-Application-Backend_Java11-Spring-w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "390", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "112" }, "properties": { "propertyType": "KV", "namespace": "http://www.example.org", "elementName": "Properties", "kvproperties": { "AppName": "", "Port": "", "context_root": "" } }, "type": "{http://opentosca.org/example/applications/nodetypes}RealWorld-Application-Backend_Java11-Spring-w1", "name": "RealWorld-Application-Backend", "minInstances": 1, "maxInstances": "1", "x": "390", "y": "112" }, "nodeTemplates": [ { "id": "MariaDB_10-w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "657", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "450" }, "properties": { "propertyType": "KV", "namespace": "http://opentosca.org/nodetypes/propertiesdefinition/winery", "elementName": "properties", "kvproperties": { "DBName": "", "DBUser": "", "DBPassword": "" } }, "type": "{http://opentosca.org/nodetypes}MariaDB_10-w1", "name": "MariaDB", "minInstances": 1, "maxInstances": "1", "x": "657", "y": "450" }, { "id": "Java_11-w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "352", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "281" }, "properties": { "propertyType": "KV", "namespace": "http://www.example.org", "elementName": "Properties", "kvproperties": { "component_version": "", "admin_credential": "" } }, "type": "{http://opentosca.org/nodetypes}Java_11-w1", "name": "Java", "minInstances": 1, "maxInstances": "1", "x": "352", "y": "281" }, { "id": "Ubuntu-VM_18.04-w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "390", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "619" }, "properties": { "propertyType": "KV", "namespace": "http://opentosca.org/nodetypes", "elementName": "VirtualMachineProperties", "kvproperties": { "instanceRef": "", "VMIP": "", "VMInstanceID": "", "VMType": "", "VMUserName": "", "VMUserPassword": "", "VMPrivateKey": "", "VMPublicKey": "", "VMKeyPairName": "" } }, "type": "{http://opentosca.org/nodetypes}Ubuntu-VM_18.04-w1", "name": "Ubuntu-VM", "minInstances": 1, "maxInstances": "1", "x": "390", "y": "619" }, { "id": "DockerContainer_w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "352", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "450" }, "properties": { "propertyType": "KV", "namespace": "http://opentosca.org/nodetypes/properties", "elementName": "properties", "kvproperties": { "Port": "", "ContainerPort": "", "ContainerID": "", "ContainerIP": "", "ImageID": "", "ContainerMountPath": "", "HostMountFiles": "", "PrivilegedMode": "" } }, "type": "{http://opentosca.org/nodetypes}DockerContainer_w1", "name": "DockerContainer_w1", "minInstances": 1, "maxInstances": "1", "x": "352", "y": "450" }, { "id": "RealWorld-Application-Backend_Java11-Spring-w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "390", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "112" }, "properties": { "propertyType": "KV", "namespace": "http://www.example.org", "elementName": "Properties", "kvproperties": { "AppName": "", "Port": "", "context_root": "" } }, "type": "{http://opentosca.org/example/applications/nodetypes}RealWorld-Application-Backend_Java11-Spring-w1", "name": "RealWorld-Application-Backend", "minInstances": 1, "maxInstances": "1", "x": "390", "y": "112" }, { "id": "AWS_w1_0", "documentation": [], "any": [], "otherAttributes": { "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": "390", "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": "788" }, "properties": { "propertyType": "KV", "namespace": "http://opentosca.org/nodetypes", "elementName": "AWS_w1", "kvproperties": { "AWS_ACCESS_KEY_ID": "", "AWS_SECRET_ACCESS_KEY": "", "AWS_REGION": "" } }, "type": "{http://opentosca.org/nodetypes}AWS_w1", "name": "AWS_w1", "minInstances": 1, "maxInstances": "1", "x": "390", "y": "788" } ], "relationshipTemplates": [ { "id": "con_AttachesTo_0", "documentation": [], "any": [], "otherAttributes": {}, "type": "{http://docs.oasis-open.org/tosca/ToscaNormativeTypes/relationshiptypes}AttachesTo", "sourceElement": { "ref": "MariaDB_10-w1_0" }, "targetElement": { "ref": "Ubuntu-VM_18.04-w1_0" }, "name": "AttachesTo" }, { "id": "con_HostedOn_0", "documentation": [], "any": [], "otherAttributes": {}, "type": "{http://docs.oasis-open.org/tosca/ns/2011/12/ToscaBaseTypes}HostedOn", "sourceElement": { "ref": "DockerContainer_w1_0" }, "targetElement": { "ref": "Ubuntu-VM_18.04-w1_0" }, "name": "HostedOn" }, { "id": "con_HostedOn_1", "documentation": [], "any": [], "otherAttributes": {}, "type": "{http://docs.oasis-open.org/tosca/ns/2011/12/ToscaBaseTypes}HostedOn", "sourceElement": { "ref": "Java_11-w1_0" }, "targetElement": { "ref": "DockerContainer_w1_0" }, "name": "HostedOn" }, { "id": "con_HostedOn_2", "documentation": [], "any": [], "otherAttributes": {}, "type": "{http://docs.oasis-open.org/tosca/ns/2011/12/ToscaBaseTypes}HostedOn", "sourceElement": { "ref": "RealWorld-Application-Backend_Java11-Spring-w1_0" }, "targetElement": { "ref": "Java_11-w1_0" }, "name": "HostedOn" }, { "id": "con_HostedOn_3", "documentation": [], "any": [], "otherAttributes": {}, "type": "{http://docs.oasis-open.org/tosca/ns/2011/12/ToscaBaseTypes}HostedOn", "sourceElement": { "ref": "Ubuntu-VM_18.04-w1_0" }, "targetElement": { "ref": "AWS_w1_0" }, "name": "HostedOn" }, { "id": "con_ConnectsTo_0", "documentation": [], "any": [], "otherAttributes": {}, "properties": { "propertyType": "KV", "namespace": "http://docs.oasis-open.org/tosca/ns/2011/12/ToscaBaseTypes/propertiesdefinition/winery", "elementName": "properties", "kvproperties": { "ChannelType": "" } }, "type": "{http://docs.oasis-open.org/tosca/ns/2011/12/ToscaBaseTypes}ConnectsTo", "sourceElement": { "ref": "Java_11-w1_0" }, "targetElement": { "ref": "MariaDB_10-w1_0" }, "name": "ConnectsTo" } ]}; + +describe('Test the OpenTosca renderer', function () { + it("Should render deployment model", () => { + setPluginConfig([{ + name: 'opentosca', + config: { + test: 'test', + } + }]); + const modeler = createTempModeler(); + const renderer = modeler.get("openToscaRenderer"); + const loadedDeploymentModelUrl = "moz://a"; + + const parentGfx = svgCreate("g"); + + renderer.showDeploymentModel(parentGfx, { + loadedDeploymentModelUrl, + deploymentModelTopology + }, loadedDeploymentModelUrl); + + expect(parentGfx.innerHTML).to.contain(""); + }); +}); \ No newline at end of file diff --git a/components/bpmn-q/webpack.config.js b/components/bpmn-q/webpack.config.js index 698931b1..6688f7d3 100644 --- a/components/bpmn-q/webpack.config.js +++ b/components/bpmn-q/webpack.config.js @@ -15,6 +15,7 @@ module.exports = { rules: [ { test: /\.(png|svg|jpg|jpeg|gif)$/i, + resourceQuery: { not: [/raw/] }, type: 'asset/inline', }, { @@ -51,6 +52,10 @@ module.exports = { test: /\.bpmn$/, type: "asset/source", }, + { + resourceQuery: /raw/, + type: 'asset/source', + } ], }, resolve: { @@ -65,9 +70,6 @@ module.exports = { AWS_RUNTIME_HANDLER_ENDPOINT: 'http://localhost:8890', CAMUNDA_ENDPOINT: 'http://localhost:8080/engine-rest', DATA_CONFIG: 'http://localhost:8100/data-objects', - GITHUB_TOKEN: '', - NISQ_ANALYZER_ENDPOINT: 'http://localhost:8098/nisq-analyzer', - OPENTOSCA_ENDPOINT: 'http://localhost:1337/csars', PROVENANCE_COLLECTION: false, DOWNLOAD_FILE_NAME: 'quantum-workflow-model', ENABLE_DATA_FLOW_PLUGIN: true, @@ -92,7 +94,7 @@ module.exports = { UPLOAD_FILE_NAME: 'workflow', UPLOAD_GITHUB_REPO: '', UPLOAD_GITHUB_USER: '', - WINERY_ENDPOINT: 'http://localhost:8081/winery' + WINERY_ENDPOINT: 'http://localhost:8080/winery' }) ], mode: 'development', From 827c620f689b2f079c29e9e53652b6b35cef46e2 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 3 Jul 2023 22:37:29 +0200 Subject: [PATCH 09/33] Feature/11 deployment model show all button (#18) --- .../opentosca/modeling/OpenToscaRenderer.js | 61 +++++++++++++--- .../deployment/services/DeploymentPlugin.js | 33 +++++++-- components/bpmn-q/package-lock.json | 71 ++----------------- 3 files changed, 83 insertions(+), 82 deletions(-) diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js index fe46a320..a96f03ef 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js @@ -32,8 +32,10 @@ const STROKE_STYLE = { }; export default class OpenToscaRenderer extends BpmnRenderer { - constructor(config, eventBus, styles, pathMap, canvas, textRenderer) { + constructor(config, eventBus, styles, pathMap, canvas, textRenderer, commandStack, elementRegistry) { super(config, eventBus, styles, pathMap, canvas, textRenderer, HIGH_PRIORITY); + this.commandStack = commandStack; + this.elementRegistry = elementRegistry; this.styles = styles; this.textRenderer = textRenderer; @@ -45,6 +47,46 @@ export default class OpenToscaRenderer extends BpmnRenderer { } }; this.addMarkerDefinition(canvas); + this.registerShowDeploymentModelHandler(); + } + + registerShowDeploymentModelHandler() { + this.commandStack.register("deploymentModel.showAll", { + execute: ({showDeploymentModel}) => { + const elementsWithDeploymentModel = this.elementRegistry + .filter(element => element.businessObject.get('opentosca:deploymentModelUrl')); + const changed = []; + for (const element of elementsWithDeploymentModel) { + const wasShownDeploymentModel = !!element.showDeploymentModel; + element.showDeploymentModel = showDeploymentModel; + element.wasShownDeploymentModel = wasShownDeploymentModel; + if (wasShownDeploymentModel !== showDeploymentModel) { + changed.push(element); + } + } + return changed; + }, + + revert({showDeploymentModel}) { + return this.execute({showDeploymentModel: !showDeploymentModel}); + } + }); + + this.commandStack.register("deploymentModel.show", { + execute: ({element, showDeploymentModel}) => { + const wasShownDeploymentModel = !!element.showDeploymentModel; + element.showDeploymentModel = showDeploymentModel; + element.wasShownDeploymentModel = wasShownDeploymentModel; + if (wasShownDeploymentModel !== showDeploymentModel) { + return [element]; + } + return []; + }, + + revert({element, showDeploymentModel}) { + return this.execute({element, showDeploymentModel: !showDeploymentModel}); + } + }); } addMarkerDefinition(canvas) { @@ -86,16 +128,16 @@ export default class OpenToscaRenderer extends BpmnRenderer { button.style['pointer-events'] = 'all'; button.style['cursor'] = 'pointer'; button.addEventListener('click', (e) => { - element.showDeploymentModel = !element.showDeploymentModel; e.preventDefault(); - if (element.showDeploymentModel) { - this.showDeploymentModel(parentGfx, element, deploymentModelUrl); - } else { - this.removeDeploymentModel(parentGfx); - } + this.commandStack.execute("deploymentModel.show", { + element: element, + showDeploymentModel: !element.showDeploymentModel + }); }); if (element.showDeploymentModel) { this.showDeploymentModel(parentGfx, element, deploymentModelUrl); + } else { + this.removeDeploymentModel(parentGfx); } } @@ -117,6 +159,7 @@ export default class OpenToscaRenderer extends BpmnRenderer { content: e.message, duration: 2000 }); + return; } } const groupDef = svgCreate('g', {id: DEPLOYMENT_GROUP_ID}); @@ -219,7 +262,9 @@ OpenToscaRenderer.$inject = [ 'styles', 'pathMap', 'canvas', - 'textRenderer' + 'textRenderer', + 'commandStack', + 'elementRegistry' ]; 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 ce2c8f05..7f1c9e91 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 @@ -22,6 +22,7 @@ import {getServiceTasksToDeploy} from '../../../deployment/DeploymentUtils'; import {getModeler} from "../../../../../editor/ModelerHandler"; import NotificationHandler from "../../../../../editor/ui/notifications/NotificationHandler"; import {getRootProcess} from '../../../../../editor/util/ModellingUtilities'; +import ExtensibleButton from "../../../../../editor/ui/ExtensibleButton"; const defaultState = { windowOpenDeploymentOverview: false, @@ -42,8 +43,8 @@ export default class DeploymentPlugin extends PureComponent { } componentDidMount() { - this.modeler = getModeler(); + this.commandStack = this.modeler.get("commandStack"); } /** @@ -299,17 +300,35 @@ export default class DeploymentPlugin extends PureComponent { return csarsToDeploy; } - render() { + showDeployment(show) { + this.commandStack.execute("deploymentModel.showAll", { + showDeploymentModel: show + }); + } + render() { // render deployment button and pop-up menu return ( -
- , + , + -
+ ]}/> {this.state.windowOpenDeploymentOverview && ( =4" } }, - "node_modules/chai-xml": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/chai-xml/-/chai-xml-0.4.1.tgz", - "integrity": "sha512-VUf5Ol4ifOAsgz+lN4tfWENgQtrKxHPWsmpL5wdbqQdkpblZkcDlaT2aFvsPQH219Yvl8vc4064yFErgBIn9bw==", - "dev": true, - "dependencies": { - "xml2js": "^0.5.0" - }, - "engines": { - "node": ">= 0.8.0" - }, - "peerDependencies": { - "chai": ">=1.10.0 " - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -13529,7 +13513,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "dev": true, + "optional": true }, "node_modules/saxen": { "version": "8.1.2", @@ -16152,28 +16137,6 @@ "node": ">=0.10.0" } }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -19918,15 +19881,6 @@ "type-detect": "^4.0.5" } }, - "chai-xml": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/chai-xml/-/chai-xml-0.4.1.tgz", - "integrity": "sha512-VUf5Ol4ifOAsgz+lN4tfWENgQtrKxHPWsmpL5wdbqQdkpblZkcDlaT2aFvsPQH219Yvl8vc4064yFErgBIn9bw==", - "dev": true, - "requires": { - "xml2js": "^0.5.0" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -26767,7 +26721,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "dev": true, + "optional": true }, "saxen": { "version": "8.1.2", @@ -28803,24 +28758,6 @@ "integrity": "sha512-dTaaRwm4ccF8UF15/PLT3pNNlZP04qko/FUcr0QBppYLk8+J7xA9gg2vI2X4Kr1PcJAVxwI9NdADex29FX2QVQ==", "dev": true }, - "xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "dependencies": { - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true - } - } - }, "xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", From a3ea1bc4a649ae14dc2b4a16e733e6477cec1c33 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Wed, 12 Jul 2023 13:55:18 +0200 Subject: [PATCH 10/33] Fix/upstream pr comments (#21) --- .../QuantumWorkflowModeler.js | 2 +- .../properties-provider/ArtifactUpload.js | 37 ++++-------------- .../ImplementationProps.js | 3 +- .../properties-provider/artifact-modal.css | 23 ++++++----- .../opentosca/resources/hide-icon.png | Bin 0 -> 29791 bytes .../opentosca/resources/opentosca-icon.png | Bin 0 -> 12345 bytes .../opentosca/resources/show-icon.png | Bin 0 -> 25199 bytes .../opentosca/styling/opentosca.css | 24 +++++++++++- .../deployment/services/DeploymentPlugin.js | 16 ++++---- .../opentosca-plugin/OpenToscaUI.jpg | Bin 0 -> 82166 bytes .../opentosca-plugin/opentosca-plugin.md | 25 ++++++++++++ .../quantme-plugin/quantme-plugin.md | 1 - 12 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/hide-icon.png create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca-icon.png create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/resources/show-icon.png create mode 100644 doc/quantum-workflow-modeler/extensions/opentosca-plugin/OpenToscaUI.jpg create mode 100644 doc/quantum-workflow-modeler/extensions/opentosca-plugin/opentosca-plugin.md diff --git a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js index 6fd80d63..02b92ae4 100644 --- a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js +++ b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js @@ -84,7 +84,7 @@ export class QuantumWorkflowModeler extends HTMLElement {
-
+
`; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js index e735656e..ae5bcd72 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js @@ -1,48 +1,27 @@ import {HeaderButton} from '@bpmn-io/properties-panel'; -import {useService} from 'bpmn-js-properties-panel'; import React from 'react'; import ArtifactWizardModal from './ArtifactWizardModal'; import {createRoot} from 'react-dom/client'; -import {useState} from 'react'; import './artifact-modal.css'; /** * Entry to display the button which opens the Artifact Wizard, a dialog which allows to upload */ export function ArtifactUpload(props) { - const {element} = props; - const translate = useService('translate'); + const {translate} = props; const onClick = () => { // render config button and pop-up menu - console.log("Button Clicked"); - const root = createRoot(document.getElementById("wizardDiv")); - root.render(); + + const root = createRoot(document.getElementById("modal-container")); + root.render( root.unmount()}/>); }; return HeaderButton({ - element, - id: 'deployment-data-button', - text: translate('Deployment Data'), - description: 'ArtifactWizard', - className: "wizard-button", - children: 'Artifact Wizard', + id: 'artifact-upload-button', + description: translate('Upload Artifact'), + className: "qwm-artifact-upload-btn", + children: translate('Upload Artifact'), onClick, }); } - -function Modal() { - const [showModal, setShowModal] = useState(true); - - function handleWizardClosed() { - setShowModal(false); - } - - return ( -
- {showModal && ( - - )} -
- ); -} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js index 7e3b8186..205f3222 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js @@ -96,9 +96,10 @@ export function ImplementationProps(props) { }); entries.push({ id: 'artifactUpload', + translate, component: ArtifactUpload, isEdited: isTextFieldEntryEdited - }) + }); } return entries; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css index 42422073..65828696 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css @@ -36,18 +36,21 @@ } -.wizard-button { - box-sizing: border-box; +.qwm-artifact-upload-btn { outline: none; - background-color: transparent; - border: solid; - border-radius: 6px; - font-size: 15px; - font-family: Arial, serif; - font-weight: bold; - color: #636363; - padding: 3px 6px 2px; + background-color: var(--color-grey-225-10-97); + border: 1px solid var(--color-grey-225-10-75); + border-radius: 4px; margin: 2px 32px 6px 12px; + padding: 2px 6px 2px 6px; + fill: var(--add-entry-fill-color); +} + +.qwm-artifact-upload-btn:hover { + color: white; + border: 1px solid white; + background-color: var(--color-blue-205-100-50); + fill: var(--add-entry-hover-fill-color); } .wizard-tab-content { diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/hide-icon.png b/components/bpmn-q/modeler-component/extensions/opentosca/resources/hide-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..53abf9aec679eaf0134ff445de87b2564d11bafa GIT binary patch literal 29791 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

Os>}XIv^|nAG3X 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 literal 0 HcmV?d00001 diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/show-icon.png b/components/bpmn-q/modeler-component/extensions/opentosca/resources/show-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1f8e2423f062f9c69e9370c2e00782ad8ff3e3 GIT binary patch literal 25199 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 literal 0 HcmV?d00001 diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css b/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css index d584e8e6..4f8d2be8 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css +++ b/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css @@ -26,8 +26,8 @@ content: ""; width: 15px; height: 15px; + background-image: url("../resources/opentosca-icon.png"); background-size: contain; - background-image: url("../resources/service-deployment-icon.png"); background-repeat: no-repeat; display: inline-block; float: left; @@ -104,3 +104,25 @@ background-color: #008000; color: #000000; } + +.qwm .show-icon:before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: url("../resources/show-icon.png") no-repeat center center; + background-size: contain; + float: left; +} + + +.qwm .hide-icon:before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: url("../resources/hide-icon.png") no-repeat center center; + background-size: contain; + float: left; +} + 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 7f1c9e91..2bbbe9e2 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 @@ -310,19 +310,19 @@ export default class DeploymentPlugin extends PureComponent { // render deployment button and pop-up menu return ( this.showDeployment(true)}> - Show deployment + Show Deployment , - , - -

- - - ); -} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js index 9854cb1f..f7836788 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js @@ -1,18 +1,21 @@ import {HeaderButton} from '@bpmn-io/properties-panel'; import React from 'react'; -import ArtifactModal from './ArtifactModal'; +import ArtifactModal from './ArtifactUploadModal'; import {createRoot} from 'react-dom/client'; import './artifact-modal.css'; +import {useService} from "bpmn-js-properties-panel"; /** * Entry to display the button which opens the Artifact Upload modal */ export function ArtifactUpload(props) { - const {translate} = props; + const {translate, wineryEndpoint, element} = props; + const commandStack = useService('commandStack'); + const onClick = () => { const root = createRoot(document.getElementById("modal-container")); - root.render( root.unmount()}/>); + root.render( root.unmount()} wineryEndpoint={wineryEndpoint} element={element} commandStack={commandStack}/>); }; return HeaderButton({ diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js new file mode 100644 index 00000000..9f81b823 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js @@ -0,0 +1,242 @@ +/** + * 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, {useState} from 'react'; +import Modal from '../../../../editor/ui/modal/Modal'; +import './artifact-modal.css'; +import '../../../../editor/config/config-modal.css'; +import { + createArtifactTemplateWithFile, + createServiceTemplateWithNodeAndArtifact, + getNodeTypeQName, + getArtifactTemplateInfo, + insertTopNodeTag +} from '../../winery-manager/winery-handler'; +import NotificationHandler from '../../../../editor/ui/notifications/NotificationHandler'; + + +// polyfill upcoming structural components +const Title = Modal.Title; +const Body = Modal.Body; +const Footer = Modal.Footer; + +/** + * Modal that allows the user to create OpenTOSCA deployment models based on a given artifact. + * An artifact can either be a local file or a Dockerimage reference. + * + * @param onClose Function called when the modal is closed. + * @param wineryEndpoint Endpoint url of winery. + * @param element Service Task represented as element + * @returns {JSX.Element} The modal as React component + * @constructor + */ +export default function ArtifactUploadModal({onClose, wineryEndpoint, element, commandStack}) { + const [uploadFile, setUploadFile] = useState(null); + const [textInputDockerImage, setTextInputDockerImage] = useState(''); + const [selectedTab, setSelectedTab] = useState("artifact"); + const [selectedOption, setSelectedOption] = useState(""); + const [selectedOptionName, setSelectedOptionName] = useState(""); + const [options, setOptions] = useState([]); + const [artifactTypes, setArtifactTypes] = useState([]); + const [acceptTypes, setAcceptTypes] = useState(''); + const [isCreating, setIsCreating] = useState(false); + + + async function updateArtifactSelect() { + const response = await fetch(`${wineryEndpoint}/artifacttypes/?includeVersions=true`, { + headers: { + 'Accept': 'application/json', + }, + }).then(res => res.json()); + + const newOptions = response + .filter(option => option.name.includes("WAR") || option.name.includes("PythonArchive")) + .map(option => ); + setOptions(newOptions); + setArtifactTypes(response); + } + + const allowedFileTypes = { + zip: '.tar.gz', + war: '.war', + }; + + async function createServiceTemplate() { + setIsCreating(true); + let serviceTemplateAddress; + try { + const namePrefix = element.businessObject.name ?? ""; + const artifactTemplateName = `${namePrefix}ArtifactTemplate-${element.id}`; + const artifactTemplateAddress = await createArtifactTemplateWithFile(artifactTemplateName, selectedOption, uploadFile); + const artifactTemplateInfo = await getArtifactTemplateInfo(artifactTemplateAddress); + const artifactTemplateQName = artifactTemplateInfo.serviceTemplateOrNodeTypeOrNodeTypeImplementation[0].type; + const nodeTypeQName = getNodeTypeQName(selectedOption); + const serviceTemplateName = `${namePrefix}ServiceTemplate-${element.id}`; + serviceTemplateAddress = await createServiceTemplateWithNodeAndArtifact(serviceTemplateName, nodeTypeQName, + `${namePrefix}Node-${element.id}`, artifactTemplateQName, + `${namePrefix}Artifact-${element.id}`, selectedOption); + await insertTopNodeTag(serviceTemplateAddress, nodeTypeQName); + } catch (e) { + NotificationHandler.getInstance().displayNotification({ + type: 'error', + title: 'Service Template Creation failed', + content: 'Service Template could not be created due to an internal error.', + duration: 2000 + }); + return; + } finally { + setIsCreating(false); + } + const deploymentModelUrl = `{{ wineryEndpoint }}/servicetemplates/${serviceTemplateAddress}?csar`; + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: element.businessObject, + properties: { + 'opentosca:deploymentModelUrl': deploymentModelUrl + } + }); + + NotificationHandler.getInstance().displayNotification({ + type: 'info', + title: 'Service Template Created', + content: 'Service Template including Nodetype with attached Deployment Artifact of chosen type was successfully created.', + duration: 4000 + }); + } + + + const onSubmit = async () => { + // Process the uploaded file or text input here + console.log('Uploaded file:', uploadFile); + console.log('Text input:', textInputDockerImage); + if (selectedTab === "artifact") { + if (uploadFile !== null && selectedOption !== "") { + await createServiceTemplate(); + onClose(); + } else { + onClose(); + setTimeout(function () { + NotificationHandler.getInstance().displayNotification({ + type: 'error', + title: 'No file selected!', + content: 'Please select a file to create an artifact!', + duration: 4000 + }); + }, 300); + + } + } + }; + + const handleOptionChange = (e) => { + const {value} = e.target; + setSelectedOption(value); + setSelectedOptionName(artifactTypes.find(x => x.qName === value).name); + if (value.includes("WAR")) { + setAcceptTypes(allowedFileTypes.war); + } else if (value.includes("PythonArchive")) { + setAcceptTypes(allowedFileTypes.zip); + } + }; + + const isOptionSelected = selectedOption !== ""; + + if (artifactTypes.length === 0) { + updateArtifactSelect(); + } + + + return ( + + Artifact Upload + + +
+
+
setSelectedTab("artifact")} + > + Local File +
+
setSelectedTab("docker")} + > + Docker Image +
+
+ + {selectedTab === "artifact" && ( +
+
+
+ + +
+ {isOptionSelected && ( +
+
+ + setUploadFile(e.target.files[0])} + /> +
+
+ )} +
+
+ )} + {selectedTab === "docker" && ( +
+ + setTextInputDockerImage(e.target.value)} + /> +
+ )} +
+ + +
+
+ + +
+
+
+ ); +} + +function ArtifactSelectItem(props) { + const {value, name} = props; + return ( + + ); +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js new file mode 100644 index 00000000..e69de29b diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js index 205f3222..ef93b4c0 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js @@ -96,7 +96,9 @@ export function ImplementationProps(props) { }); entries.push({ id: 'artifactUpload', + element, translate, + wineryEndpoint, component: ArtifactUpload, isEdited: isTextFieldEntryEdited }); diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css index 4f9cfc42..4c8a1878 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css @@ -64,4 +64,47 @@ .dockerimage-input { flex-grow: 2; -} \ No newline at end of file +} + +.wizard-properties-panel-input { + padding: 3px 6px 2px; + border: 1px solid hsl(225, 10%, 75%); + border-radius: 2px; + background-color: rgb(247, 247, 248); + font-size: 14px; + font-family: inherit; + } + +.wizard-properties-panel-label { + display: block; + font-size: var(--text-size-small); + margin: 2px 0 1px; +} + +.wizard-artifact-div { + display: flex; + flex-direction: row; + align-items: center; + padding: 3px 6px 2px; + font-size: 14px; + font-family: inherit; + width: 100% +} + +.wizard-artifact-selector{ + flex: 1; + max-width: 282px; +} + +.wizard-file-upload { + flex: 1; + max-width: 282px; + overflow: clip; + text-overflow: ellipsis; +} + +.wizard-file-upload-button { + overflow: hidden; + text-overflow: ellipsis; +} + diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/winery-manager/winery-handler.js b/components/bpmn-q/modeler-component/extensions/opentosca/winery-manager/winery-handler.js new file mode 100644 index 00000000..ca8fd285 --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/winery-manager/winery-handler.js @@ -0,0 +1,229 @@ +/** + * 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 {getWineryEndpoint} from "../framework-config/config-manager"; + +const QUANTME_NAMESPACE_PUSH = 'http://quantil.org/quantme/push'; + +export async function createArtifactTemplate(name, artifactTypeQName) { + const artifactTemplate = { + localname: name, + namespace: QUANTME_NAMESPACE_PUSH + '/artifacttemplates', + type: artifactTypeQName, + }; + const response = await fetch(`${getWineryEndpoint()}/artifacttemplates`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/plain' + }, + body: JSON.stringify(artifactTemplate) + }); + return response.text(); +} + +export async function addFileToArtifactTemplate(artifactTemplateAddress, file) { + const formData = new FormData(); + formData.append('file', file); + const response = await fetch(`${getWineryEndpoint()}/artifacttemplates/${artifactTemplateAddress}files`, { + method: 'POST', + body: formData, + headers: { + 'Accept': '*/*' + }, + }); + return response.json(); +} + +export async function createArtifactTemplateWithFile(name, artifactType, file) { + const artifactTemplateAddress = await createArtifactTemplate(name, artifactType); + await addFileToArtifactTemplate(artifactTemplateAddress, file); + return artifactTemplateAddress; +} + +export async function createServiceTemplate(name) { + const serviceTemplate = { + localname: name, + namespace: QUANTME_NAMESPACE_PUSH, + }; + const response = await fetch(getWineryEndpoint() + '/servicetemplates', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/plain' + }, + body: JSON.stringify(serviceTemplate) + }); + return response.text(); +} + +export async function addNodeToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name) { + const nodeTemplate = { + "documentation": [], + "any": [], + "otherAttributes": {}, + "relationshipTemplates": [], + "nodeTemplates": [ + { + "documentation": [], + "any": [], + "otherAttributes": { + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": 1245, + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": 350 + }, + "properties": { + "propertyType": "KV", + "kvproperties": { + "Port": "", + "Name": "" + }, + "elementName": "properties", + "namespace": "http://opentosca.org/nodetypes/propertiesdefinition/winery" + }, + "id": nodeTypeQName.split(/}(.*)/s)[1], + "type": nodeTypeQName, + "name": name, + "minInstances": 1, + "maxInstances": 1, + "x": 1245, + "y": 350, + "capabilities": [], + "requirements": [], + "deploymentArtifacts": null, + "policies": null + } + ] + }; + const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}topologytemplate`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Accept': '*/*' + }, + body: JSON.stringify(nodeTemplate) + }); + return response.status === 204; +} + +export async function addNodeWithArtifactToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name, artifactTemplateQName, artifactName, artifactTypeQName) { + const nodeTemplate = { + "documentation": [], + "any": [], + "otherAttributes": {}, + "relationshipTemplates": [], + "nodeTemplates": [{ + "documentation": [], + "any": [], + "otherAttributes": { + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": 1245, + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": 350 + }, + "properties": { + "propertyType": "KV", + "kvproperties": { + "Port": "", + "Name": "" + }, + "elementName": "properties", + "namespace": "http://opentosca.org/nodetypes/propertiesdefinition/winery" + }, + "id": nodeTypeQName.split(/}(.*)/s)[1], + "type": nodeTypeQName, + "name": name, + "minInstances": 1, + "maxInstances": 1, + "x": 1245, + "y": 350, + "capabilities": [], + "requirements": [], + "deploymentArtifacts": [{ + "documentation": [], + "any": [], + "otherAttributes": {}, + "name": artifactName, + "artifactType": artifactTypeQName, + "artifactRef": artifactTemplateQName + }], + "policies": null + } + ] + }; + const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}topologytemplate`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Accept': '*/*' + }, + body: JSON.stringify(nodeTemplate) + }); + return response.status === 204; +} + +export async function getServiceTemplateXML(serviceTemplateAddress) { + const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}xml`, { + method: 'GET', + headers: { + 'Accept': 'application/xml' + }, + }); + return response.json(); +} + +export async function setServiceTemplateXML(serviceTemplateAddress, newXml) { + const response = await fetch(getWineryEndpoint() + '/servicetemplates/' + serviceTemplateAddress + 'xml', { + method: 'PUT', + body: newXml, + headers: {'Content-Type': 'application/xml'} + }); +} + +export async function insertTopNodeTag(serviceTemplateAddress, nodeTypeQName) { + const tag = { + name: "top-node", + value: nodeTypeQName.split(/}(.*)/s)[1], + }; + const response = await fetch(getWineryEndpoint() + '/servicetemplates/' + serviceTemplateAddress + "tags/", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': '*/*' + }, + body: JSON.stringify(tag) + }); + return response.text(); +} + +export async function createServiceTemplateWithNodeAndArtifact(name, nodeTypeQName, nodeName, artifactTemplateQName, artifactName, artifactTypeQName) { + const serviceTemplateAddress = await createServiceTemplate(name); + await addNodeWithArtifactToServiceTemplate(serviceTemplateAddress, nodeTypeQName, nodeName, artifactTemplateQName, artifactName, artifactTypeQName); + return serviceTemplateAddress; +} + +export async function getArtifactTemplateInfo(artifactTemplateAddress) { + const response = await fetch(`${getWineryEndpoint()}/artifacttemplates/${artifactTemplateAddress}`, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + return response.json(); +} + +const nodeTypeQNameMapping = new Map([ + ['{http://opentosca.org/artifacttypes}WAR', '{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1'], + ['{http://opentosca.org/artifacttypes}WAR17', '{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1'], + ['{http://opentosca.org/artifacttypes}WAR8', '{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1'], + ['{http://opentosca.org/artifacttypes}PythonArchiveArtifact', '{http://opentosca.org/nodetypes}PythonApp_3-w1'], +]); +export function getNodeTypeQName(artifactTypeQName) { + return nodeTypeQNameMapping.get(artifactTypeQName); +} + diff --git a/components/bpmn-q/webpack.config.js b/components/bpmn-q/webpack.config.js index 6688f7d3..2d886901 100644 --- a/components/bpmn-q/webpack.config.js +++ b/components/bpmn-q/webpack.config.js @@ -10,7 +10,9 @@ module.exports = { filename: 'index.js', path: path.resolve(__dirname, 'public'), }, - + devServer: { + allowedHosts: "all" + }, module: { rules: [ { From 69586d7b9c64b82a4fc3d17e8d48700fce3120b1 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 24 Jul 2023 10:40:35 +0200 Subject: [PATCH 15/33] nit picking Signed-off-by: Christoph Walcher Signed-off-by: Maximilian Kuhn Signed-off-by: Furkan Lokman --- .../ui/notifications/NotificationHandler.js | 10 +-- .../editor/ui/notifications/Notifications.js | 8 --- ...{OpenToscaPlugin.js => OpenTOSCAPlugin.js} | 4 +- .../{OpenToscaTab.js => OpenTOSCATab.js} | 6 +- .../opentosca/deployment/OpenTOSCAUtils.js | 47 -------------- .../WineryUtils.js} | 46 ++++++++++++++ ...nToscaRenderer.js => OpenTOSCARenderer.js} | 11 ++-- .../extensions/opentosca/modeling/index.js | 8 +-- .../properties-provider/ArtifactUpload.js | 4 +- .../ArtifactUploadModal.js | 62 +++++++++---------- .../ArtifactWizardModal.js | 0 .../ImplementationProps.js | 1 - .../properties-provider/artifact-modal.css | 14 ++--- .../opentosca-plugin/opentosca-plugin.md | 2 +- 14 files changed, 101 insertions(+), 122 deletions(-) rename components/bpmn-q/modeler-component/extensions/opentosca/{OpenToscaPlugin.js => OpenTOSCAPlugin.js} (88%) rename components/bpmn-q/modeler-component/extensions/opentosca/configTabs/{OpenToscaTab.js => OpenTOSCATab.js} (95%) rename components/bpmn-q/modeler-component/extensions/opentosca/{winery-manager/winery-handler.js => deployment/WineryUtils.js} (82%) rename components/bpmn-q/modeler-component/extensions/opentosca/modeling/{OpenToscaRenderer.js => OpenTOSCARenderer.js} (98%) delete mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactWizardModal.js diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js b/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js index 38a4b58f..b65579b7 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js @@ -50,17 +50,15 @@ export default class NotificationHandler { * @param title The title of the notification. * @param content The text displayed by the notification. * @param duration The duration in milliseconds. - * @returns the id of the created notification. + * @returns {{update: update, close: close}} */ displayNotification({type = 'info', title, content, duration = 4000}) { - this.notificationRef.current.displayNotification({ + return this.notificationRef.current.displayNotification({ type: type, title: title, content: content, duration: duration }); - - return this.notificationRef.current.currentNotificationId - 1; // -1 because the id is incremented before the notification is displayed } /** @@ -69,8 +67,4 @@ export default class NotificationHandler { closeNotifications() { this.notificationRef.current.closeNotifications(); } - - closeNotification(id) { - this.notificationRef.current.closeNotification(id); - } } \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js b/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js index c913b149..19b67193 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js @@ -101,16 +101,8 @@ export default class Notifications extends PureComponent { * @private */ _closeNotification(id) { - console.log(id); - console.log(this.state.notifications); const notifications = this.state.notifications.filter(({id: currentId}) => currentId !== id); - console.log(notifications); this.setState({notifications: notifications}); - console.log(this.state.notifications); - } - - closeNotification(id) { - this._closeNotification(id); } render() { diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js similarity index 88% rename from components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js rename to components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js index 5b37ef8e..fe43c934 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/OpenToscaPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js @@ -1,6 +1,6 @@ import React from "react"; -import OpenToscaTab from "./configTabs/OpenToscaTab"; +import OpenTOSCATab from "./configTabs/OpenTOSCATab"; import opentoscaStyles from './styling/opentosca.css'; import DeploymentPlugin from "./ui/deployment/services/DeploymentPlugin"; @@ -17,7 +17,7 @@ export default { { tabId: 'OpenTOSCAEndpointTab', tabTitle: 'OpenTOSCA', - configTab: OpenToscaTab, + configTab: OpenTOSCATab, } ], extensionModule: OpenToscaExtensionModule, diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenToscaTab.js b/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js similarity index 95% rename from components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenToscaTab.js rename to components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js index 2cf3550b..ca6cb92b 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenToscaTab.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js @@ -9,7 +9,7 @@ import * as config from "../framework-config/config-manager"; * @return {JSX.Element} The tab as a React component * @constructor */ -export default function OpenToscaTab() { +export default function OpenTOSCATab() { const [opentoscaEndpoint, setOpentoscaEndpoint] = useState(config.getOpenTOSCAEndpoint()); const [wineryEndpoint, setWineryEndpoint] = useState(config.getWineryEndpoint()); @@ -37,7 +37,7 @@ export default function OpenToscaTab() { } // save changed config entries on close - OpenToscaTab.prototype.onClose = () => { + OpenTOSCATab.prototype.onClose = () => { modeler.config.opentoscaEndpoint = opentoscaEndpoint; modeler.config.wineryEndpoint = wineryEndpoint; config.setOpenTOSCAEndpoint(opentoscaEndpoint); @@ -73,7 +73,7 @@ export default function OpenToscaTab() { ; } -OpenToscaTab.prototype.config = () => { +OpenTOSCATab.prototype.config = () => { const modeler = getModeler(); modeler.config.opentoscaEndpoint = config.getOpenTOSCAEndpoint(); 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 00737290..7e57328e 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -10,7 +10,6 @@ */ import {fetch} from 'whatwg-fetch'; -import * as config from "../framework-config/config-manager"; /** * Upload the CSAR located at the given URL to the connected OpenTOSCA Container and return the corresponding URL and required input parameters @@ -224,52 +223,6 @@ export async function createServiceInstance(csar, camundaEngineEndpoint) { return result; } -export async function loadTopology(deploymentModelUrl) { - console.log(config.getWineryEndpoint()) - if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { - deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); - } - let topology; - let tags; - try { - topology = await fetch(deploymentModelUrl.replace('?csar', 'topologytemplate')) - .then(res => res.json()); - tags = await fetch(deploymentModelUrl.replace('?csar', 'tags')) - .then(res => res.json()); - - } catch (e) { - throw new Error('An unexpected error occurred during loading the deployments models topology.'); - } - let topNode; - const topNodeTag = tags.find(tag => tag.name === "top-node"); - if (topNodeTag) { - const topNodeId = topNodeTag.value; - topNode = topology.nodeTemplates.find(nodeTemplate => nodeTemplate.id === topNodeId); - if (!topNode) { - throw new Error(`Top level node "${topNodeId}" not found.`); - } - } else { - let nodes = new Map(topology.nodeTemplates.map(nodeTemplate => [nodeTemplate.id, nodeTemplate])); - for(let relationship of topology.relationshipTemplates) { - if(relationship.name === "HostedOn") { - nodes.delete(relationship.targetElement.ref); - } - } - if(nodes.size === 1) { - topNode = nodes.values().next().value; - } - } - if (!topNode) { - throw new Error("No top level node found."); - } - - return { - topNode, - nodeTemplates: topology.nodeTemplates, - relationshipTemplates: topology.relationshipTemplates - }; -} - function makeId(length) { let result = ''; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/winery-manager/winery-handler.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js similarity index 82% rename from components/bpmn-q/modeler-component/extensions/opentosca/winery-manager/winery-handler.js rename to components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js index ca8fd285..1cca23f9 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/winery-manager/winery-handler.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js @@ -9,7 +9,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as config from "../framework-config/config-manager"; import {getWineryEndpoint} from "../framework-config/config-manager"; +import {fetch} from "whatwg-fetch"; const QUANTME_NAMESPACE_PUSH = 'http://quantil.org/quantme/push'; @@ -227,3 +229,47 @@ export function getNodeTypeQName(artifactTypeQName) { return nodeTypeQNameMapping.get(artifactTypeQName); } +export async function loadTopology(deploymentModelUrl) { + if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { + deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); + } + let topology; + let tags; + try { + topology = await fetch(deploymentModelUrl.replace('?csar', 'topologytemplate')) + .then(res => res.json()); + tags = await fetch(deploymentModelUrl.replace('?csar', 'tags')) + .then(res => res.json()); + + } catch (e) { + throw new Error('An unexpected error occurred during loading the deployments models topology.'); + } + let topNode; + const topNodeTag = tags.find(tag => tag.name === "top-node"); + if (topNodeTag) { + const topNodeId = topNodeTag.value; + topNode = topology.nodeTemplates.find(nodeTemplate => nodeTemplate.id === topNodeId); + if (!topNode) { + throw new Error(`Top level node "${topNodeId}" not found.`); + } + } else { + let nodes = new Map(topology.nodeTemplates.map(nodeTemplate => [nodeTemplate.id, nodeTemplate])); + for (let relationship of topology.relationshipTemplates) { + if (relationship.name === "HostedOn") { + nodes.delete(relationship.targetElement.ref); + } + } + if (nodes.size === 1) { + topNode = nodes.values().next().value; + } + } + if (!topNode) { + throw new Error("No top level node found."); + } + + return { + topNode, + nodeTemplates: topology.nodeTemplates, + relationshipTemplates: topology.relationshipTemplates + }; +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js similarity index 98% rename from components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js rename to components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js index b638af48..1cf77329 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenToscaRenderer.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js @@ -10,11 +10,11 @@ import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer'; import buttonIcon from '../resources/show-deployment-button.svg?raw'; import {drawTaskSVG} from '../../../editor/util/RenderUtilities'; -import * as config from '../framework-config/config-manager'; import NotificationHandler from '../../../editor/ui/notifications/NotificationHandler'; import {append as svgAppend, attr as svgAttr, create as svgCreate, select, prepend as svgPrepend} from 'tiny-svg'; import {query as domQuery} from 'min-dom'; -import {loadTopology} from "../deployment/OpenTOSCAUtils"; + +import {loadTopology} from "../deployment/WineryUtils"; const HIGH_PRIORITY = 14001; const SERVICE_TASK_TYPE = 'bpmn:ServiceTask'; @@ -23,7 +23,7 @@ const DEPLOYMENT_REL_MARKER_ID = 'deployment-rel'; const NODE_WIDTH = 100; const NODE_HEIGHT = 60; -const NODE_SHIFT_MARGIN = 5; +const NODE_SHIFT_MARGIN = 10; const STROKE_STYLE = { strokeLinecap: 'round', strokeLinejoin: 'round', @@ -32,7 +32,7 @@ const STROKE_STYLE = { strokeDasharray: 4, }; -export default class OpenToscaRenderer extends BpmnRenderer { +export default class OpenTOSCARenderer extends BpmnRenderer { constructor(config, eventBus, styles, pathMap, canvas, textRenderer, commandStack, elementRegistry) { super(config, eventBus, styles, pathMap, canvas, textRenderer, HIGH_PRIORITY); this.commandStack = commandStack; @@ -131,6 +131,7 @@ export default class OpenToscaRenderer extends BpmnRenderer { button.style['cursor'] = 'pointer'; button.addEventListener('click', (e) => { e.preventDefault(); + element.deploymentModelTopology = undefined; this.commandStack.execute("deploymentModel.show", { element: element, showDeploymentModel: !element.showDeploymentModel @@ -336,7 +337,7 @@ export default class OpenToscaRenderer extends BpmnRenderer { } } -OpenToscaRenderer.$inject = [ +OpenTOSCARenderer.$inject = [ 'config', 'eventBus', 'styles', diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js index 5c43a562..eac8b36b 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js @@ -9,10 +9,10 @@ * SPDX-License-Identifier: Apache-2.0 */ import ServiceTaskPropertiesProvider from './properties-provider/ServiceTaskPropertiesProvider' -import OpenToscaRenderer from './OpenToscaRenderer' +import OpenTOSCARenderer from './OpenTOSCARenderer'; export default { - __init__: ['openToscaRenderer', 'customPropertiesProvider'], - openToscaRenderer: ['type', OpenToscaRenderer], - customPropertiesProvider: ['type', ServiceTaskPropertiesProvider], + __init__: ['openToscaRenderer', 'serviceTaskPropertyProvider'], + openToscaRenderer: ['type', OpenTOSCARenderer], + serviceTaskPropertyProvider: ['type', ServiceTaskPropertiesProvider], }; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js index f7836788..df10dcb5 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js @@ -9,13 +9,13 @@ import {useService} from "bpmn-js-properties-panel"; * Entry to display the button which opens the Artifact Upload modal */ export function ArtifactUpload(props) { - const {translate, wineryEndpoint, element} = props; + const {translate, element} = props; const commandStack = useService('commandStack'); const onClick = () => { const root = createRoot(document.getElementById("modal-container")); - root.render( root.unmount()} wineryEndpoint={wineryEndpoint} element={element} commandStack={commandStack}/>); + root.render( root.unmount()} element={element} commandStack={commandStack}/>); }; return HeaderButton({ diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js index 9f81b823..1d48a0cc 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js @@ -20,8 +20,9 @@ import { getNodeTypeQName, getArtifactTemplateInfo, insertTopNodeTag -} from '../../winery-manager/winery-handler'; +} from '../../deployment/WineryUtils'; import NotificationHandler from '../../../../editor/ui/notifications/NotificationHandler'; +import {getWineryEndpoint} from "../../framework-config/config-manager"; // polyfill upcoming structural components @@ -39,30 +40,26 @@ const Footer = Modal.Footer; * @returns {JSX.Element} The modal as React component * @constructor */ -export default function ArtifactUploadModal({onClose, wineryEndpoint, element, commandStack}) { +export default function ArtifactUploadModal({onClose, element, commandStack}) { const [uploadFile, setUploadFile] = useState(null); const [textInputDockerImage, setTextInputDockerImage] = useState(''); const [selectedTab, setSelectedTab] = useState("artifact"); const [selectedOption, setSelectedOption] = useState(""); const [selectedOptionName, setSelectedOptionName] = useState(""); - const [options, setOptions] = useState([]); const [artifactTypes, setArtifactTypes] = useState([]); const [acceptTypes, setAcceptTypes] = useState(''); - const [isCreating, setIsCreating] = useState(false); async function updateArtifactSelect() { - const response = await fetch(`${wineryEndpoint}/artifacttypes/?includeVersions=true`, { + const response = await fetch(`${getWineryEndpoint()}/artifacttypes/?includeVersions=true`, { headers: { 'Accept': 'application/json', }, }).then(res => res.json()); - const newOptions = response - .filter(option => option.name.includes("WAR") || option.name.includes("PythonArchive")) - .map(option => ); - setOptions(newOptions); - setArtifactTypes(response); + const artifactTypes = response + .filter(option => option.name.includes("WAR") || option.name.includes("PythonArchive")); + setArtifactTypes(artifactTypes); } const allowedFileTypes = { @@ -71,7 +68,12 @@ export default function ArtifactUploadModal({onClose, wineryEndpoint, element, c }; async function createServiceTemplate() { - setIsCreating(true); + const {close: closeNotification} = NotificationHandler.getInstance().displayNotification({ + type: 'info', + title: 'Uploading Artifact...', + content: 'Uploading artifact for ' + element.id, + duration: 1000 * 60 * 60 // very long time out because this notification gets closed after the API calls are finished + }); let serviceTemplateAddress; try { const namePrefix = element.businessObject.name ?? ""; @@ -86,6 +88,7 @@ export default function ArtifactUploadModal({onClose, wineryEndpoint, element, c `${namePrefix}Artifact-${element.id}`, selectedOption); await insertTopNodeTag(serviceTemplateAddress, nodeTypeQName); } catch (e) { + setTimeout(closeNotification, 1); NotificationHandler.getInstance().displayNotification({ type: 'error', title: 'Service Template Creation failed', @@ -93,8 +96,6 @@ export default function ArtifactUploadModal({onClose, wineryEndpoint, element, c duration: 2000 }); return; - } finally { - setIsCreating(false); } const deploymentModelUrl = `{{ wineryEndpoint }}/servicetemplates/${serviceTemplateAddress}?csar`; commandStack.execute('element.updateModdleProperties', { @@ -104,7 +105,7 @@ export default function ArtifactUploadModal({onClose, wineryEndpoint, element, c 'opentosca:deploymentModelUrl': deploymentModelUrl } }); - + setTimeout(closeNotification, 1); NotificationHandler.getInstance().displayNotification({ type: 'info', title: 'Service Template Created', @@ -120,8 +121,8 @@ export default function ArtifactUploadModal({onClose, wineryEndpoint, element, c console.log('Text input:', textInputDockerImage); if (selectedTab === "artifact") { if (uploadFile !== null && selectedOption !== "") { - await createServiceTemplate(); onClose(); + await createServiceTemplate(); } else { onClose(); setTimeout(function () { @@ -178,22 +179,23 @@ export default function ArtifactUploadModal({onClose, wineryEndpoint, element, c {selectedTab === "artifact" && (
-
-
- - - {options} + {artifactTypes.map(option => )}
{isOptionSelected && ( -
+
+ className="upload-properties-panel-label">{`Upload ${selectedOptionName.charAt(0).toUpperCase() + selectedOptionName.slice(1)}:`}
- , ]}/> + {this.state.windowOpenDeploymentTransformation && ( + + )} {this.state.windowOpenDeploymentOverview && (

{children}

); +const Body = Modal.Body || (({children}) =>
{children}
); +const Footer = Modal.Footer || (({children}) =>
{children}
); + +export default function ServiceDeploymentTransformationModal({onClose, initValues}) { + + // close if no deployment required + if (!initValues || initValues.length === 0) { + onClose(); + } + + const onOnDemand = (value) => onClose({ + onDemand: value, + }); + + return + + + Enable On Demand Service Deployment? + +
+
+ + +
+
+
; +} From fe83257ed1a560c5b4368e8007b1acd8aebf9da0 Mon Sep 17 00:00:00 2001 From: Maximilian Kuhn Date: Thu, 27 Jul 2023 16:46:43 +0200 Subject: [PATCH 17/33] Fix button integration --- .../editor/ui/DeploymentButton.js | 50 +++++++++++++++++-- .../ui/OnDemandDeploymentModal.js} | 10 +--- .../deployment/services/DeploymentPlugin.js | 47 +---------------- 3 files changed, 49 insertions(+), 58 deletions(-) rename components/bpmn-q/modeler-component/{extensions/opentosca/ui/deployment/services/ServiceDeploymentTransformationModal.js => editor/ui/OnDemandDeploymentModal.js} (77%) diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index e6cb6c17..dfa74426 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -1,8 +1,15 @@ -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'; + +const defaultState = { + windowOpenOnDemandDeployment: false, +}; /** * React button for starting the deployment of the workflow. @@ -12,9 +19,30 @@ 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')) { + if (result.onDemand === true) { + //TODO: Cooles Deployment + } + // deploy in any case + deploy(); + } + // handle cancellation (don't deploy) + setWindowOpenOnDemandDeployment(false); + + } + /** * Deploy the current workflow to the Camunda engine */ @@ -56,12 +84,26 @@ export default function DeploymentButton(props) { } } + async function onClick() { + let csarsToDeploy = getServiceTasksToDeploy(getRootProcess(getModeler().getDefinitions())); + if (csarsToDeploy.length > 0) { + setWindowOpenOnDemandDeployment(true); + } else { + deploy(); + } + } + return ( - <> + - + {windowOpenOnDemandDeployment && ( + handleOnDemandDeployment(e)} + /> + )} + ); } \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentTransformationModal.js b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js similarity index 77% rename from components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentTransformationModal.js rename to components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js index be3ad5f0..46c0850c 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentTransformationModal.js +++ b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js @@ -13,18 +13,12 @@ import React from 'react'; // polyfill upcoming structural components -import Modal from "../../../../../editor/ui/modal/Modal"; +import Modal from './modal/Modal'; const Title = Modal.Title || (({children}) =>

{children}

); -const Body = Modal.Body || (({children}) =>
{children}
); const Footer = Modal.Footer || (({children}) =>
{children}
); -export default function ServiceDeploymentTransformationModal({onClose, initValues}) { - - // close if no deployment required - if (!initValues || initValues.length === 0) { - onClose(); - } +export default function OnDemandDeploymentModal({onClose}) { const onOnDemand = (value) => onClose({ onDemand: value, 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 b06b14a6..cbb9325b 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 @@ -15,7 +15,6 @@ import React, {Fragment, PureComponent} from 'react'; import ServiceDeploymentOverviewModal from './ServiceDeploymentOverviewModal'; import ServiceDeploymentInputModal from './ServiceDeploymentInputModal'; import ServiceDeploymentBindingModal from './ServiceDeploymentBindingModal'; -import ServiceDeploymentTransformationModal from './ServiceDeploymentTransformationModal'; import {createServiceInstance, uploadCSARToContainer} from '../../../deployment/OpenTOSCAUtils'; import {bindUsingPull, bindUsingPush} from '../../../deployment/BindingUtils'; @@ -26,7 +25,6 @@ import {getRootProcess} from '../../../../../editor/util/ModellingUtilities'; import ExtensibleButton from "../../../../../editor/ui/ExtensibleButton"; const defaultState = { - windowOpenDeploymentTransformation: false, windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false @@ -42,7 +40,6 @@ export default class DeploymentPlugin extends PureComponent { this.handleDeploymentOverviewClosed = this.handleDeploymentOverviewClosed.bind(this); this.handleDeploymentInputClosed = this.handleDeploymentInputClosed.bind(this); this.handleDeploymentBindingClosed = this.handleDeploymentBindingClosed.bind(this); - this.handleDeploymentTransformationClosed = this.handleDeploymentTransformationClosed.bind(this); } componentDidMount() { @@ -110,7 +107,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false }); return; } @@ -129,7 +125,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: true, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false, csarList: csarList }); return; @@ -140,7 +135,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false }); } @@ -185,7 +179,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false }); return; } @@ -206,7 +199,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: true, - windowOpenDeploymentTransformation: false }); return; } @@ -216,7 +208,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false }); } @@ -262,7 +253,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false }); return; } @@ -282,7 +272,6 @@ export default class DeploymentPlugin extends PureComponent { windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false }); } @@ -317,34 +306,6 @@ export default class DeploymentPlugin extends PureComponent { }); } - /** - * Handle the result of a close operation on the tramfpr,atopm - * - * @param result the result from the close operation - */ - handleDeploymentTransformationClosed(result) { - console.log(result); - if (result && result.hasOwnProperty('onDemand')) { - if (result.onDemand === true) { - //TODO: Cooles Deployment - } - this.setState({ - windowOpenDeploymentOverview: true, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false - }); - return; - } - // handle cancellation - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - windowOpenDeploymentTransformation: false - }); - } - render() { // render deployment button and pop-up menu return ( @@ -364,16 +325,10 @@ export default class DeploymentPlugin extends PureComponent { className="qwm-indent">Hide Deployment , ]}/> - {this.state.windowOpenDeploymentTransformation && ( - - )} {this.state.windowOpenDeploymentOverview && ( Date: Sat, 29 Jul 2023 13:06:38 +0200 Subject: [PATCH 18/33] implement on demand transformation --- .../editor/ui/DeploymentButton.js | 5 +- .../opentosca/deployment/BindingUtils.js | 17 ++++- .../opentosca/deployment/DeploymentUtils.js | 2 +- .../opentosca/modeling/OpenTOSCARenderer.js | 4 +- .../replacement/OnDemandTransformator.js | 70 +++++++++++++++++++ .../deployment/services/DeploymentPlugin.js | 2 +- 6 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index dfa74426..6a230c4d 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -6,6 +6,7 @@ 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, @@ -33,7 +34,9 @@ export default function DeploymentButton(props) { console.log(result); if (result && result.hasOwnProperty('onDemand')) { if (result.onDemand === true) { - //TODO: Cooles Deployment + const xml = (await modeler.saveXML({format: true})).xml; + console.log("Post Transfrom", await startOnDemandReplacementProcess(xml)); + } // deploy in any case deploy(); 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 4e8a81cd..c6b721a7 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,8 +63,18 @@ 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}); + 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/modeling/OpenTOSCARenderer.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js index 1cf77329..149c5a89 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js @@ -235,7 +235,7 @@ export default class OpenTOSCARenderer extends BpmnRenderer { const commands = []; if (shifts.right || shifts.left) { - const xPosition = (newBoundingBox.left + newBoundingBox.right) / 2 + const xPosition = (newBoundingBox.left + newBoundingBox.right) / 2; for (const otherElement of allElements) { let otherXPosition = element.x + NODE_WIDTH / 2; const otherElementBoundingBox = this.currentlyShownDeploymentsModels.get(otherElement.id)?.boundingBox; @@ -248,7 +248,7 @@ export default class OpenTOSCARenderer extends BpmnRenderer { } else if (shifts.left && otherXPosition <= xPosition && otherElement.id !== element.id) { xShift = -shifts.left - NODE_SHIFT_MARGIN } else { - continue + continue; } // Can not move elements without parent if(!otherElement.parent) continue; 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..e3a1880a --- /dev/null +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -0,0 +1,70 @@ +/** + * 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 { + getRootProcess, +} from '../../../editor/util/ModellingUtilities'; +import {getXml} from '../../../editor/util/IoUtilities'; +import {isDeployableServiceTask} from "../deployment/DeploymentUtils"; +import * as config from "../framework-config/config-manager"; + +/** + * 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) { + const bounds = { + x: serviceTask.x, + y: serviceTask.y, + }; + 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); + + + const serviceTask1 = modeling.appendShape(startEvent, { + type: 'bpmn:ServiceTask' + }, {x: 400, y: 200}); + + + const serviceTask2 = modeling.appendShape(serviceTask1, { + type: 'bpmn:ServiceTask' + }, {x: 600, y: 200}, subProcess); + } + + // layout diagram after successful transformation + let updated_xml = await getXml(modeler); + console.log(updated_xml); + return {status: 'transformed', xml: updated_xml}; +} 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 cbb9325b..710fb2af 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 @@ -232,7 +232,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')); } From 84538849669d0ff2f24bdb1aaa0c40d3ae334260 Mon Sep 17 00:00:00 2001 From: Maximilian Kuhn Date: Tue, 1 Aug 2023 16:51:16 +0200 Subject: [PATCH 19/33] Move connectorUrl to opentosca plugin --- .../extensions/opentosca/resources/opentosca4bpmn.json | 5 +++++ package-lock.json | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 package-lock.json diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json b/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json index 47c975f6..ee1b10a4 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json +++ b/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json @@ -14,6 +14,11 @@ "name": "deploymentModelUrl", "isAttr": true, "type": "String" + }, + { + "name": "connectorUrl", + "isAttr": true, + "type": "String" } ] } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..26e0a275 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "workflow-modeler", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} From 5c7f84dc9e466dfc5cd8a86122c4dd16ae7bbc32 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Tue, 1 Aug 2023 18:39:29 +0200 Subject: [PATCH 20/33] fix merge of upstream --- .../opentosca/deployment/OpenTOSCAUtils.js | 3 + .../properties-provider/YamlUpload.js | 30 +-- .../quantme/deployment/OpenTOSCAUtils.js | 215 ------------------ 3 files changed, 5 insertions(+), 243 deletions(-) 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 7e57328e..9ee53089 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -207,6 +207,7 @@ export async function createServiceInstance(csar, camundaEngineEndpoint) { let state = 'CREATING'; console.log('Polling for finished service instance at URL: %s', pollingUrl); + let properties = ''; while (!(state === 'CREATED' || state === 'FAILED')) { // wait 5 seconds for next poll @@ -215,11 +216,13 @@ export async function createServiceInstance(csar, camundaEngineEndpoint) { let pollingResponse = await fetch(pollingUrl); let pollingResponseJson = await pollingResponse.json(); console.log('Polling response: ', pollingResponseJson); + properties = pollingResponseJson._links.properties.href; state = pollingResponseJson.state; } result.success = true; + result.properties = properties; return result; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js index 42ea94ea..8d56b51a 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js @@ -3,7 +3,6 @@ import { useService } from 'bpmn-js-properties-panel'; import React from 'react'; import YamlModal from './YamlModal'; import { createRoot } from 'react-dom/client'; -import { useState } from 'react'; import './yaml-modal.css'; /** @@ -15,16 +14,8 @@ export function YamlUpload(props) { const commandStack = useService('commandStack'); const onClick = () => { - const yamlUploadDiv = document.querySelector("#yamlUploadDiv"); - - if (yamlUploadDiv) { - yamlUploadDiv.remove(); - } - const div = document.createElement("div"); - div.id = "yamlUploadDiv"; - document.getElementById('main-div').appendChild(div); - const root = createRoot(document.getElementById("yamlUploadDiv")); - root.render(); + const root = createRoot(document.getElementById("modal-container")); + root.render( root.unmount()} element={element} commandStack={commandStack}/>); }; return HeaderButton({ @@ -37,20 +28,3 @@ export function YamlUpload(props) { onClick, }); } - -function YAMLModal(props) { - const [showModal, setShowModal] = useState(true); - const {element, commandStack} = props; - - function handleModalClosed() { - setShowModal(false); - } - - return ( -
- {showModal && ( - - )} -
- ); -} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js b/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js index 0359c609..97162a22 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/deployment/OpenTOSCAUtils.js @@ -12,221 +12,6 @@ import {fetch} from 'whatwg-fetch'; import {performAjax} from '../utilities/Utilities'; -/** - * Upload the CSAR located at the given URL to the connected OpenTOSCA Container and return the corresponding URL and required input parameters - * - * @param opentoscaEndpoint the endpoint of the OpenTOSCA Container - * @param csarName the name of the CSAR to upload - * @param url the URL pointing to the CSAR - * @param wineryEndpoint the endpoint of the Winery containing the CSAR to upload - */ -export async function uploadCSARToContainer(opentoscaEndpoint, csarName, url, wineryEndpoint) { - - if (opentoscaEndpoint === undefined) { - console.error('OpenTOSCA endpoint is undefined. Unable to upload CSARs...'); - return {success: false}; - } - - try { - if (url.startsWith('{{ wineryEndpoint }}')) { - url = url.replace('{{ wineryEndpoint }}', wineryEndpoint); - } - console.log('Checking if CSAR at following URL is already uploaded to the OpenTOSCA Container: ', url); - - // check if CSAR is already uploaded - let getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); - - if (!getCSARResult.success) { - console.log('CSAR is not yet uploaded. Uploading...'); - - let body = { - enrich: 'false', - name: csarName, - url: url - }; - - // upload the CSAR - await fetch(opentoscaEndpoint, { - method: 'POST', - body: JSON.stringify(body), - headers: {'Content-Type': 'application/json'} - }); - - // check successful upload and retrieve corresponding url - getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); - } - - if (!getCSARResult.success) { - console.error('Uploading CSAR failed!'); - return {success: false}; - } - - // retrieve input parameters for the build plan - let buildPlanResult = await fetch(getCSARResult.url); - let buildPlanResultJson = await buildPlanResult.json(); - - return {success: true, url: getCSARResult.url, inputParameters: buildPlanResultJson.input_parameters}; - } catch (e) { - console.error('Error while uploading CSAR: ' + e); - return {success: false}; - } -} - -/** - * Get the link to the build plan of the CSAR with the given name if it is uploaded to the OpenTOSCA Container - * - * @param opentoscaEndpoint the endpoint of the OpenTOSCA Container - * @param csarName the name of the csar - * @return the status whether the given CSAR is uploaded and the corresponding build plan link if available - */ -async function getBuildPlanForCSAR(opentoscaEndpoint, csarName) { - - // get all currently deployed CSARs - let response = await fetch(opentoscaEndpoint); - let responseJson = await response.json(); - - let deployedCSARs = responseJson.csars; - if (deployedCSARs === undefined) { - - // no CSARs available - return {success: false}; - } - - for (let i = 0; i < deployedCSARs.length; i++) { - let deployedCSAR = deployedCSARs[i]; - if (deployedCSAR.id === csarName) { - console.log('Found uploaded CSAR with id: %s', csarName); - let url = deployedCSAR._links.self.href; - - // retrieve the URl to the build plan required to get the input parameters and to instantiate the CSAR - return getBuildPlanUrl(url); - } - } - - // unable to find CSAR - return {success: false}; -} - -/** - * Get the URL to the build plan of the given CSAR - * - * @param csarUrl the URL to a CSAR - * @return the URL to the build plan for the given CSAR - */ -async function getBuildPlanUrl(csarUrl) { - - let response = await fetch(csarUrl + '/servicetemplates'); - let responseJson = await response.json(); - - if (!responseJson.service_templates || responseJson.service_templates.length !== 1) { - console.error('Unable to find service template in CSAR at URL: %s', csarUrl); - return {success: false}; - } - - let buildPlansUrl = responseJson.service_templates[0]._links.self.href + '/buildplans'; - response = await fetch(buildPlansUrl); - responseJson = await response.json(); - - if (!responseJson.plans || responseJson.plans.length !== 1) { - console.error('Unable to find build plan at URL: %s', buildPlansUrl); - return {success: false}; - } - - return {success: true, url: responseJson.plans[0]._links.self.href}; -} - -/** - * Create an instance of the ServiceTemplate contained in the given CSAR - * - * @param csar the details about the CSAR to create an instance from the contained ServiceTemplate - * @param camundaEngineEndpoint the endpoint of the Camunda engine to bind services using the pulling pattern - * @return the result of the instance creation (success, endpoint, topic on which the service listens, ...) - */ -export async function createServiceInstance(csar, camundaEngineEndpoint) { - - let result = {success: false}; - - let inputParameters = csar.inputParameters; - if (csar.type === 'pull') { - - // get special parameters that are required to bind services using external tasks / the pulling pattern - let camundaTopicParam = inputParameters.find((param) => param.name === 'camundaTopic'); - let camundaEndpointParam = inputParameters.find((param) => param.name === 'camundaEndpoint'); - - // abort if parameters are not available - if (camundaTopicParam === undefined || camundaEndpointParam === undefined) { - console.error('Unable to pass topic to poll to service instance creation. Service binding will fail!'); - return result; - } - - // generate topic for the binding - let topicName = makeId(12); - - camundaTopicParam.value = topicName; - camundaEndpointParam.value = camundaEngineEndpoint; - result.topicName = topicName; - } - - // trigger instance creation - let instanceCreationResponse = await fetch(csar.buildPlanUrl + '/instances', { - method: 'POST', - body: JSON.stringify(inputParameters), - headers: {'Content-Type': 'application/json'} - }); - let instanceCreationResponseJson = await instanceCreationResponse.json(); - - // wait for the service instance to be created - await new Promise(r => setTimeout(r, 5000)); - - // get service template instance to poll for completness - let buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - let buildPlanResponseJson = await buildPlanResponse.json(); - - // retry polling 10 times, creation of the build time takes some time - for (let retry = 0; retry < 10; retry++) { - - // stop retries in case of correct response - if (buildPlanResponseJson._links) { - break; - } - - await new Promise(r => setTimeout(r, 5000)); - - console.log('Retry fetching build plan'); - - buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - buildPlanResponseJson = await buildPlanResponse.json(); - } - - if (!buildPlanResponseJson._links) { - console.log('Unable to fetch build plans for ' + csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - result.success = false; - return result; - } - - let pollingUrl = buildPlanResponseJson._links.service_template_instance.href; - - let state = 'CREATING'; - console.log('Polling for finished service instance at URL: %s', pollingUrl); - let properties = ''; - while (!(state === 'CREATED' || state === 'FAILED')) { - - // wait 5 seconds for next poll - await new Promise(r => setTimeout(r, 5000)); - // poll for current state - let pollingResponse = await fetch(pollingUrl); - let pollingResponseJson = await pollingResponse.json(); - console.log('Polling response: ', pollingResponseJson); - properties = pollingResponseJson._links.properties.href; - - state = pollingResponseJson.state; - } - - result.success = true; - result.properties = properties; - return result; -} - /** * Create a new ArtifactTemplate of the given type and add the given blob as file * From 6220a5a8d668a0f91f8e5d6cd265be70c94bfade Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Wed, 2 Aug 2023 09:51:31 +0200 Subject: [PATCH 21/33] upload transformed xml --- .../modeler-component/editor/ui/DeploymentButton.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index 6a230c4d..4f3dff20 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -33,13 +33,15 @@ export default function DeploymentButton(props) { 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; + if (result.onDemand === true) { - const xml = (await modeler.saveXML({format: true})).xml; - console.log("Post Transfrom", await startOnDemandReplacementProcess(xml)); + xml = await startOnDemandReplacementProcess(xml); } // deploy in any case - deploy(); + deploy(xml); } // handle cancellation (don't deploy) setWindowOpenOnDemandDeployment(false); @@ -49,7 +51,7 @@ export default function DeploymentButton(props) { /** * Deploy the current workflow to the Camunda engine */ - async function deploy() { + async function deploy(xml) { NotificationHandler.getInstance().displayNotification({ title: 'Deployment started', @@ -58,7 +60,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 = {}; From 396ff92e12a549f05b24519acc820a8f01bc0155 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Wed, 2 Aug 2023 10:22:43 +0200 Subject: [PATCH 22/33] fix review comments Signed-off-by: Maximilian Kuhn --- .../editor/plugin/PluginHandler.js | 2 +- .../opentosca/modeling/OpenTOSCARenderer.js | 38 ++++++++++++++++--- .../ArtifactUploadModal.js | 1 + 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js index 7037a1ba..d6072290 100644 --- a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js +++ b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js @@ -1,6 +1,6 @@ import PlanQKPlugin from "../../extensions/planqk/PlanQKPlugin"; import QuantMEPlugin from "../../extensions/quantme/QuantMEPlugin"; -import OpenToscaPlugin from "../../extensions/opentosca/OpenToscaPlugin"; +import OpenToscaPlugin from "../../extensions/opentosca/OpenTOSCAPlugin"; import DataFlowPlugin from '../../extensions/data-extension/DataFlowPlugin'; import QHAnaPlugin from '../../extensions/qhana/QHAnaPlugin'; import {getAllConfigs} from "./PluginConfigHandler"; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js index 1cf77329..40299fda 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js @@ -7,12 +7,20 @@ import { } from 'diagram-js/lib/util/RenderUtil'; import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer'; +import { getOrientation } from 'diagram-js/lib/layout/LayoutUtil'; + import buttonIcon from '../resources/show-deployment-button.svg?raw'; import {drawTaskSVG} from '../../../editor/util/RenderUtilities'; import NotificationHandler from '../../../editor/ui/notifications/NotificationHandler'; import {append as svgAppend, attr as svgAttr, create as svgCreate, select, prepend as svgPrepend} from 'tiny-svg'; import {query as domQuery} from 'min-dom'; +import { + isSnapped, + setSnapped, + topLeft, + bottomRight +} from 'diagram-js/lib/features/snapping/SnapUtil'; import {loadTopology} from "../deployment/WineryUtils"; @@ -40,6 +48,26 @@ export default class OpenTOSCARenderer extends BpmnRenderer { this.styles = styles; this.textRenderer = textRenderer; + // snap boundary events + eventBus.on([ + 'create.move', + 'create.end', + 'shape.move.move', + 'shape.move.end' + ], 140000, function(event) { + var context = event.context, + canExecute = context.canExecute, + target = context.target; + + var canAttach = canExecute && (canExecute === 'attach' || canExecute.attach); + const isRelevantEvent = context.shape.type === "bpmn:IntermediateThrowEvent" || context.shape.type === "bpmn:BoundaryEvent"; + + if (canAttach && isRelevantEvent && context.target.businessObject.get('opentosca:deploymentModelUrl') && getOrientation(event, target, -30) === "bottom-right") { + // Prevent snapping on deployment visualisation toggle button + event.stopPropagation(); + } + }); + this.openToscaHandlers = { [SERVICE_TASK_TYPE]: function (self, parentGfx, element) { const task = self.renderer('bpmn:ServiceTask')(parentGfx, element); @@ -235,20 +263,20 @@ export default class OpenTOSCARenderer extends BpmnRenderer { const commands = []; if (shifts.right || shifts.left) { - const xPosition = (newBoundingBox.left + newBoundingBox.right) / 2 + const xPosition = (newBoundingBox.left + newBoundingBox.right) / 2; for (const otherElement of allElements) { let otherXPosition = element.x + NODE_WIDTH / 2; const otherElementBoundingBox = this.currentlyShownDeploymentsModels.get(otherElement.id)?.boundingBox; if (otherElementBoundingBox) { otherXPosition = (otherElementBoundingBox.left + otherElementBoundingBox.right) / 2; } - let xShift + let xShift; if (shifts.right && otherXPosition >= xPosition && otherElement.id !== element.id) { - xShift = shifts.right + NODE_SHIFT_MARGIN + xShift = shifts.right + NODE_SHIFT_MARGIN; } else if (shifts.left && otherXPosition <= xPosition && otherElement.id !== element.id) { - xShift = -shifts.left - NODE_SHIFT_MARGIN + xShift = -shifts.left - NODE_SHIFT_MARGIN; } else { - continue + continue; } // Can not move elements without parent if(!otherElement.parent) continue; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js index 1d48a0cc..d5d7669d 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js @@ -172,6 +172,7 @@ export default function ArtifactUploadModal({onClose, element, commandStack}) {
setSelectedTab("docker")} + style={{display: "none"}} > Docker Image
From 09fcb15dea096e16da6c006017de78c684251222 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 28 Aug 2023 21:27:41 +0200 Subject: [PATCH 23/33] Prototyp script task --- .../editor/ui/DeploymentButton.js | 11 ++- .../replacement/OnDemandTransformator.js | 75 +++++++++++++++++-- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index 4f3dff20..e4b5c242 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -1,10 +1,10 @@ -import React, { Fragment, useState } 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 {getModeler} from '../ModelerHandler'; import OnDemandDeploymentModal from './OnDemandDeploymentModal'; import {startOnDemandReplacementProcess} from "../../extensions/opentosca/replacement/OnDemandTransformator"; @@ -35,17 +35,16 @@ export default function DeploymentButton(props) { 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); - + } /** @@ -93,7 +92,7 @@ export default function DeploymentButton(props) { if (csarsToDeploy.length > 0) { setWindowOpenOnDemandDeployment(true); } else { - deploy(); + deploy((await modeler.saveXML({format: true})).xml); } } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index e3a1880a..594a5fca 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -17,6 +17,57 @@ import {getXml} from '../../../editor/util/IoUtilities'; import {isDeployableServiceTask} from "../deployment/DeploymentUtils"; import * as config from "../framework-config/config-manager"; + +function createDeploymentScript(params) { + return ` +var params = ${JSON.stringify(params)}; + +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 createInstanceResponse = fetch('POST', buildPlanUrl + "/instances", JSON.stringify([])) +execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", createInstanceResponse);`; +} + /** * Initiate the replacement process for the QuantME tasks that are contained in the current process model * @@ -35,10 +86,6 @@ export async function startOnDemandReplacementProcess(xml) { const serviceTasks = elementRegistry.filter(({businessObject}) => isDeployableServiceTask(businessObject)); for (const serviceTask of serviceTasks) { - const bounds = { - x: serviceTask.x, - y: serviceTask.y, - }; let deploymentModelUrl = serviceTask.businessObject.get('opentosca:deploymentModelUrl'); if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); @@ -54,17 +101,29 @@ export async function startOnDemandReplacementProcess(xml) { const serviceTask1 = modeling.appendShape(startEvent, { - type: 'bpmn:ServiceTask' + type: 'bpmn:ScriptTask', }, {x: 400, y: 200}); + serviceTask1.businessObject.set("scriptFormat", "javascript"); + serviceTask1.businessObject.set("script", createDeploymentScript( + { + opentoscaEndpoint: config.getOpenTOSCAEndpoint(), + csarName: "ondemand_" + (Math.random().toString().substring(3)), + deploymentModelUrl: deploymentModelUrl, + subprocessId: subProcess.id + } + )); const serviceTask2 = modeling.appendShape(serviceTask1, { type: 'bpmn:ServiceTask' }, {x: 600, y: 200}, subProcess); + + serviceTask2.businessObject.set("camunda:type", "external"); + serviceTask2.businessObject.set("camunda:topic", "fjhdhg"); } // layout diagram after successful transformation - let updated_xml = await getXml(modeler); - console.log(updated_xml); - return {status: 'transformed', xml: updated_xml}; + let updatedXml = await getXml(modeler); + console.log(updatedXml); + return updatedXml; } From a9bae65a0cb233306001c5e013eef1547a1ddace Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 28 Aug 2023 21:36:21 +0200 Subject: [PATCH 24/33] fixup! Prototyp script task --- .../extensions/opentosca/replacement/OnDemandTransformator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index 594a5fca..49c1a2b4 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -65,7 +65,7 @@ var buildPlans = JSON.parse(fetch('GET', buildPlansUrl)) var buildPlanUrl = buildPlans.plans[0]._links.self.href var createInstanceResponse = fetch('POST', buildPlanUrl + "/instances", JSON.stringify([])) -execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", createInstanceResponse);`; +execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", buildPlanUrl + "/instances/" + createInstanceResponse);`; } /** From 6557e96f565f7baf51fbb68ee0df8c90b5d5e3d9 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 11 Sep 2023 16:51:06 +0200 Subject: [PATCH 25/33] fix input parameter supplier --- .../opentosca/deployment/OpenTOSCAUtils.js | 2 +- .../replacement/OnDemandTransformator.js | 28 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) 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 7e57328e..32cfc4f1 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -224,7 +224,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 index 49c1a2b4..4aece5d6 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -10,17 +10,17 @@ */ import {createTempModelerFromXml} from '../../../editor/ModelerHandler'; -import { - getRootProcess, -} from '../../../editor/util/ModellingUtilities'; 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); @@ -63,8 +63,17 @@ var serviceTemplates = JSON.parse(fetch('GET', params.opentoscaEndpoint + "/" + 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 createInstanceResponse = fetch('POST', buildPlanUrl + "/instances", JSON.stringify([])) +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);`; } @@ -99,7 +108,7 @@ export async function startOnDemandReplacementProcess(xml) { type: 'bpmn:StartEvent' }, {x: 200, y: 200}, subProcess); - + let topicName = makeId(12); const serviceTask1 = modeling.appendShape(startEvent, { type: 'bpmn:ScriptTask', }, {x: 400, y: 200}); @@ -107,9 +116,10 @@ export async function startOnDemandReplacementProcess(xml) { serviceTask1.businessObject.set("script", createDeploymentScript( { opentoscaEndpoint: config.getOpenTOSCAEndpoint(), - csarName: "ondemand_" + (Math.random().toString().substring(3)), deploymentModelUrl: deploymentModelUrl, - subprocessId: subProcess.id + subprocessId: subProcess.id, + camundaTopic: topicName, + camundaEndpoint: getCamundaEndpoint() } )); @@ -119,7 +129,7 @@ export async function startOnDemandReplacementProcess(xml) { }, {x: 600, y: 200}, subProcess); serviceTask2.businessObject.set("camunda:type", "external"); - serviceTask2.businessObject.set("camunda:topic", "fjhdhg"); + serviceTask2.businessObject.set("camunda:topic", topicName); } // layout diagram after successful transformation From abfb5bf17c4a4ebaff335fea8e66596ea3a8950c Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Mon, 11 Sep 2023 17:09:12 +0200 Subject: [PATCH 26/33] fix tests --- components/bpmn-q/test/tests/editor/plugin.spec.js | 4 ---- 1 file changed, 4 deletions(-) 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); }); }); }); From 1219c524e0da8ecbe89255bbfec139a697861d07 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Thu, 21 Sep 2023 16:39:53 +0200 Subject: [PATCH 27/33] implement push on-demand deployements --- .../modeling/properties-provider/Connector.js | 26 ++-- .../ImplementationProps.js | 1 - .../modeling/properties-provider/YamlModal.js | 3 +- .../replacement/OnDemandTransformator.js | 144 ++++++++++++++---- 4 files changed, 127 insertions(+), 47 deletions(-) diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js index 84382986..33fb2197 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js @@ -125,13 +125,14 @@ function determineInputParameters(yamlData, schemePath) { } }} - const document = yaml.load(yamlData); - scheme = String(scheme.$ref).replace('#/', '').replaceAll('/', '.'); - - // Access the dynamically determined schema - const schemaPath = scheme; - const schema = getObjectByPath(document, schemaPath); + if (scheme.$ref) { + const document = yaml.load(yamlData); + scheme = String(scheme.$ref).replace('#/', '').replaceAll('/', '.'); + // Access the dynamically determined schema + const schemaPath = scheme; + scheme = getObjectByPath(document, schemaPath); + } // Function to access an object property by path function getObjectByPath(obj, path) { const parts = path.split('.'); @@ -146,7 +147,7 @@ function determineInputParameters(yamlData, schemePath) { } // Access the properties of the schema - const properties = Object.keys(schema.properties); + const properties = Object.keys(scheme.properties); return properties; } @@ -165,10 +166,11 @@ function determineOutputParameters(yamlData) { // Access the properties of the schema // Access the schema referenced by "200" const statusCode = "200"; - const schemaRef = response[statusCode].content["application/json"].schema.$ref; - const schemaPath = schemaRef.replace("#/", "").replaceAll("/", "."); - const schema = getObjectByPath2(data, schemaPath); - + let schema = response[statusCode].content["application/json"].schema; + if(schema.$ref) { + const schemaPath = schema.$ref.replace("#/", "").replaceAll("/", "."); + schema = getObjectByPath2(data, schemaPath); + } // Function to access an object property by path function getObjectByPath2(obj, path) { const parts = path.split('.'); @@ -230,4 +232,4 @@ function removeLastComma(str) { } else { return str.slice(0, lastIndex) + str.slice(lastIndex + 1); } -} +} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js index e35f9e6f..3b8d033f 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js @@ -102,7 +102,6 @@ export function ImplementationProps(props) { component: YamlUpload, isEdited: isTextFieldEntryEdited }) - console.log(!element.businessObject.deploymentModelUrl.includes(encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PULL)))) if (!element.businessObject.deploymentModelUrl.includes(encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PULL))) && element.businessObject.yaml !== undefined) { const urls = extractUrlsFromYaml(element.businessObject.yaml); entries.push({ diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js index cf79d1ba..2ad48048 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js @@ -48,7 +48,6 @@ export default function YamlModal(props) { }); }; reader.readAsText(uploadFile); - // Call close callback onClose(); }; @@ -66,7 +65,7 @@ export default function YamlModal(props) { { setUploadFile(e.target.files[0]) }} + onChange={(e) => { setUploadFile(e.target.files[0]); }} /> diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index 4aece5d6..bcbf1bbf 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -15,41 +15,51 @@ import {isDeployableServiceTask} from "../deployment/DeploymentUtils"; import * as config from "../framework-config/config-manager"; import {makeId} from "../deployment/OpenTOSCAUtils"; import {getCamundaEndpoint} from "../../../editor/config/EditorConfigManager"; +import {createElement} from "../../../editor/util/camunda-utils/ElementUtil"; +import {useService} from "bpmn-js-properties-panel"; +const fetchMethod = ` +function fetch(method, url, body) { + try { + 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; + } catch (e) { + java.lang.System.err.println(e); + return e; + } +}`; + 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; -} +${fetchMethod} var createCsarResponse = fetch('POST', params.opentoscaEndpoint, JSON.stringify({ @@ -77,6 +87,35 @@ var createInstanceResponse = fetch('POST', buildPlanUrl + "/instances", JSON.str execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", buildPlanUrl + "/instances/" + createInstanceResponse);`; } +function createWaitScript(params) { + return ` +var params = ${JSON.stringify(params)}; + +${fetchMethod} +var buildPlanInstanceUrl = execution.getVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl"); +var instanceUrl = buildPlanInstanceUrl.replace(/buildplans.+/, "instances") + +java.lang.System.out.println(instanceUrl); + +while(true) { + var createInstanceResponse = fetch('GET', instanceUrl); + var instances = JSON.parse(createInstanceResponse).service_template_instances; + if (instances && instances.length > 0 && instances[0].state === "CREATED") { + break; + } + java.lang.Thread.sleep(2000); +} + +var outputs = JSON.parse(fetch('GET', buildPlanInstanceUrl)).outputs; +for(var i = 0; i < outputs.length; i++) { + if(outputs[i].name === "selfserviceApplicationUrl") { + execution.setVariable("selfserviceApplicationUrl", outputs[i].value); + break; + } +} +`; +} + /** * Initiate the replacement process for the QuantME tasks that are contained in the current process model * @@ -90,6 +129,7 @@ export async function startOnDemandReplacementProcess(xml) { const elementRegistry = modeler.get('elementRegistry'); const bpmnReplace = modeler.get('bpmnReplace'); const bpmnAutoResizeProvider = modeler.get('bpmnAutoResizeProvider'); + const bpmnFactory = modeler.get('bpmnFactory'); bpmnAutoResizeProvider.canResize = () => false; const serviceTasks = elementRegistry.filter(({businessObject}) => isDeployableServiceTask(businessObject)); @@ -99,6 +139,10 @@ export async function startOnDemandReplacementProcess(xml) { if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); } + + const extensionElements = serviceTask.businessObject.extensionElements; + + console.log("SVCTSK", serviceTask) let subProcess = bpmnReplace.replaceElement(serviceTask, {type: 'bpmn:SubProcess'}); subProcess.businessObject.set("opentosca:onDemandDeployment", true); @@ -123,17 +167,53 @@ export async function startOnDemandReplacementProcess(xml) { } )); - const serviceTask2 = modeling.appendShape(serviceTask1, { + type: 'bpmn:ScriptTask', + }, {x: 600, y: 200}); + serviceTask2.businessObject.set("scriptFormat", "javascript"); + serviceTask2.businessObject.set("script", createWaitScript( + {subprocessId: subProcess.id} + )); + + + const serviceTask3 = modeling.appendShape(serviceTask2, { type: 'bpmn:ServiceTask' - }, {x: 600, y: 200}, subProcess); + }, {x: 800, y: 200}); + + if (!extensionElements) { + serviceTask3.businessObject.set("camunda:type", "external"); + serviceTask3.businessObject.set("camunda:topic", topicName); + + } else { + const values = extensionElements.values; + for (let value of values) { + if (value.inputOutput === undefined) continue; + for (let param of value.inputOutput.inputParameters) { + if (param.name === "url") { + param.value = `\${selfserviceApplicationUrl + ${JSON.stringify(param.value || "")}}`; + break; + } + } + } + + const newExtensionElements = createElement( + 'bpmn:ExtensionElements', + {values}, + serviceTask2.businessObject, + bpmnFactory + ); + subProcess.businessObject.set("extensionElements", undefined); + serviceTask3.businessObject.set("extensionElements", newExtensionElements); + } + const endTask = modeling.appendShape(serviceTask3, { + type: 'bpmn:EndEvent' + }, {x: 1000, 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; } From 2ee3ba2bf7d692611a84eb127b4c2eb94e253a8a Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Sun, 1 Oct 2023 17:47:01 +0200 Subject: [PATCH 28/33] improve retry mechanism --- .../replacement/OnDemandTransformator.js | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index bcbf1bbf..be7cc820 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -26,6 +26,7 @@ function fetch(method, url, body) { var urlConnection = resourceURL.openConnection(); urlConnection.setRequestMethod(method); + urlConnection.setRequestProperty("Accept", "application/json"); if (body) { urlConnection.setDoOutput(true); urlConnection.setRequestProperty("Content-Type", "application/json"); @@ -47,10 +48,11 @@ function fetch(method, url, body) { text += inputLine } bufferedReader.close(); + java.lang.System.out.println("Response from " + url + ": " + text); return text; } catch (e) { java.lang.System.err.println(e); - return e; + throw e; } }`; @@ -93,26 +95,33 @@ var params = ${JSON.stringify(params)}; ${fetchMethod} var buildPlanInstanceUrl = execution.getVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl"); -var instanceUrl = buildPlanInstanceUrl.replace(/buildplans.+/, "instances") +var instanceUrl; +for(var i = 0; i < 30; i++) { + try { + instanceUrl = JSON.parse(fetch('GET', buildPlanInstanceUrl))._links.service_template_instance.href; + if (instanceUrl) break; + } catch (e) { + } + java.lang.Thread.sleep(2000); +} -java.lang.System.out.println(instanceUrl); +java.lang.System.out.println("InstanceUrl: " + instanceUrl); -while(true) { - var createInstanceResponse = fetch('GET', instanceUrl); - var instances = JSON.parse(createInstanceResponse).service_template_instances; - if (instances && instances.length > 0 && instances[0].state === "CREATED") { - break; - } - java.lang.Thread.sleep(2000); +for(var i = 0; i < 30; i++) { + try { + var createInstanceResponse = fetch('GET', instanceUrl); + var instance = JSON.parse(createInstanceResponse).service_template_instances; + if (instance && instance.state === "CREATED") { + break; + } + } catch (e) { + } + java.lang.Thread.sleep(2000); } -var outputs = JSON.parse(fetch('GET', buildPlanInstanceUrl)).outputs; -for(var i = 0; i < outputs.length; i++) { - if(outputs[i].name === "selfserviceApplicationUrl") { - execution.setVariable("selfserviceApplicationUrl", outputs[i].value); - break; - } -} +var properties = JSON.parse(fetch('GET', instanceUrl + "/properties")); + +execution.setVariable("selfserviceApplicationUrl", properties.selfserviceApplicationUrl); `; } @@ -190,7 +199,7 @@ export async function startOnDemandReplacementProcess(xml) { if (value.inputOutput === undefined) continue; for (let param of value.inputOutput.inputParameters) { if (param.name === "url") { - param.value = `\${selfserviceApplicationUrl + ${JSON.stringify(param.value || "")}}`; + param.value = `\${selfserviceApplicationUrl.concat(${JSON.stringify(param.value || "")})}`; break; } } From 2028743919b2cbf35336799ffb22f63cb16a1bef Mon Sep 17 00:00:00 2001 From: lokmanfl Date: Sun, 1 Oct 2023 19:34:23 +0200 Subject: [PATCH 29/33] Implement changes suggested in review --- .vscode/settings.json | 2 -- Dockerfile | 11 +++++++++ README.md | 2 +- .../editor/plugin/PluginHandler.js | 4 ++-- .../extensions/opentosca/OpenTOSCAPlugin.js | 12 +++++----- .../opentosca/deployment/OpenTOSCAUtils.js | 2 +- .../opentosca/deployment/WineryUtils.js | 24 +++++++++++++++++++ .../framework-config/config-manager.js | 2 +- .../opentosca/framework-config/config.js | 2 +- .../opentosca/framework-config/index.js | 2 +- .../extensions/opentosca/modeling/index.js | 2 +- .../ArtifactUploadModal.js | 24 ++++++++++++++----- .../opentosca/utilities/Utilities.js | 2 +- .../quantme/modeling/QuantMESVGMap.js | 2 +- .../quantme/qrm-manager/git-handler.js | 2 +- .../tests/opentosca/opentosca-config.spec.js | 10 ++++---- 16 files changed, 75 insertions(+), 30 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41b..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aaf60721..01ceaabc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,17 @@ ARG ENABLE_PLANQK_PLUGIN ARG ENABLE_QHANA_PLUGIN ARG ENABLE_QUANTME_PLUGIN ARG ENABLE_OPENTOSCA_PLUGIN +ARG AUTOSAVE_INTERVAL +ARG CAMUNDA_ENDPOINT +ARG DOWNLOAD_FILE_NAME +ARG GITHUB_TOKEN +ARG QHANA_LIST_PLUGINS_URL +ARG SERVICE_DATA_CONFIG +ARG UPLOAD_BRANCH_NAME +ARG UPLOAD_FILE_NAME +ARG UPLOAD_GITHUB_REPO +ARG UPLOAD_GITHUB_USER + RUN env RUN npm run build -- --mode=production diff --git a/README.md b/README.md index d34c402a..a40a9f6e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ to start the modeler in a simple html website which runs on localhost:8080. ## Execution in Docker To serve the application from a Docker container execute: ``` -docker run --name workflow-modeler -p 8080:8080 ghcr.io/sequenc-consortium/workflow-modeler +docker run --name workflow-modeler -p 8080:8080 planqk/workflow-modeler ``` Afterwards the application is available in a browser on localhost:8080 diff --git a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js index efc963b5..61fa6075 100644 --- a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js +++ b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js @@ -1,6 +1,6 @@ import PlanQKPlugin from '../../extensions/planqk/PlanQKPlugin'; import QuantMEPlugin from '../../extensions/quantme/QuantMEPlugin'; -import OpenToscaPlugin from "../../extensions/opentosca/OpenTOSCAPlugin"; +import OpenTOSCAPlugin from "../../extensions/opentosca/OpenTOSCAPlugin"; import DataFlowPlugin from '../../extensions/data-extension/DataFlowPlugin'; import QHAnaPlugin from '../../extensions/qhana/QHAnaPlugin'; import { getAllConfigs } from './PluginConfigHandler'; @@ -32,7 +32,7 @@ const PLUGINS = [ dependencies: [] }, { - plugin: OpenToscaPlugin, + plugin: OpenTOSCAPlugin, dependencies: [] } ]; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js index fe43c934..bc5feb78 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js @@ -2,10 +2,10 @@ import React from "react"; import OpenTOSCATab from "./configTabs/OpenTOSCATab"; -import opentoscaStyles from './styling/opentosca.css'; +import OpenTOSCAStyles from './styling/opentosca.css'; import DeploymentPlugin from "./ui/deployment/services/DeploymentPlugin"; -import OpenToscaExtensionModule from "./modeling"; -let openToscaModdleExtension = require('./resources/opentosca4bpmn.json'); +import OpenTOSCAExtensionModule from "./modeling"; +let OpenTOSCAModdleExtension = require('./resources/opentosca4bpmn.json'); /** @@ -20,8 +20,8 @@ export default { configTab: OpenTOSCATab, } ], - extensionModule: OpenToscaExtensionModule, - moddleDescription: openToscaModdleExtension, + extensionModule: OpenTOSCAExtensionModule, + moddleDescription: OpenTOSCAModdleExtension, name: 'opentosca', - styling: [opentoscaStyles] + styling: [OpenTOSCAStyles] }; \ No newline at end of file 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 a394e504..1804914e 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js index 1cca23f9..0072e717 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js @@ -67,6 +67,30 @@ export async function createServiceTemplate(name) { return response.text(); } +export async function deleteArtifactTemplate(artifactTemplateName) { + // /artifacttemplates/http%253A%252F%252Fquantil.org%252Fquantme%252Fpush%252Fartifacttemplates/ArtifactTemplate-Activity_01b3qkz/ + const response = await fetch(`${getWineryEndpoint()}/artifacttemplates/${encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH + '/'))}artifacttemplates/${encodeURIComponent(encodeURIComponent(artifactTemplateName))}`, { + method: 'DELETE', + headers: { + 'Accept': 'application/json' + }, + }); + return response.status === 204; +} + +export async function serviceTemplateExists(serviceTemplateName) { + const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH))}/${encodeURIComponent(encodeURIComponent(serviceTemplateName))}`, { + method: 'GET', + }); + return response.status === 200; +} + +export async function addNodeWithArtifactToServiceTemplateByName(serviceTemplateName, nodeTypeQName, name, artifactTemplateQName, artifactName, artifactTypeQName) { + const serviceTemplateAddress = encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH)) + '/' + encodeURIComponent(encodeURIComponent(serviceTemplateName)) + '/'; + await addNodeWithArtifactToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name, artifactTemplateQName, artifactName, artifactTypeQName); + return serviceTemplateAddress; +} + export async function addNodeToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name) { const nodeTemplate = { "documentation": [], diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js index 849efc1c..e9614967 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js index 3adab154..144ee909 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js index c9a1d7e1..3acdba7f 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js index eac8b36b..814c71ca 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js index d5d7669d..2030e2d6 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js @@ -19,7 +19,10 @@ import { createServiceTemplateWithNodeAndArtifact, getNodeTypeQName, getArtifactTemplateInfo, - insertTopNodeTag + insertTopNodeTag, + serviceTemplateExists, + addNodeWithArtifactToServiceTemplateByName, + deleteArtifactTemplate } from '../../deployment/WineryUtils'; import NotificationHandler from '../../../../editor/ui/notifications/NotificationHandler'; import {getWineryEndpoint} from "../../framework-config/config-manager"; @@ -78,14 +81,22 @@ export default function ArtifactUploadModal({onClose, element, commandStack}) { try { const namePrefix = element.businessObject.name ?? ""; const artifactTemplateName = `${namePrefix}ArtifactTemplate-${element.id}`; + await deleteArtifactTemplate(artifactTemplateName); const artifactTemplateAddress = await createArtifactTemplateWithFile(artifactTemplateName, selectedOption, uploadFile); const artifactTemplateInfo = await getArtifactTemplateInfo(artifactTemplateAddress); const artifactTemplateQName = artifactTemplateInfo.serviceTemplateOrNodeTypeOrNodeTypeImplementation[0].type; const nodeTypeQName = getNodeTypeQName(selectedOption); const serviceTemplateName = `${namePrefix}ServiceTemplate-${element.id}`; - serviceTemplateAddress = await createServiceTemplateWithNodeAndArtifact(serviceTemplateName, nodeTypeQName, - `${namePrefix}Node-${element.id}`, artifactTemplateQName, - `${namePrefix}Artifact-${element.id}`, selectedOption); + const doesExist = await serviceTemplateExists(serviceTemplateName); + if(doesExist) { + serviceTemplateAddress = await addNodeWithArtifactToServiceTemplateByName(serviceTemplateName, nodeTypeQName, + `${namePrefix}Node-${element.id}`, artifactTemplateQName, + `${namePrefix}Artifact-${element.id}`, selectedOption); + } else { + serviceTemplateAddress = await createServiceTemplateWithNodeAndArtifact(serviceTemplateName, nodeTypeQName, + `${namePrefix}Node-${element.id}`, artifactTemplateQName, + `${namePrefix}Artifact-${element.id}`, selectedOption); + } await insertTopNodeTag(serviceTemplateAddress, nodeTypeQName); } catch (e) { setTimeout(closeNotification, 1); @@ -106,10 +117,11 @@ export default function ArtifactUploadModal({onClose, element, commandStack}) { } }); setTimeout(closeNotification, 1); + const content = doesExist ? 'updated' : 'created'; NotificationHandler.getInstance().displayNotification({ type: 'info', - title: 'Service Template Created', - content: 'Service Template including Nodetype with attached Deployment Artifact of chosen type was successfully created.', + title: 'Service Template ' + content.charAt(0).toUpperCase() + content.slice(1), + content: 'Service Template including Nodetype with attached Deployment Artifact of chosen type was successfully ' + content + '.', duration: 4000 }); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js b/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js index fc2ff59e..f1aaefea 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/utilities/Utilities.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/quantme/modeling/QuantMESVGMap.js b/components/bpmn-q/modeler-component/extensions/quantme/modeling/QuantMESVGMap.js index 7d47bcbf..423718ca 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/modeling/QuantMESVGMap.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/modeling/QuantMESVGMap.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/modeler-component/extensions/quantme/qrm-manager/git-handler.js b/components/bpmn-q/modeler-component/extensions/quantme/qrm-manager/git-handler.js index 232e1506..1556aeee 100644 --- a/components/bpmn-q/modeler-component/extensions/quantme/qrm-manager/git-handler.js +++ b/components/bpmn-q/modeler-component/extensions/quantme/qrm-manager/git-handler.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the diff --git a/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js b/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js index 865dae6a..13133bfb 100644 --- a/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js +++ b/components/bpmn-q/test/tests/opentosca/opentosca-config.spec.js @@ -2,19 +2,19 @@ import {setPluginConfig} from '../../../modeler-component/editor/plugin/PluginCo import {expect} from 'chai'; import * as opentoscaConfig from '../../../modeler-component/extensions/opentosca/framework-config/config-manager'; -describe('Test OpenTosca ConfigManager', function () { +describe('Test OpenTOSCA ConfigManager', function () { - describe('Test OpenTosca endpoint', function () { + describe('Test OpenTOSCA endpoint', function () { - before('Reset OpenTosca configuration', function () { + before('Reset OpenTOSCA configuration', function () { opentoscaConfig.resetConfig(); }); - afterEach('Reset OpenTosca configuration', function () { + afterEach('Reset OpenTOSCA configuration', function () { opentoscaConfig.resetConfig(); }); - it('Should configure OpenTosca endpoints', function () { + it('Should configure OpenTOSCA endpoints', function () { setPluginConfig([ { name: 'opentosca', From 70c8619584a22591a3f1335404c8b213590cee2b Mon Sep 17 00:00:00 2001 From: lokmanfl Date: Mon, 2 Oct 2023 21:11:12 +0200 Subject: [PATCH 30/33] change organization in docker-push.yaml to planqk --- .github/workflows/docker-publish.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index 820f1934..03c04d6d 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -11,14 +11,13 @@ jobs: contents: read packages: write steps: - - name: Login to GitHub Container Registry + - name: Login to DockerHub uses: docker/login-action@v2 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: push: true - tags: ghcr.io/sequenc-consortium/workflow-modeler:latest + tags: planqk/workflow-modeler:latest From e55c0b42f9840cd2665844ab3740280e13358b29 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Wed, 4 Oct 2023 10:51:27 +0200 Subject: [PATCH 31/33] name tasks in subprocess & extend timeout --- .../opentosca/replacement/OnDemandTransformator.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index be7cc820..9d3b0489 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -107,7 +107,7 @@ for(var i = 0; i < 30; i++) { java.lang.System.out.println("InstanceUrl: " + instanceUrl); -for(var i = 0; i < 30; i++) { +for(var i = 0; i < 30 * 8; i++) { try { var createInstanceResponse = fetch('GET', instanceUrl); var instance = JSON.parse(createInstanceResponse).service_template_instances; @@ -122,6 +122,7 @@ for(var i = 0; i < 30; i++) { var properties = JSON.parse(fetch('GET', instanceUrl + "/properties")); execution.setVariable("selfserviceApplicationUrl", properties.selfserviceApplicationUrl); +java.lang.Thread.sleep(12000); `; } @@ -151,7 +152,6 @@ export async function startOnDemandReplacementProcess(xml) { const extensionElements = serviceTask.businessObject.extensionElements; - console.log("SVCTSK", serviceTask) let subProcess = bpmnReplace.replaceElement(serviceTask, {type: 'bpmn:SubProcess'}); subProcess.businessObject.set("opentosca:onDemandDeployment", true); @@ -163,7 +163,7 @@ export async function startOnDemandReplacementProcess(xml) { let topicName = makeId(12); const serviceTask1 = modeling.appendShape(startEvent, { - type: 'bpmn:ScriptTask', + type: 'bpmn:ScriptTask' }, {x: 400, y: 200}); serviceTask1.businessObject.set("scriptFormat", "javascript"); serviceTask1.businessObject.set("script", createDeploymentScript( @@ -175,6 +175,7 @@ export async function startOnDemandReplacementProcess(xml) { camundaEndpoint: getCamundaEndpoint() } )); + serviceTask1.businessObject.set("name", "Create deployment"); const serviceTask2 = modeling.appendShape(serviceTask1, { type: 'bpmn:ScriptTask', @@ -183,12 +184,13 @@ export async function startOnDemandReplacementProcess(xml) { serviceTask2.businessObject.set("script", createWaitScript( {subprocessId: subProcess.id} )); - + serviceTask2.businessObject.set("name", "Wait for deployment"); const serviceTask3 = modeling.appendShape(serviceTask2, { - type: 'bpmn:ServiceTask' + type: 'bpmn:ServiceTask', }, {x: 800, y: 200}); + serviceTask3.businessObject.set("name", "Call service"); if (!extensionElements) { serviceTask3.businessObject.set("camunda:type", "external"); serviceTask3.businessObject.set("camunda:topic", topicName); From 51444a07ada89e6716db179cd10966c2eecd66c7 Mon Sep 17 00:00:00 2001 From: Maximilian Kuhn Date: Fri, 20 Oct 2023 01:37:15 +0100 Subject: [PATCH 32/33] Implement PR comments --- Dockerfile | 1 + .../bpmn-q/modeler-component/editor/ui/DeploymentButton.js | 5 ++--- .../modeler-component/editor/ui/OnDemandDeploymentModal.js | 7 ++++++- .../extensions/opentosca/OpenTOSCAPlugin.js | 2 +- .../modeling/properties-provider/ArtifactUploadModal.js | 1 + 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 01ceaabc..80733202 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ ARG AUTOSAVE_INTERVAL ARG CAMUNDA_ENDPOINT ARG DOWNLOAD_FILE_NAME ARG GITHUB_TOKEN +ARG QHANA_GET_PLUGIN_URL ARG QHANA_LIST_PLUGINS_URL ARG SERVICE_DATA_CONFIG ARG UPLOAD_BRANCH_NAME diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index e4b5c242..9180be11 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -26,12 +26,11 @@ export default function DeploymentButton(props) { /** - * Handle the result of a close operation on the tramfpr,atopm + * Handle result of the on demand deployment dialog * - * @param result the result from the close operation + * @param result the result from the dialog */ 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; diff --git a/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js index 46c0850c..d7ded0da 100644 --- a/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js +++ b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js @@ -16,6 +16,7 @@ import React from 'react'; import Modal from './modal/Modal'; const Title = Modal.Title || (({children}) =>

{children}

); +const Body = Modal.Body || (({children}) =>
{children}
); const Footer = Modal.Footer || (({children}) =>
{children}
); export default function OnDemandDeploymentModal({onClose}) { @@ -27,8 +28,12 @@ export default function OnDemandDeploymentModal({onClose}) { return - Enable On Demand Service Deployment? + Workflow Deployment + + The current workflow contains service task with attached deployment models which support on-demand service deployment. + Would you like to use on-demand service deployment? +