From b48d8ee684a7633352af974e347a6fec11ca19a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= Date: Mon, 14 Oct 2024 14:31:02 +0200 Subject: [PATCH] QHAna plugin refactor (#162) * Refactor QHAna plugin config and loading plugin list * Always provide a QHAna service task option * Refactor QHAna transformation to use variables over topic names * Fix broken QHAna tests due to config changes * Fix linting issues --- .../qhana/configTabs/QHAnaConfigTab.js | 42 ++-- .../configurations/QHAnaConfigurations.js | 201 +++++++++++++----- .../framework-config/QHAnaConfigManager.js | 49 +---- .../replacement/QHAnaTransformationHandler.js | 10 +- components/bpmn-q/public/env.js.template | 3 +- .../tests/qhana/qhana-plugin-config.spec.js | 10 +- .../tests/qhana/qhana-service-configs.spec.js | 7 +- components/bpmn-q/webpack.config.js | 3 +- .../modeler-configuration.md | 4 +- 9 files changed, 180 insertions(+), 149 deletions(-) diff --git a/components/bpmn-q/modeler-component/extensions/qhana/configTabs/QHAnaConfigTab.js b/components/bpmn-q/modeler-component/extensions/qhana/configTabs/QHAnaConfigTab.js index 5919b36c..58f81f8a 100644 --- a/components/bpmn-q/modeler-component/extensions/qhana/configTabs/QHAnaConfigTab.js +++ b/components/bpmn-q/modeler-component/extensions/qhana/configTabs/QHAnaConfigTab.js @@ -10,59 +10,41 @@ import { getModeler } from "../../../editor/ModelerHandler"; * @constructor */ export default function QHAnaConfigurationsTab() { - const [listPluginsEndpoint, setListPluginsEndpoint] = useState( - configManager.getListPluginsURL() - ); - const [getPluginEndpoint, setGetPluginEndpoint] = useState( - configManager.getGetPluginsURL() + const [getPluginRegistryURL, setPluginRegistryURL] = useState( + configManager.getPluginRegistryURL() ); const modeler = getModeler(); // save changed values on close QHAnaConfigurationsTab.prototype.onClose = () => { - modeler.config.listPluginsEndpoint = listPluginsEndpoint; - modeler.config.getGetPluginsURL = getPluginEndpoint; - configManager.setListPluginsURL(listPluginsEndpoint); - configManager.setGetPluginsURL(getPluginEndpoint); + modeler.config.getPluginRegistryURL = getPluginRegistryURL; + configManager.setPluginRegistryURL(setPluginRegistryURL); }; return ( - <> +

QHAna endpoint configuration:

- - - - - +
List Plugins Endpoint - setListPluginsEndpoint(event.target.value)} - /> -
Get Plugin EndpointPlugin Registry URL setGetPluginEndpoint(event.target.value)} + type="url" + name="qhanaPluginRegistryURL" + value={getPluginRegistryURL} + onChange={(event) => setPluginRegistryURL(event.target.value)} />
- +
); } QHAnaConfigurationsTab.prototype.config = () => { const modeler = getModeler(); - modeler.config.qhanaListPluginsURL = configManager.getListPluginsURL(); - modeler.config.qhanqGetPluginURL = configManager.getGetPluginsURL(); + modeler.config.qhanaPluginRegistryURL = configManager.getPluginRegistryURL(); }; diff --git a/components/bpmn-q/modeler-component/extensions/qhana/configurations/QHAnaConfigurations.js b/components/bpmn-q/modeler-component/extensions/qhana/configurations/QHAnaConfigurations.js index fd4a4d8c..4f1b27ec 100644 --- a/components/bpmn-q/modeler-component/extensions/qhana/configurations/QHAnaConfigurations.js +++ b/components/bpmn-q/modeler-component/extensions/qhana/configurations/QHAnaConfigurations.js @@ -2,6 +2,56 @@ import ConfigurationsEndpoint from "../../../editor/configurations/Configuration import * as configManager from "../framework-config/QHAnaConfigManager"; import * as consts from "../Constants"; +const CUSTOM_PLUGIN_CONFIG = { + name: "CUSTOM", + id: "CUSTOM", + description: "", + appliesTo: "qhana:QHAnaServiceTask", + groupLabel: "Service Properties", + attributes: [ + { + name: "identifier", + label: "Identifier", + type: "string", + value: "", + editable: "true", + bindTo: { + name: "qhanaIdentifier", + }, + }, + { + name: "version", + label: "Version", + type: "string", + value: "", + editable: "true", + bindTo: { + name: "qhanaVersion", + }, + }, + { + name: "name", + label: "Title", + type: "string", + value: "CUSTOM", + editable: "true", + bindTo: { + name: "qhanaName", + }, + }, + { + name: "description", + label: "Description", + type: "string", + value: "", + editable: "true", + bindTo: { + name: "qhanaDescription", + }, + }, + ], +}; + /** * Custom ConfigurationsEndpoint for the QHAna Plugin. Extends the ConfigurationsEndpoint to fetch the configurations directly * from the QHAna plugin registry. @@ -14,71 +64,105 @@ export default class QHAnaConfigurationsEndpoint extends ConfigurationsEndpoint /** * Fetch all plugins from the QHAna plugin registry and transform them into configurations for QHAna service tasks. */ - fetchConfigurations() { - const self = this; - - // fetch all QHAna services from the QHAna plugin registry - fetch(configManager.getListPluginsURL()) - .then((response) => response.json()) - .then((data) => { - try { - const allServices = data.data.items; - console.log("Received " + allServices.length + " QHAna services: "); - - let serviceId; - - // fetch details for each service and create configuration - allServices.forEach(function (service) { - serviceId = service.resourceKey.pluginId; - - // fetch plugin details for serviceId - fetch(configManager.getGetPluginsURL() + serviceId + "/") - .then((response) => response.json()) - .then((data) => { - const serviceData = data.data; - console.log( - "Received QHAna service details for service " + serviceId - ); - console.log(serviceData); - - // create configuration from serviceData - self._configurations.push( - createConfigurationForServiceData(serviceData) - ); - }) - .catch((error) => { - console.error( - "Error fetching QHAna service with id " + - serviceId + - ": \n" + - error - ); - }); - }); - } catch (error) { - console.error( - "Error while parsing QHAna services from " + - configManager.getGetPluginsURL() + - ": \n" + - error - ); + async fetchConfigurations() { + const newConfigurations = []; + + const registryUrl = configManager.getPluginRegistryURL(); + + if (!registryUrl) { + console.info( + "Cannot fetch QHAna Plugins, Plugin Registry URL is not configured." + ); + return; // nothing to fetch, registry is not configured + } + + let pluginsLink = null; + try { + const apiResult = await (await fetch(registryUrl)).json(); + pluginsLink = apiResult?.links?.find?.( + (link) => + link.resourceType === "plugin" && + link.rel.some((r) => r === "collection") + ); + } catch (error) { + console.error( + "Could not reach QHAna Plugin Registry to load available plugins!", + error + ); + } + + if (!pluginsLink) { + // no plugins found + this.configurations = newConfigurations; + return; + } + + async function loadPlugins(url, configurations, seen) { + try { + const pluginListResponse = await (await fetch(url)).json(); + + await Promise.allSettled( + pluginListResponse.data.items.map(async (pluginLink) => { + if (seen.has(pluginLink.href)) { + return; // plugin already processed + } + seen.add(pluginLink.href); + + let pluginResponse = pluginListResponse.embedded.find( + (e) => e.data.self.href === pluginLink.href + ); + + try { + if (!pluginResponse) { + pluginResponse = await (await fetch(pluginLink.href)).json(); + } + + // create configuration from plugin data + configurations.push( + createConfigurationForServiceData(pluginResponse.data) + ); + } catch (error) { + console.error( + `Failed to load plugin ${pluginLink.name} (${pluginLink.href})!`, + error + ); + } + }) + ); + + const nextLink = pluginListResponse.links.find( + (link) => + link.resourceType === "plugin" && + link.rel.some((r) => r === "page") && + link.rel.some((r) => r === "next") + ); + if (nextLink && nextLink.href !== url) { + await loadPlugins(nextLink.href, configurations, seen); } - }) - .catch((error) => { + } catch (error) { console.error( - "Error fetching configurations from " + - configManager.getListPluginsURL() + - ": \n" + - error + "Failed to fetch plugin page from QHAna Plugin Registry.", + error ); - }); + } + } + + await loadPlugins(pluginsLink.href, newConfigurations, new Set()); + + console.info(`${newConfigurations.length} QHAna plugins loaded`); + + this.configurations = newConfigurations; + return; } /** * Returns all Configurations for QHAna service tasks which are saved in this endpoint. */ getQHAnaServiceConfigurations() { - return this.getConfigurations(consts.QHANA_SERVICE_TASK); + return [ + CUSTOM_PLUGIN_CONFIG, + ...this.getConfigurations(consts.QHANA_SERVICE_TASK), + ]; } /** @@ -88,6 +172,9 @@ export default class QHAnaConfigurationsEndpoint extends ConfigurationsEndpoint * @return {*} */ getQHAnaServiceConfiguration(id) { + if (id === "CUSTOM") { + return CUSTOM_PLUGIN_CONFIG; + } return this.getConfiguration(id); } @@ -208,7 +295,5 @@ export function createConfigurationForServiceData(serviceData) { }); }); - console.log("Created configuration for QHAna service"); - console.log(configuration); return configuration; } diff --git a/components/bpmn-q/modeler-component/extensions/qhana/framework-config/QHAnaConfigManager.js b/components/bpmn-q/modeler-component/extensions/qhana/framework-config/QHAnaConfigManager.js index b8c3a5bf..e90abca3 100644 --- a/components/bpmn-q/modeler-component/extensions/qhana/framework-config/QHAnaConfigManager.js +++ b/components/bpmn-q/modeler-component/extensions/qhana/framework-config/QHAnaConfigManager.js @@ -2,62 +2,33 @@ import { getPluginConfig } from "../../../editor/plugin/PluginConfigHandler"; // default config entries used if no value is specified in the initial plugin config const defaultConfig = { - qhanaListPluginsURL: process.env.QHANA_LIST_PLUGINS_URL, - qhanqGetPluginURL: process.env.QHANA_GET_PLUGIN_URL, + qhanaPluginRegistryURL: process.env.QHANA_PLUGIN_REGISTRY_URL ?? "", }; const config = {}; /** - * Get the url to list all plugins of the QHAna plugin registry + * Get the url of the QHAna plugin registry * * @return {string} the url */ -export function getListPluginsURL() { +export function getPluginRegistryURL() { if (config.qhanaListPluginsURL === undefined) { - setListPluginsURL( - getPluginConfig("qhana").qhanaListPluginsURL || - defaultConfig.qhanaListPluginsURL + setPluginRegistryURL( + getPluginConfig("qhana").qhanaPluginRegistryURL || + defaultConfig.qhanaPluginRegistryURL ); } - return config.qhanaListPluginsURL; + return config.qhanaPluginRegistryURL; } /** - * Set the url to list all plugins of the QHAna plugin registry + * Set the url of the QHAna plugin registry * * @return {string} the url */ -export function setListPluginsURL(url) { +export function setPluginRegistryURL(url) { if (url !== null && url !== undefined) { - // remove trailing slashes - config.qhanaListPluginsURL = url.replace(/\/$/, ""); - } -} - -/** - * Get the url to get a specific plugin from the QHAna plugin registry - * - * @return {string} the url - */ -export function getGetPluginsURL() { - if (config.qhanqGetPluginURL === undefined) { - setGetPluginsURL( - getPluginConfig("qhana").qhanqGetPluginURL || - defaultConfig.qhanqGetPluginURL - ); - } - return config.qhanqGetPluginURL; -} - -/** - * Set the url to get a specific plugin from the QHAna plugin registry - * - * @return {string} the url - */ -export function setGetPluginsURL(url) { - if (url !== null && url !== undefined) { - // remove trailing slashes - config.qhanqGetPluginURL = url; + config.qhanaPluginRegistryURL = url; } } diff --git a/components/bpmn-q/modeler-component/extensions/qhana/replacement/QHAnaTransformationHandler.js b/components/bpmn-q/modeler-component/extensions/qhana/replacement/QHAnaTransformationHandler.js index 3b4627a8..a5eed359 100644 --- a/components/bpmn-q/modeler-component/extensions/qhana/replacement/QHAnaTransformationHandler.js +++ b/components/bpmn-q/modeler-component/extensions/qhana/replacement/QHAnaTransformationHandler.js @@ -155,7 +155,7 @@ async function replaceQHAnaServiceTaskByServiceTask( const bpmnFactory = modeler.get("bpmnFactory"); // create a BPMN service task with implementation external - const topic = "qhana-plugin." + qhanaServiceTask.get(consts.IDENTIFIER); + const topic = "qhana-task"; const newServiceTask = bpmnFactory.create("bpmn:ServiceTask", { type: "external", topic: topic, @@ -176,13 +176,13 @@ async function replaceQHAnaServiceTaskByServiceTask( const newElement = result.element; addCamundaInputParameter( newElement.businessObject, - "qhanaIdentifier", + "qhanaPlugin", qhanaServiceTask.qhanaIdentifier, bpmnFactory ); addCamundaInputParameter( newElement.businessObject, - "qhanaVersion", + "qhanaPluginVersion", qhanaServiceTask.qhanaVersion, bpmnFactory ); @@ -221,7 +221,7 @@ async function replaceQHAnaServiceStepTaskByServiceTask( const bpmnFactory = modeler.get("bpmnFactory"); // create a BPMN service task with implementation external and the topic defined in the next step attribute - const topic = "plugin-step." + consts.NEXT_STEP; + const topic = "qhana-task"; const newServiceTask = bpmnFactory.create("bpmn:ServiceTask", { type: "external", topic: topic, @@ -242,7 +242,7 @@ async function replaceQHAnaServiceStepTaskByServiceTask( const newElement = result.element; addCamundaInputParameter( newElement.businessObject, - "qhanaNextStep", + "qhanaPluginStep", qhanaServiceTask.qhanaNextStep, bpmnFactory ); diff --git a/components/bpmn-q/public/env.js.template b/components/bpmn-q/public/env.js.template index 3002bf3b..6fa0ff91 100644 --- a/components/bpmn-q/public/env.js.template +++ b/components/bpmn-q/public/env.js.template @@ -20,8 +20,7 @@ window.env = { "PATTERN_ATLAS_UI_ENDPOINT": "${PATTERN_ATLAS_UI_ENDPOINT}" !== "" ? "${PATTERN_ATLAS_UI_ENDPOINT}" : undefined, "PROVENANCE_COLLECTION": "${PROVENANCE_COLLECTION}" !== "" ? "${PROVENANCE_COLLECTION}" : undefined, "QC_ATLAS_ENDPOINT": "${QC_ATLAS_ENDPOINT}" !== "" ? "${QC_ATLAS_ENDPOINT}" : undefined, - "QHANA_GET_PLUGIN_URL": "${QHANA_GET_PLUGIN_URL}" !== "" ? "${QHANA_GET_PLUGIN_URL}" : undefined, - "QHANA_LIST_PLUGINS_URL": "${QHANA_LIST_PLUGINS_URL}" !== "" ? "${QHANA_LIST_PLUGINS_URL}" : undefined, + "QHANA_PLUGIN_REGISTRY_URL": "${QHANA_PLUGIN_REGISTRY_URL}" !== "" ? "${QHANA_PLUGIN_REGISTRY_URL}" : undefined, "QISKIT_RUNTIME_HANDLER_ENDPOINT": "${QISKIT_RUNTIME_HANDLER_ENDPOINT}" !== "" ? "${QISKIT_RUNTIME_HANDLER_ENDPOINT}" : undefined, "QPROV_ENDPOINT": "${QPROV_ENDPOINT}" !== "" ? "${QPROV_ENDPOINT}" : undefined, "QRM_USERNAME": "${QRM_USERNAME}" !== "" ? "${QRM_USERNAME}" : undefined, diff --git a/components/bpmn-q/test/tests/qhana/qhana-plugin-config.spec.js b/components/bpmn-q/test/tests/qhana/qhana-plugin-config.spec.js index a83d02ff..13340ac8 100644 --- a/components/bpmn-q/test/tests/qhana/qhana-plugin-config.spec.js +++ b/components/bpmn-q/test/tests/qhana/qhana-plugin-config.spec.js @@ -9,17 +9,13 @@ describe("Test QHAna plugin config", function () { { name: "qhana", config: { - qhanaListPluginsURL: "http://test:5006/api/plugins/?item-count=100", - qhanqGetPluginURL: "http://test:5006/api/plugins/", + qhanaPluginRegistryURL: "http://test:5006/api/", }, }, ]); - expect(qhanaConfig.getListPluginsURL()).to.equal( - "http://test:5006/api/plugins/?item-count=100" - ); - expect(qhanaConfig.getGetPluginsURL()).to.equal( - "http://test:5006/api/plugins/" + expect(qhanaConfig.getPluginRegistryURL()).to.equal( + "http://test:5006/api/" ); }); }); diff --git a/components/bpmn-q/test/tests/qhana/qhana-service-configs.spec.js b/components/bpmn-q/test/tests/qhana/qhana-service-configs.spec.js index a3c4c205..68025d73 100644 --- a/components/bpmn-q/test/tests/qhana/qhana-service-configs.spec.js +++ b/components/bpmn-q/test/tests/qhana/qhana-service-configs.spec.js @@ -39,9 +39,10 @@ describe("Test QHAnaConfigurations", function () { sinon.assert.calledOnce(fetchStub); - expect(configurations.length).to.equal(2); - expect(configurations[0].name).to.equal("Aggregators"); - expect(configurations[1].name).to.equal("Ultimate Aggregators"); + expect(configurations.length).to.equal(3); + expect(configurations[0].name).to.equal("CUSTOM"); + expect(configurations[1].name).to.equal("Aggregators"); + expect(configurations[2].name).to.equal("Ultimate Aggregators"); }); }); diff --git a/components/bpmn-q/webpack.config.js b/components/bpmn-q/webpack.config.js index 8f9daa04..c73ff93e 100644 --- a/components/bpmn-q/webpack.config.js +++ b/components/bpmn-q/webpack.config.js @@ -25,8 +25,7 @@ let defaultConfig = { PATTERN_ATLAS_UI_ENDPOINT: "http://localhost:1978", PROVENANCE_COLLECTION: "false", QC_ATLAS_ENDPOINT: "http://localhost:6626", - QHANA_GET_PLUGIN_URL: "http://localhost:5006/api/plugins/", - QHANA_LIST_PLUGINS_URL: "http://localhost:5006/api/plugins/?item-count=100", + QHANA_PLUGIN_REGISTRY_URL: "http://localhost:5006/api/", QISKIT_RUNTIME_HANDLER_ENDPOINT: "http://localhost:8889", QPROV_ENDPOINT: "http://localhost:8099/qprov", QRM_USERNAME: "", diff --git a/doc/quantum-workflow-modeler/modeler-configuration.md b/doc/quantum-workflow-modeler/modeler-configuration.md index 996afbef..b6071915 100644 --- a/doc/quantum-workflow-modeler/modeler-configuration.md +++ b/doc/quantum-workflow-modeler/modeler-configuration.md @@ -38,9 +38,7 @@ In the following, all environment variables that can be used to customize the wo * ```QISKIT_RUNTIME_HANDLER_ENDPOINT``` (default: 'http://localhost:8889'): Defines the endpoint of the [Qiskit Runtime Handler](https://github.com/UST-QuAntiL/qiskit-runtime-handler) which enables the automatic generation of hybrid programs from Qiskit programs. -* ```QHANA_GET_PLUGIN_URL``` (default: 'http://localhost:5006/api/plugins/'): Defines the plugin url for QHAna. - -* ```QHANA_LIST_PLUGINS_URL``` (default: 'http://localhost:5006/api/plugins/?item-count=100'): Defines the plugin list url for QHAna. +* ```QHANA_PLUGIN_REGISTRY_URL``` (default: 'http://localhost:5006/api/'): Defines the url of the plugin registry api for QHAna. * ```QProv_ENDPOINT``` (default: 'http://localhost:8099/qprov'): Defines the endpoint of [QProv](https://github.com/UST-QuAntiL/qprov) to store and retrieve provenance data.