From 2423adfa2d8b58875094f9fe00ece0fe94b70067 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 12 Aug 2025 17:56:14 +0200 Subject: [PATCH 01/38] Remove old and deprecated environment deployment --- Control/lib/api.js | 2 +- Control/lib/control-core/GrpcServiceClient.js | 24 +- Control/protobuf/environmentinfo_pb.js | 1101 ----------------- Control/protobuf/status_pb.js | 237 ---- 4 files changed, 4 insertions(+), 1360 deletions(-) delete mode 100644 Control/protobuf/environmentinfo_pb.js delete mode 100644 Control/protobuf/status_pb.js diff --git a/Control/lib/api.js b/Control/lib/api.js index ccb2bc2c7..68580101d 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -174,7 +174,7 @@ module.exports.setup = (http, ws) => { ctrlProxy.methods.forEach( (method) => http.post(`/${method}`, coreMiddleware, (req, res) => ctrlService.executeCommand(req, res)), ); - http.post('/core/request', coreMiddleware, (req, res) => aliecsReqHandler.add(req, res)); + http.get('/core/requests', coreMiddleware, (req, res) => aliecsReqHandler.getAll(req, res)); http.post('/core/removeRequest/:id', coreMiddleware, (req, res) => aliecsReqHandler.remove(req, res)); diff --git a/Control/lib/control-core/GrpcServiceClient.js b/Control/lib/control-core/GrpcServiceClient.js index 177eaca2a..219f28d5a 100644 --- a/Control/lib/control-core/GrpcServiceClient.js +++ b/Control/lib/control-core/GrpcServiceClient.js @@ -15,10 +15,7 @@ // Doc: https://grpc.io/docs/languages/node/ const protoLoader = require('@grpc/proto-loader'); const grpcLibrary = require('@grpc/grpc-js'); -const path = require('path'); -const {LogManager, grpcErrorToNativeError, ServiceUnavailableError, InvalidInputError} = require('@aliceo2/web-ui'); -const {Status} = require(path.join(__dirname, './../../protobuf/status_pb.js')); -const {EnvironmentInfo} = require(path.join(__dirname, './../../protobuf/environmentinfo_pb.js')); +const {LogManager, ServiceUnavailableError, InvalidInputError} = require('@aliceo2/web-ui'); const RECONNECT_DELTA_TIME = 1000; // 1 second /** @@ -102,23 +99,8 @@ class GrpcServiceClient { return new Promise((resolve, reject) => { this.client[methodName](args, options, (error, response) => { if (error) { - try { - if (methodName === 'NewEnvironment' && error.metadata?.internalRepr?.has('grpc-status-details-bin')) { - const buffer = error.metadata.get('grpc-status-details-bin')[0]; - Status.deserializeBinary(buffer).getDetailsList().map((detail) => { - if (detail.getTypeName() == 'o2control.EnvironmentInfo') { - const deserialized = detail.unpack(EnvironmentInfo.deserializeBinary, detail.getTypeName()); - error.envId = deserialized.array[0]; - } - }); - } - reject(error); - } catch (exception) { - this._logger.debugMessage('Failed new env details error' + exception); - reject(exception); - } - reject(grpcErrorToNativeError(error)); - return; + reject(error); + return } resolve(response); }); diff --git a/Control/protobuf/environmentinfo_pb.js b/Control/protobuf/environmentinfo_pb.js deleted file mode 100644 index fa7d8a6a8..000000000 --- a/Control/protobuf/environmentinfo_pb.js +++ /dev/null @@ -1,1101 +0,0 @@ -/** - * @fileoverview - * @enhanceable - * @suppress {messageConventions} JS Compiler reports an error if a variable or - * field starts with 'MSG_' and isn't a translatable message. - * @public - */ -// GENERATED CODE -- DO NOT EDIT! - -var jspb = require('google-protobuf'); -var goog = jspb; -var global = Function('return this')(); - -goog.exportSymbol('proto.o2control.EnvironmentInfo', null, global); -goog.exportSymbol('proto.o2control.ShortTaskInfo', null, global); -goog.exportSymbol('proto.o2control.TaskDeploymentInfo', null, global); - -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.o2control.EnvironmentInfo = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, proto.o2control.EnvironmentInfo.repeatedFields_, null); -}; -goog.inherits(proto.o2control.EnvironmentInfo, jspb.Message); -if (goog.DEBUG && !COMPILED) { - proto.o2control.EnvironmentInfo.displayName = 'proto.o2control.EnvironmentInfo'; -} -/** - * List of repeated fields within this message type. - * @private {!Array} - * @const - */ -proto.o2control.EnvironmentInfo.repeatedFields_ = [4,11]; - - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto suitable for use in Soy templates. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. - * @param {boolean=} opt_includeInstance Whether to include the JSPB instance - * for transitional soy proto support: http://goto/soy-param-migration - * @return {!Object} - */ -proto.o2control.EnvironmentInfo.prototype.toObject = function(opt_includeInstance) { - return proto.o2control.EnvironmentInfo.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Whether to include the JSPB - * instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.o2control.EnvironmentInfo} msg The msg instance to transform. - * @return {!Object} - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.o2control.EnvironmentInfo.toObject = function(includeInstance, msg) { - var f, obj = { - id: jspb.Message.getFieldWithDefault(msg, 1, ""), - createdwhen: jspb.Message.getFieldWithDefault(msg, 2, 0), - state: jspb.Message.getFieldWithDefault(msg, 3, ""), - tasksList: jspb.Message.toObjectList(msg.getTasksList(), - proto.o2control.ShortTaskInfo.toObject, includeInstance), - rootrole: jspb.Message.getFieldWithDefault(msg, 5, ""), - currentrunnumber: jspb.Message.getFieldWithDefault(msg, 6, 0), - defaultsMap: (f = msg.getDefaultsMap()) ? f.toObject(includeInstance, undefined) : [], - varsMap: (f = msg.getVarsMap()) ? f.toObject(includeInstance, undefined) : [], - uservarsMap: (f = msg.getUservarsMap()) ? f.toObject(includeInstance, undefined) : [], - numberofflps: jspb.Message.getFieldWithDefault(msg, 10, 0), - includeddetectorsList: jspb.Message.getRepeatedField(msg, 11), - description: jspb.Message.getFieldWithDefault(msg, 12, "") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.o2control.EnvironmentInfo} - */ -proto.o2control.EnvironmentInfo.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.o2control.EnvironmentInfo; - return proto.o2control.EnvironmentInfo.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.o2control.EnvironmentInfo} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.o2control.EnvironmentInfo} - */ -proto.o2control.EnvironmentInfo.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setId(value); - break; - case 2: - var value = /** @type {number} */ (reader.readInt64()); - msg.setCreatedwhen(value); - break; - case 3: - var value = /** @type {string} */ (reader.readString()); - msg.setState(value); - break; - case 4: - var value = new proto.o2control.ShortTaskInfo; - reader.readMessage(value,proto.o2control.ShortTaskInfo.deserializeBinaryFromReader); - msg.addTasks(value); - break; - case 5: - var value = /** @type {string} */ (reader.readString()); - msg.setRootrole(value); - break; - case 6: - var value = /** @type {number} */ (reader.readUint32()); - msg.setCurrentrunnumber(value); - break; - case 7: - var value = msg.getDefaultsMap(); - reader.readMessage(value, function(message, reader) { - jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString); - }); - break; - case 8: - var value = msg.getVarsMap(); - reader.readMessage(value, function(message, reader) { - jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString); - }); - break; - case 9: - var value = msg.getUservarsMap(); - reader.readMessage(value, function(message, reader) { - jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString); - }); - break; - case 10: - var value = /** @type {number} */ (reader.readInt32()); - msg.setNumberofflps(value); - break; - case 11: - var value = /** @type {string} */ (reader.readString()); - msg.addIncludeddetectors(value); - break; - case 12: - var value = /** @type {string} */ (reader.readString()); - msg.setDescription(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.o2control.EnvironmentInfo.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.o2control.EnvironmentInfo.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.o2control.EnvironmentInfo} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.o2control.EnvironmentInfo.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getId(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } - f = message.getCreatedwhen(); - if (f !== 0) { - writer.writeInt64( - 2, - f - ); - } - f = message.getState(); - if (f.length > 0) { - writer.writeString( - 3, - f - ); - } - f = message.getTasksList(); - if (f.length > 0) { - writer.writeRepeatedMessage( - 4, - f, - proto.o2control.ShortTaskInfo.serializeBinaryToWriter - ); - } - f = message.getRootrole(); - if (f.length > 0) { - writer.writeString( - 5, - f - ); - } - f = message.getCurrentrunnumber(); - if (f !== 0) { - writer.writeUint32( - 6, - f - ); - } - f = message.getDefaultsMap(true); - if (f && f.getLength() > 0) { - f.serializeBinary(7, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); - } - f = message.getVarsMap(true); - if (f && f.getLength() > 0) { - f.serializeBinary(8, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); - } - f = message.getUservarsMap(true); - if (f && f.getLength() > 0) { - f.serializeBinary(9, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); - } - f = message.getNumberofflps(); - if (f !== 0) { - writer.writeInt32( - 10, - f - ); - } - f = message.getIncludeddetectorsList(); - if (f.length > 0) { - writer.writeRepeatedString( - 11, - f - ); - } - f = message.getDescription(); - if (f.length > 0) { - writer.writeString( - 12, - f - ); - } -}; - - -/** - * optional string id = 1; - * @return {string} - */ -proto.o2control.EnvironmentInfo.prototype.getId = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** @param {string} value */ -proto.o2control.EnvironmentInfo.prototype.setId = function(value) { - jspb.Message.setProto3StringField(this, 1, value); -}; - - -/** - * optional int64 createdWhen = 2; - * @return {number} - */ -proto.o2control.EnvironmentInfo.prototype.getCreatedwhen = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); -}; - - -/** @param {number} value */ -proto.o2control.EnvironmentInfo.prototype.setCreatedwhen = function(value) { - jspb.Message.setProto3IntField(this, 2, value); -}; - - -/** - * optional string state = 3; - * @return {string} - */ -proto.o2control.EnvironmentInfo.prototype.getState = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); -}; - - -/** @param {string} value */ -proto.o2control.EnvironmentInfo.prototype.setState = function(value) { - jspb.Message.setProto3StringField(this, 3, value); -}; - - -/** - * repeated ShortTaskInfo tasks = 4; - * @return {!Array.} - */ -proto.o2control.EnvironmentInfo.prototype.getTasksList = function() { - return /** @type{!Array.} */ ( - jspb.Message.getRepeatedWrapperField(this, proto.o2control.ShortTaskInfo, 4)); -}; - - -/** @param {!Array.} value */ -proto.o2control.EnvironmentInfo.prototype.setTasksList = function(value) { - jspb.Message.setRepeatedWrapperField(this, 4, value); -}; - - -/** - * @param {!proto.o2control.ShortTaskInfo=} opt_value - * @param {number=} opt_index - * @return {!proto.o2control.ShortTaskInfo} - */ -proto.o2control.EnvironmentInfo.prototype.addTasks = function(opt_value, opt_index) { - return jspb.Message.addToRepeatedWrapperField(this, 4, opt_value, proto.o2control.ShortTaskInfo, opt_index); -}; - - -proto.o2control.EnvironmentInfo.prototype.clearTasksList = function() { - this.setTasksList([]); -}; - - -/** - * optional string rootRole = 5; - * @return {string} - */ -proto.o2control.EnvironmentInfo.prototype.getRootrole = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, "")); -}; - - -/** @param {string} value */ -proto.o2control.EnvironmentInfo.prototype.setRootrole = function(value) { - jspb.Message.setProto3StringField(this, 5, value); -}; - - -/** - * optional uint32 currentRunNumber = 6; - * @return {number} - */ -proto.o2control.EnvironmentInfo.prototype.getCurrentrunnumber = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); -}; - - -/** @param {number} value */ -proto.o2control.EnvironmentInfo.prototype.setCurrentrunnumber = function(value) { - jspb.Message.setProto3IntField(this, 6, value); -}; - - -/** - * map defaults = 7; - * @param {boolean=} opt_noLazyCreate Do not create the map if - * empty, instead returning `undefined` - * @return {!jspb.Map} - */ -proto.o2control.EnvironmentInfo.prototype.getDefaultsMap = function(opt_noLazyCreate) { - return /** @type {!jspb.Map} */ ( - jspb.Message.getMapField(this, 7, opt_noLazyCreate, - null)); -}; - - -proto.o2control.EnvironmentInfo.prototype.clearDefaultsMap = function() { - this.getDefaultsMap().clear(); -}; - - -/** - * map vars = 8; - * @param {boolean=} opt_noLazyCreate Do not create the map if - * empty, instead returning `undefined` - * @return {!jspb.Map} - */ -proto.o2control.EnvironmentInfo.prototype.getVarsMap = function(opt_noLazyCreate) { - return /** @type {!jspb.Map} */ ( - jspb.Message.getMapField(this, 8, opt_noLazyCreate, - null)); -}; - - -proto.o2control.EnvironmentInfo.prototype.clearVarsMap = function() { - this.getVarsMap().clear(); -}; - - -/** - * map userVars = 9; - * @param {boolean=} opt_noLazyCreate Do not create the map if - * empty, instead returning `undefined` - * @return {!jspb.Map} - */ -proto.o2control.EnvironmentInfo.prototype.getUservarsMap = function(opt_noLazyCreate) { - return /** @type {!jspb.Map} */ ( - jspb.Message.getMapField(this, 9, opt_noLazyCreate, - null)); -}; - - -proto.o2control.EnvironmentInfo.prototype.clearUservarsMap = function() { - this.getUservarsMap().clear(); -}; - - -/** - * optional int32 numberOfFlps = 10; - * @return {number} - */ -proto.o2control.EnvironmentInfo.prototype.getNumberofflps = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 10, 0)); -}; - - -/** @param {number} value */ -proto.o2control.EnvironmentInfo.prototype.setNumberofflps = function(value) { - jspb.Message.setProto3IntField(this, 10, value); -}; - - -/** - * repeated string includedDetectors = 11; - * @return {!Array.} - */ -proto.o2control.EnvironmentInfo.prototype.getIncludeddetectorsList = function() { - return /** @type {!Array.} */ (jspb.Message.getRepeatedField(this, 11)); -}; - - -/** @param {!Array.} value */ -proto.o2control.EnvironmentInfo.prototype.setIncludeddetectorsList = function(value) { - jspb.Message.setField(this, 11, value || []); -}; - - -/** - * @param {!string} value - * @param {number=} opt_index - */ -proto.o2control.EnvironmentInfo.prototype.addIncludeddetectors = function(value, opt_index) { - jspb.Message.addToRepeatedField(this, 11, value, opt_index); -}; - - -proto.o2control.EnvironmentInfo.prototype.clearIncludeddetectorsList = function() { - this.setIncludeddetectorsList([]); -}; - - -/** - * optional string description = 12; - * @return {string} - */ -proto.o2control.EnvironmentInfo.prototype.getDescription = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 12, "")); -}; - - -/** @param {string} value */ -proto.o2control.EnvironmentInfo.prototype.setDescription = function(value) { - jspb.Message.setProto3StringField(this, 12, value); -}; - - - -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.o2control.ShortTaskInfo = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); -}; -goog.inherits(proto.o2control.ShortTaskInfo, jspb.Message); -if (goog.DEBUG && !COMPILED) { - proto.o2control.ShortTaskInfo.displayName = 'proto.o2control.ShortTaskInfo'; -} - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto suitable for use in Soy templates. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. - * @param {boolean=} opt_includeInstance Whether to include the JSPB instance - * for transitional soy proto support: http://goto/soy-param-migration - * @return {!Object} - */ -proto.o2control.ShortTaskInfo.prototype.toObject = function(opt_includeInstance) { - return proto.o2control.ShortTaskInfo.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Whether to include the JSPB - * instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.o2control.ShortTaskInfo} msg The msg instance to transform. - * @return {!Object} - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.o2control.ShortTaskInfo.toObject = function(includeInstance, msg) { - var f, obj = { - name: jspb.Message.getFieldWithDefault(msg, 1, ""), - locked: jspb.Message.getFieldWithDefault(msg, 2, false), - taskid: jspb.Message.getFieldWithDefault(msg, 3, ""), - status: jspb.Message.getFieldWithDefault(msg, 4, ""), - state: jspb.Message.getFieldWithDefault(msg, 5, ""), - classname: jspb.Message.getFieldWithDefault(msg, 6, ""), - deploymentinfo: (f = msg.getDeploymentinfo()) && proto.o2control.TaskDeploymentInfo.toObject(includeInstance, f), - pid: jspb.Message.getFieldWithDefault(msg, 8, ""), - sandboxstdout: jspb.Message.getFieldWithDefault(msg, 9, "") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.o2control.ShortTaskInfo} - */ -proto.o2control.ShortTaskInfo.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.o2control.ShortTaskInfo; - return proto.o2control.ShortTaskInfo.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.o2control.ShortTaskInfo} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.o2control.ShortTaskInfo} - */ -proto.o2control.ShortTaskInfo.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setName(value); - break; - case 2: - var value = /** @type {boolean} */ (reader.readBool()); - msg.setLocked(value); - break; - case 3: - var value = /** @type {string} */ (reader.readString()); - msg.setTaskid(value); - break; - case 4: - var value = /** @type {string} */ (reader.readString()); - msg.setStatus(value); - break; - case 5: - var value = /** @type {string} */ (reader.readString()); - msg.setState(value); - break; - case 6: - var value = /** @type {string} */ (reader.readString()); - msg.setClassname(value); - break; - case 7: - var value = new proto.o2control.TaskDeploymentInfo; - reader.readMessage(value,proto.o2control.TaskDeploymentInfo.deserializeBinaryFromReader); - msg.setDeploymentinfo(value); - break; - case 8: - var value = /** @type {string} */ (reader.readString()); - msg.setPid(value); - break; - case 9: - var value = /** @type {string} */ (reader.readString()); - msg.setSandboxstdout(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.o2control.ShortTaskInfo.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.o2control.ShortTaskInfo.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.o2control.ShortTaskInfo} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.o2control.ShortTaskInfo.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getName(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } - f = message.getLocked(); - if (f) { - writer.writeBool( - 2, - f - ); - } - f = message.getTaskid(); - if (f.length > 0) { - writer.writeString( - 3, - f - ); - } - f = message.getStatus(); - if (f.length > 0) { - writer.writeString( - 4, - f - ); - } - f = message.getState(); - if (f.length > 0) { - writer.writeString( - 5, - f - ); - } - f = message.getClassname(); - if (f.length > 0) { - writer.writeString( - 6, - f - ); - } - f = message.getDeploymentinfo(); - if (f != null) { - writer.writeMessage( - 7, - f, - proto.o2control.TaskDeploymentInfo.serializeBinaryToWriter - ); - } - f = message.getPid(); - if (f.length > 0) { - writer.writeString( - 8, - f - ); - } - f = message.getSandboxstdout(); - if (f.length > 0) { - writer.writeString( - 9, - f - ); - } -}; - - -/** - * optional string name = 1; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getName = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setName = function(value) { - jspb.Message.setProto3StringField(this, 1, value); -}; - - -/** - * optional bool locked = 2; - * Note that Boolean fields may be set to 0/1 when serialized from a Java server. - * You should avoid comparisons like {@code val === true/false} in those cases. - * @return {boolean} - */ -proto.o2control.ShortTaskInfo.prototype.getLocked = function() { - return /** @type {boolean} */ (jspb.Message.getFieldWithDefault(this, 2, false)); -}; - - -/** @param {boolean} value */ -proto.o2control.ShortTaskInfo.prototype.setLocked = function(value) { - jspb.Message.setProto3BooleanField(this, 2, value); -}; - - -/** - * optional string taskId = 3; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getTaskid = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setTaskid = function(value) { - jspb.Message.setProto3StringField(this, 3, value); -}; - - -/** - * optional string status = 4; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getStatus = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setStatus = function(value) { - jspb.Message.setProto3StringField(this, 4, value); -}; - - -/** - * optional string state = 5; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getState = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setState = function(value) { - jspb.Message.setProto3StringField(this, 5, value); -}; - - -/** - * optional string className = 6; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getClassname = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setClassname = function(value) { - jspb.Message.setProto3StringField(this, 6, value); -}; - - -/** - * optional TaskDeploymentInfo deploymentInfo = 7; - * @return {?proto.o2control.TaskDeploymentInfo} - */ -proto.o2control.ShortTaskInfo.prototype.getDeploymentinfo = function() { - return /** @type{?proto.o2control.TaskDeploymentInfo} */ ( - jspb.Message.getWrapperField(this, proto.o2control.TaskDeploymentInfo, 7)); -}; - - -/** @param {?proto.o2control.TaskDeploymentInfo|undefined} value */ -proto.o2control.ShortTaskInfo.prototype.setDeploymentinfo = function(value) { - jspb.Message.setWrapperField(this, 7, value); -}; - - -proto.o2control.ShortTaskInfo.prototype.clearDeploymentinfo = function() { - this.setDeploymentinfo(undefined); -}; - - -/** - * Returns whether this field is set. - * @return {!boolean} - */ -proto.o2control.ShortTaskInfo.prototype.hasDeploymentinfo = function() { - return jspb.Message.getField(this, 7) != null; -}; - - -/** - * optional string pid = 8; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getPid = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setPid = function(value) { - jspb.Message.setProto3StringField(this, 8, value); -}; - - -/** - * optional string sandboxStdout = 9; - * @return {string} - */ -proto.o2control.ShortTaskInfo.prototype.getSandboxstdout = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 9, "")); -}; - - -/** @param {string} value */ -proto.o2control.ShortTaskInfo.prototype.setSandboxstdout = function(value) { - jspb.Message.setProto3StringField(this, 9, value); -}; - - - -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.o2control.TaskDeploymentInfo = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); -}; -goog.inherits(proto.o2control.TaskDeploymentInfo, jspb.Message); -if (goog.DEBUG && !COMPILED) { - proto.o2control.TaskDeploymentInfo.displayName = 'proto.o2control.TaskDeploymentInfo'; -} - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto suitable for use in Soy templates. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. - * @param {boolean=} opt_includeInstance Whether to include the JSPB instance - * for transitional soy proto support: http://goto/soy-param-migration - * @return {!Object} - */ -proto.o2control.TaskDeploymentInfo.prototype.toObject = function(opt_includeInstance) { - return proto.o2control.TaskDeploymentInfo.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Whether to include the JSPB - * instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.o2control.TaskDeploymentInfo} msg The msg instance to transform. - * @return {!Object} - */ -proto.o2control.TaskDeploymentInfo.toObject = function(includeInstance, msg) { - var obj = { - hostname: jspb.Message.getFieldWithDefault(msg, 1, ""), - agentid: jspb.Message.getFieldWithDefault(msg, 2, ""), - offerid: jspb.Message.getFieldWithDefault(msg, 3, ""), - executorid: jspb.Message.getFieldWithDefault(msg, 4, "") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.o2control.TaskDeploymentInfo} - */ -proto.o2control.TaskDeploymentInfo.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.o2control.TaskDeploymentInfo; - return proto.o2control.TaskDeploymentInfo.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.o2control.TaskDeploymentInfo} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.o2control.TaskDeploymentInfo} - */ -proto.o2control.TaskDeploymentInfo.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setHostname(value); - break; - case 2: - var value = /** @type {string} */ (reader.readString()); - msg.setAgentid(value); - break; - case 3: - var value = /** @type {string} */ (reader.readString()); - msg.setOfferid(value); - break; - case 4: - var value = /** @type {string} */ (reader.readString()); - msg.setExecutorid(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.o2control.TaskDeploymentInfo.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.o2control.TaskDeploymentInfo.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.o2control.TaskDeploymentInfo} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.o2control.TaskDeploymentInfo.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getHostname(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } - f = message.getAgentid(); - if (f.length > 0) { - writer.writeString( - 2, - f - ); - } - f = message.getOfferid(); - if (f.length > 0) { - writer.writeString( - 3, - f - ); - } - f = message.getExecutorid(); - if (f.length > 0) { - writer.writeString( - 4, - f - ); - } -}; - - -/** - * optional string hostname = 1; - * @return {string} - */ -proto.o2control.TaskDeploymentInfo.prototype.getHostname = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** @param {string} value */ -proto.o2control.TaskDeploymentInfo.prototype.setHostname = function(value) { - jspb.Message.setProto3StringField(this, 1, value); -}; - - -/** - * optional string agentId = 2; - * @return {string} - */ -proto.o2control.TaskDeploymentInfo.prototype.getAgentid = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); -}; - - -/** @param {string} value */ -proto.o2control.TaskDeploymentInfo.prototype.setAgentid = function(value) { - jspb.Message.setProto3StringField(this, 2, value); -}; - - -/** - * optional string offerId = 3; - * @return {string} - */ -proto.o2control.TaskDeploymentInfo.prototype.getOfferid = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); -}; - - -/** @param {string} value */ -proto.o2control.TaskDeploymentInfo.prototype.setOfferid = function(value) { - jspb.Message.setProto3StringField(this, 3, value); -}; - - -/** - * optional string executorId = 4; - * @return {string} - */ -proto.o2control.TaskDeploymentInfo.prototype.getExecutorid = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); -}; - - -/** @param {string} value */ -proto.o2control.TaskDeploymentInfo.prototype.setExecutorid = function(value) { - jspb.Message.setProto3StringField(this, 4, value); -}; - - -goog.object.extend(exports, proto.o2control); diff --git a/Control/protobuf/status_pb.js b/Control/protobuf/status_pb.js deleted file mode 100644 index 61153ccc4..000000000 --- a/Control/protobuf/status_pb.js +++ /dev/null @@ -1,237 +0,0 @@ -/** - * @fileoverview - * @enhanceable - * @suppress {messageConventions} JS Compiler reports an error if a variable or - * field starts with 'MSG_' and isn't a translatable message. - * @public - */ -// GENERATED CODE -- DO NOT EDIT! - -var jspb = require('google-protobuf'); -var goog = jspb; -var global = Function('return this')(); - -var google_protobuf_any_pb = require('google-protobuf/google/protobuf/any_pb.js'); -goog.exportSymbol('proto.google.rpc.Status', null, global); - -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.google.rpc.Status = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, proto.google.rpc.Status.repeatedFields_, null); -}; -goog.inherits(proto.google.rpc.Status, jspb.Message); -if (goog.DEBUG && !COMPILED) { - proto.google.rpc.Status.displayName = 'proto.google.rpc.Status'; -} -/** - * List of repeated fields within this message type. - * @private {!Array} - * @const - */ -proto.google.rpc.Status.repeatedFields_ = [3]; - - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto suitable for use in Soy templates. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. - * @param {boolean=} opt_includeInstance Whether to include the JSPB instance - * for transitional soy proto support: http://goto/soy-param-migration - * @return {!Object} - */ -proto.google.rpc.Status.prototype.toObject = function(opt_includeInstance) { - return proto.google.rpc.Status.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Whether to include the JSPB - * instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.google.rpc.Status} msg The msg instance to transform. - * @return {!Object} - */ -proto.google.rpc.Status.toObject = function(includeInstance, msg) { - var obj = { - code: jspb.Message.getFieldWithDefault(msg, 1, 0), - message: jspb.Message.getFieldWithDefault(msg, 2, ""), - detailsList: jspb.Message.toObjectList(msg.getDetailsList(), - google_protobuf_any_pb.Any.toObject, includeInstance) - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.google.rpc.Status} - */ -proto.google.rpc.Status.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.google.rpc.Status; - return proto.google.rpc.Status.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.google.rpc.Status} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.google.rpc.Status} - */ -proto.google.rpc.Status.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {number} */ (reader.readInt32()); - msg.setCode(value); - break; - case 2: - var value = /** @type {string} */ (reader.readString()); - msg.setMessage(value); - break; - case 3: - var value = new google_protobuf_any_pb.Any; - reader.readMessage(value,google_protobuf_any_pb.Any.deserializeBinaryFromReader); - msg.addDetails(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.google.rpc.Status.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.google.rpc.Status.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.google.rpc.Status} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.google.rpc.Status.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getCode(); - if (f !== 0) { - writer.writeInt32( - 1, - f - ); - } - f = message.getMessage(); - if (f.length > 0) { - writer.writeString( - 2, - f - ); - } - f = message.getDetailsList(); - if (f.length > 0) { - writer.writeRepeatedMessage( - 3, - f, - google_protobuf_any_pb.Any.serializeBinaryToWriter - ); - } -}; - - -/** - * optional int32 code = 1; - * @return {number} - */ -proto.google.rpc.Status.prototype.getCode = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); -}; - - -/** @param {number} value */ -proto.google.rpc.Status.prototype.setCode = function(value) { - jspb.Message.setProto3IntField(this, 1, value); -}; - - -/** - * optional string message = 2; - * @return {string} - */ -proto.google.rpc.Status.prototype.getMessage = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); -}; - - -/** @param {string} value */ -proto.google.rpc.Status.prototype.setMessage = function(value) { - jspb.Message.setProto3StringField(this, 2, value); -}; - - -/** - * repeated google.protobuf.Any details = 3; - * @return {!Array.} - */ -proto.google.rpc.Status.prototype.getDetailsList = function() { - return /** @type{!Array.} */ ( - jspb.Message.getRepeatedWrapperField(this, google_protobuf_any_pb.Any, 3)); -}; - - -/** @param {!Array.} value */ -proto.google.rpc.Status.prototype.setDetailsList = function(value) { - jspb.Message.setRepeatedWrapperField(this, 3, value); -}; - - -/** - * @param {!proto.google.protobuf.Any=} opt_value - * @param {number=} opt_index - * @return {!proto.google.protobuf.Any} - */ -proto.google.rpc.Status.prototype.addDetails = function(opt_value, opt_index) { - return jspb.Message.addToRepeatedWrapperField(this, 3, opt_value, proto.google.protobuf.Any, opt_index); -}; - - -proto.google.rpc.Status.prototype.clearDetailsList = function() { - this.setDetailsList([]); -}; - - -goog.object.extend(exports, proto.google.rpc); From aa097f4d784fe92c1e20755d820862f12746d23b Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 13 Aug 2025 12:59:19 +0200 Subject: [PATCH 02/38] Use 'useVars' instead of 'vars' for DeploymentRequest object --- .../pages/EnvironmentCreation/EnvironmentCreation.model.js | 4 ++-- Control/public/workflow/Workflow.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Control/public/pages/EnvironmentCreation/EnvironmentCreation.model.js b/Control/public/pages/EnvironmentCreation/EnvironmentCreation.model.js index a37adb72d..850c63399 100644 --- a/Control/public/pages/EnvironmentCreation/EnvironmentCreation.model.js +++ b/Control/public/pages/EnvironmentCreation/EnvironmentCreation.model.js @@ -93,7 +93,7 @@ export class EnvironmentCreationModel extends Observable { this._model.environment.newEnvironment({ workflowTemplate: path, selectedConfiguration: this._selectedConfigurationLabel, - vars: this._creationModel.variables, + userVars: this._creationModel.variables, detectors: this._model.workflow.flpSelection.selectedDetectors }); } @@ -197,7 +197,7 @@ export class EnvironmentCreationModel extends Observable { } /** - * Check if all selected detectors are PFR ready. + * Check if all selected detectors are PFR ready. * @return {Boolean} */ isPfrAvailable() { diff --git a/Control/public/workflow/Workflow.js b/Control/public/workflow/Workflow.js index c79429233..7e06215ba 100644 --- a/Control/public/workflow/Workflow.js +++ b/Control/public/workflow/Workflow.js @@ -263,7 +263,7 @@ export default class Workflow extends Observable { // Combine Readout URI if it was used this.model.environment.newEnvironment({ workflowTemplate: path, - vars: variables, + userVars: variables, detectors: this.flpSelection.selectedDetectors }); } From e0a00990f0a08f5fffae4057b3d93be23cd0a749 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 13 Aug 2025 12:59:35 +0200 Subject: [PATCH 03/38] Redirect user to env-details page if request is successful --- Control/public/environment/Environment.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Control/public/environment/Environment.js b/Control/public/environment/Environment.js index de4a55b56..19df48aa9 100644 --- a/Control/public/environment/Environment.js +++ b/Control/public/environment/Environment.js @@ -130,16 +130,22 @@ export default class Environment extends Observable { /** * Create a new remote environment, creation action result into `itemNew` as RemoteData - * See protobuf definition for properties of `itemForm` as body - * @param {string} itemForm + * If request is successful, the user will be redirected to the active environments page + * @param {DeploymentRequest} deploymentRequest - deployment request object containing all necessary information to deploy a new environment + * @return {void} */ - async newEnvironment(itemForm) { + async newEnvironment(deploymentRequest) { this.itemNew = RemoteData.loading(); this.notify(); - const {result, ok} = await this.model.loader.post(`/api/core/request`, itemForm); - this.itemNew = !ok ? RemoteData.failure(result.message) : RemoteData.notAsked(); - this.model.router.go(`?page=environments`); + const { result, ok } = await this.model.loader.post(`/api/deploy`, deploymentRequest); + if (!ok) { + this.itemNew = RemoteData.failure(result.message); + this.notify(); + } else { + this.itemNew = RemoteData.notAsked(); + this.model.router.go(`?page=environment&id=${id}`); + } } /** From 3bf374e5f05fd7eece7589082909b11bdee0c54c Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 14 Aug 2025 17:16:38 +0200 Subject: [PATCH 04/38] Update types and docs --- Control/lib/common/environmentState.enum.js | 27 +++++++++++++++++++++ Control/lib/typedefs/EnvironmentEvent.js | 3 ++- Control/lib/typedefs/EnvironmentInfo.js | 4 ++- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 Control/lib/common/environmentState.enum.js diff --git a/Control/lib/common/environmentState.enum.js b/Control/lib/common/environmentState.enum.js new file mode 100644 index 000000000..cc44c1d34 --- /dev/null +++ b/Control/lib/common/environmentState.enum.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +/** + * Available environment transitions of ECS + */ +const EnvironmentState = Object.freeze({ + STANDBY: 'STANDBY', + DEPLOYED: 'DEPLOYED', + CONFIGURED: 'CONFIGURED', + RUNNING: 'RUNNING', + ERROR: 'ERROR', + UNKNOWN: 'UNKNOWN' +}); + +exports.EnvironmentState = EnvironmentState; diff --git a/Control/lib/typedefs/EnvironmentEvent.js b/Control/lib/typedefs/EnvironmentEvent.js index 187e36935..2903b44ae 100644 --- a/Control/lib/typedefs/EnvironmentEvent.js +++ b/Control/lib/typedefs/EnvironmentEvent.js @@ -18,10 +18,11 @@ * The parsing is done based on the object received from ECS in `events.proto` definition * * @property {string} id + * @property {EnvironmentState} state - STANDBY, DEPLOYED, CONFIGURED, RUNNING, ERROR, UNKNOWN * @property {Error} error - any error that occurred during the transition * @property {String} message - any additional message concerning the current state or transition * @property {object} transition - DEPLOY, CONFIGURE, RESET, START_ACTIVITY, STOP_ACTIVITY, EXIT, GO_ERROR, RECOVER AS PER https://github.com/AliceO2Group/Control/blob/master/core/environment/environment.go#L143 * @property {string} transition.name - the name of the transition as defined in ControlWorkflows * @property {string} transition.step - the current step of the transition as defined in ControlWorkflows - * @property {string} transition.status - the status of the transition as defined in ControlWorkflows + * @property {string} transition.status - the status of the transition: NULL,STARTED,ONGOING,DONE_OK,DONE_ERROR,DONE_TIMEOUT */ diff --git a/Control/lib/typedefs/EnvironmentInfo.js b/Control/lib/typedefs/EnvironmentInfo.js index 9a40518df..ee8d005f7 100644 --- a/Control/lib/typedefs/EnvironmentInfo.js +++ b/Control/lib/typedefs/EnvironmentInfo.js @@ -21,8 +21,10 @@ * @property {Number} currentRunNumber * @property {Number} createdWhen - timestamp in ms * @property {TaskEvent} firstTaskInError - the first task in error, if any - * @property {String} state - STANDBY, DEPLOYED, CONFIGURED, RUNNING, ERROR, MIXED, SHUTDOWN + * @property {EnvironmentState} state - STANDBY, DEPLOYED, CONFIGURED, RUNNING, ERROR, MIXED, SHUTDOWN * @property {String} currentTransition - DEPLOY, CONFIGURE, RESET, START_ACTIVITY, STOP_ACTIVITY, EXIT, GO_ERROR, RECOVER AS PER https://github.com/AliceO2Group/Control/blob/master/core/environment/environment.go#L143 + * @property {Boolean} isDeploying - if true, the environment is being deployed (custom ECS GUI property only). Once ECS reports the environment in state CONFIGURED with no currentTransition, this property will be set to false + * @property {String} deploymentError - if the deployment failed, this will contain the error message * @property {String} rootRole - workflow selected by user * @property {String} description * @property {Number} numberOfFlps From 7b653dc13142a815edd01b4ccb51763a1705b049 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 14 Aug 2025 17:31:31 +0200 Subject: [PATCH 05/38] Update adapter of environment event --- Control/lib/common/environmentState.enum.js | 3 ++- Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js | 3 ++- Control/public/common/enums/EnvironmentState.enum.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Control/lib/common/environmentState.enum.js b/Control/lib/common/environmentState.enum.js index cc44c1d34..d507a5536 100644 --- a/Control/lib/common/environmentState.enum.js +++ b/Control/lib/common/environmentState.enum.js @@ -13,7 +13,8 @@ */ /** - * Available environment transitions of ECS + * Available environment transitions of ECS + * @link https://github.com/AliceO2Group/Control/blob/master/core/environment/environment.go#L153-L160 */ const EnvironmentState = Object.freeze({ STANDBY: 'STANDBY', diff --git a/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js b/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js index bfc9ac63e..66937c090 100644 --- a/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js +++ b/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js @@ -20,10 +20,11 @@ exports.fromEcsEventToEnvironmentEvent = ({ environmentEvent }) => { const { environmentId: id, - error, message, transition, transitionStep, transitionStatus, + state, error, message, transition, transitionStep, transitionStatus, } = environmentEvent; return { id, + state, error, message, transition: { diff --git a/Control/public/common/enums/EnvironmentState.enum.js b/Control/public/common/enums/EnvironmentState.enum.js index 7432e7351..e4cd98e3c 100644 --- a/Control/public/common/enums/EnvironmentState.enum.js +++ b/Control/public/common/enums/EnvironmentState.enum.js @@ -13,7 +13,7 @@ /** * States of an environment - * @link https://github.com/AliceO2Group/Control/blob/c5fdca8e25f6f724231d15e34bb1bde7b2c267ab/core/integration/dcs/protos/dcs.proto#L251 + * @link https://github.com/AliceO2Group/Control/blob/master/core/environment/environment.go#L153-L160 * @return {Object} */ export const EnvironmentState = Object.freeze({ From e6330d893cd04b60ed864ecfdc37bc65f6654aab Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 14 Aug 2025 17:31:49 +0200 Subject: [PATCH 06/38] Update service to retrieve also failed deployments --- Control/lib/services/Environment.service.js | 25 +++++++---- .../mocha-environment.service.test.js | 45 ++++++++++++++++++- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/Control/lib/services/Environment.service.js b/Control/lib/services/Environment.service.js index e0f5f3579..0be3e2aa5 100644 --- a/Control/lib/services/Environment.service.js +++ b/Control/lib/services/Environment.service.js @@ -75,13 +75,12 @@ class EnvironmentService { this._broadcastService.broadcast(ENVIRONMENTS_OVERVIEW, []); return []; } - const environmentList = []; + const activeEnvironmentList = []; const cachedEnvironmentIds = [...this._environmentCacheService.environments.keys()]; for (const { id } of environments) { let environment; try { // Retrieving environments one by one is needed so that ODC devices tasks info is part of the payload - // Issue reported: OCTRL-1012 environment = await this.getEnvironment(id, '', false); } catch (error) { this._logger.errorMessage(`Failed to retrieve environment ${id}: ${error}`); @@ -90,18 +89,28 @@ class EnvironmentService { if (shouldUpdateCache) { this._environmentCacheService.addOrUpdateEnvironment(environment, false); } - environmentList.push(environment); + activeEnvironmentList.push(environment); } } - // Remove environments from cache that are not in the retrieved list + // Remove environments from cache that are not in the retrieved list and that are not in deploying state + // Environments that are `isDeploying` should not be removed. If deployment failed, ECS will delete it + // but we need to keep it until user acknowledges the failure + // and removes it from the cache manually for (const cachedEnvironmentId of cachedEnvironmentIds) { - if (!environmentList.some(env => env.id === cachedEnvironmentId)) { - this._environmentCacheService.environments.delete(cachedEnvironmentId); + if (!activeEnvironmentList.some(env => env.id === cachedEnvironmentId)) { + const environmentPotentiallyToRemove = this._environmentCacheService.environments.get(cachedEnvironmentId); + if (environmentPotentiallyToRemove.isDeploying || environmentPotentiallyToRemove.deploymentError) { + // If the environment is deploying or has a deployment error, we still consider it active + // and we do not remove it from the cache + activeEnvironmentList.push(environmentPotentiallyToRemove); + } else { + this._environmentCacheService.environments.delete(cachedEnvironmentId); + } } } this._broadcastService.broadcast(ENVIRONMENTS_OVERVIEW, [...this._environmentCacheService.environments.values()]); - return environmentList; + return activeEnvironmentList; } catch (error) { this._logger.errorMessage(error); } @@ -207,7 +216,7 @@ class EnvironmentService { * @type {EnvironmentInfo} * @property {string} currentTransition - the current transition of the environment */ - environmentInfo.currentTransition = environmentInfo.currentTransition || 'DEPLOY'; + environmentInfo.isDeploying = true; this._environmentCacheService.addOrUpdateEnvironment(environmentInfo, true); return environmentInfo; } diff --git a/Control/test/lib/services/mocha-environment.service.test.js b/Control/test/lib/services/mocha-environment.service.test.js index 563f21674..6fd621d7d 100644 --- a/Control/test/lib/services/mocha-environment.service.test.js +++ b/Control/test/lib/services/mocha-environment.service.test.js @@ -142,6 +142,49 @@ describe('EnvironmentService test suite', () => { assert.strictEqual(result[1].id, 'env2'); assert.ok(envService._environmentCacheService.addOrUpdateEnvironment.calledTwice); }); + it('should retrieve environments and update the cache and keep environment that are isDeploying true', async () => { + const mockEnvironments = [ + { id: 'env1', state: 'active' }, + { id: 'env2', state: 'inactive' }, + ]; + GetEnvironmentsStub.resolves({ environments: mockEnvironments }); + GetEnvironmentStub.withArgs({ id: mockEnvironments[0].id }).resolves({ environment: mockEnvironments[0] }); + GetEnvironmentStub.withArgs({ id: mockEnvironments[1].id }).resolves({ environment: mockEnvironments[1] }); + envService._environmentCacheService.environments = new Map([ + ['env3', { id: 'env3', isDeploying: true, events: [] }], + ['env2', { id: 'env2', isDeploying: false, events: [] }], + ]); + envService._environmentCacheService.addOrUpdateEnvironment = sinon.stub().returns(); + const result = await envService.getEnvironments(false, true); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].id, 'env1'); + assert.strictEqual(result[1].id, 'env2'); + assert.strictEqual(result[2].id, 'env3'); + assert.ok(envService._environmentCacheService.addOrUpdateEnvironment.calledTwice); + }); + + it('should retrieve environments and update the cache and keep environment that failed deployment', async () => { + const mockEnvironments = [ + { id: 'env1', state: 'active' }, + { id: 'env2', state: 'inactive' }, + ]; + GetEnvironmentsStub.resolves({ environments: mockEnvironments }); + GetEnvironmentStub.withArgs({ id: mockEnvironments[0].id }).resolves({ environment: mockEnvironments[0] }); + GetEnvironmentStub.withArgs({ id: mockEnvironments[1].id }).resolves({ environment: mockEnvironments[1] }); + envService._environmentCacheService.environments = new Map([ + ['env4', { id: 'env4', isDeploying: false, deploymentError: 'ERROR', events: [] }], + ['env2', { id: 'env2', isDeploying: false, events: [] }], + ]); + envService._environmentCacheService.addOrUpdateEnvironment = sinon.stub().returns(); + const result = await envService.getEnvironments(false, true); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].id, 'env1'); + assert.strictEqual(result[1].id, 'env2'); + assert.strictEqual(result[2].id, 'env4'); + assert.ok(envService._environmentCacheService.addOrUpdateEnvironment.calledTwice); + }); }); describe(`'getEnvironment' test suite`, async () => { @@ -198,7 +241,7 @@ describe('EnvironmentService test suite', () => { it('should successfully return environment id if successfully created', async () => { const environmentTransitioned = await envService.newEnvironmentAsync({ workflowTemplate: 'github/template/1.1.0', userVars: {keyA: 'keyA'}, user }); assert.strictEqual(environmentTransitioned.id, ENVIRONMENT_VALID); - assert.strictEqual(environmentTransitioned.currentTransition, 'DEPLOY'); + assert.ok(environmentTransitioned.isDeploying); }); it('should add environment to cache when successfully deployed', async () => { From 2dce08a2fa51027108cf2567b45022d0e8b4cc86 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 14 Aug 2025 17:32:16 +0200 Subject: [PATCH 07/38] Add new property for environments during deployment --- .../lib/adapters/EnvironmentInfoAdapter.js | 4 ++++ .../environment/EnvironmentCache.service.js | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Control/lib/adapters/EnvironmentInfoAdapter.js b/Control/lib/adapters/EnvironmentInfoAdapter.js index df047ebbd..814158fa0 100644 --- a/Control/lib/adapters/EnvironmentInfoAdapter.js +++ b/Control/lib/adapters/EnvironmentInfoAdapter.js @@ -49,6 +49,8 @@ class EnvironmentInfoAdapter { const { id, currentRunNumber, + isDeploying, + deploymentError, createdWhen, firstTaskInError = null, state = '', @@ -69,6 +71,8 @@ class EnvironmentInfoAdapter { const environmentInfo = { id, currentRunNumber, + isDeploying, + deploymentError, createdWhen, firstTaskInError, state, diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index ebb83a000..6f5dbb1a4 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -21,9 +21,12 @@ const { ENVIRONMENTS_TRACK, INTEGRATED_SERVICES_TRACK, TASKS_TRACK } } = require('./../../common/emitterKeys.enum.js'); +const { EnvironmentState } = require('../../common/environmentState.enum.js'); const { TaskState } = require('../../common/taskState.enum.js'); const EPN_PATH_IN_ENVIRONMENT_INFO = 'hardware.epn.info'; +const ECS_TRANSITION_DONE_MESSAGE = 'transition completed successfully'; + /** * @class * EnvironmentCacheService class is designed to store in-memory information and allow users to also broadcast new information to the all or registered clients. @@ -118,12 +121,28 @@ class EnvironmentCacheService { * @param {EnvironmentEvent} environmentEvent - the event object containing the payload and environmentId */ this._eventEmitter.on(ENVIRONMENTS_TRACK, (environmentEvent) => { - const { id } = environmentEvent; + const { id, state, message, error } = environmentEvent; const cachedEnvironment = this._environments.has(id) ? this._environments.get(id) : { id, events: [] }; + if (cachedEnvironment.isDeploying && error) { + // If the environment is deploying and there is an error, environment will not be active in ECS anymore but + // we still want to keep the information in the cache until a user acknowledges the error + cachedEnvironment.isDeploying = false; + cachedEnvironment.deploymentError = error; + } + if ( + state === EnvironmentState.CONFIGURED && + message === ECS_TRANSITION_DONE_MESSAGE + // OCTRL-1038 - currently comparing to hardcoded string, but this should be replaced with transition status + ) { + // Once the environment is configured and ongoing transition is done, we can set the isDeploying to false + // This can happen when the environment also goes form RUNNING to CONFIGURED but it is already marked as not deploying anymore + cachedEnvironment.isDeploying = false; + } + cachedEnvironment.events.push(environmentEvent); cachedEnvironment.lastUpdate = environmentEvent.timestamp; this._environments.set(id, cachedEnvironment); From a3358e8071b762431cfa10a8f2e252730e42ac2f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 14 Aug 2025 17:38:21 +0200 Subject: [PATCH 08/38] Update front-end tests for new redirects --- Control/public/environment/Environment.js | 1 + Control/test/config/core-grpc.js | 4 ++++ Control/test/public/page-new-environment-mocha.js | 13 +++---------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Control/public/environment/Environment.js b/Control/public/environment/Environment.js index 19df48aa9..70ac012e7 100644 --- a/Control/public/environment/Environment.js +++ b/Control/public/environment/Environment.js @@ -143,6 +143,7 @@ export default class Environment extends Observable { this.itemNew = RemoteData.failure(result.message); this.notify(); } else { + const { id } = result; this.itemNew = RemoteData.notAsked(); this.model.router.go(`?page=environment&id=${id}`); } diff --git a/Control/test/config/core-grpc.js b/Control/test/config/core-grpc.js index 9648e54cd..42c4e4078 100644 --- a/Control/test/config/core-grpc.js +++ b/Control/test/config/core-grpc.js @@ -78,6 +78,10 @@ const coreGRPCServer = (config) => { callback(null, {environment: envTest.environment}); } }, + async newEnvironmentAsync(call, callback) { + calls['newEnvironmentAsync'] = true; + callback(null, {environment: envTest.environment}); + }, getWorkflowTemplates(call, callback) { calls['getWorkflowTemplates'] = true; callback(null, envTest.workflowTemplates); diff --git a/Control/test/public/page-new-environment-mocha.js b/Control/test/public/page-new-environment-mocha.js index 0e95b7e51..b14ff8a52 100644 --- a/Control/test/public/page-new-environment-mocha.js +++ b/Control/test/public/page-new-environment-mocha.js @@ -430,7 +430,7 @@ describe('`pageNewEnvironment` test-suite', async () => { assert.strictEqual(message, 'Configuration saved successfully as My_Config'); }); - it('should successfully create a new environment', async () => { + it('should successfully create a new environment and redirect user to its environment details page', async () => { await page.locator('#deploy-env') .setTimeout(1000) .click(); @@ -438,15 +438,8 @@ describe('`pageNewEnvironment` test-suite', async () => { waitUntil: 'networkidle0', }); const location = await page.evaluate(() => window.location); - assert.strictEqual(location.search, '?page=environments'); - }); - - it('should display successful environment request', async () => { - await page.waitForSelector('tr.primary > th:nth-child(1)'); - const detector = await page.evaluate(() => document.querySelector('table.table:nth-child(4) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(2)').innerText); - const state = await page.evaluate(() => document.querySelector('table.table:nth-child(4) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(6)').innerText); - assert.strictEqual(detector, 'MID'); - assert.strictEqual(state, 'ONGOING'); + assert.strictEqual(location.search, '?page=environment&id=6f6d6387-6577-11e8-993a-f07959157220'); + assert.ok(calls['newEnvironmentAsync']); }); /** From f69356b44548176c1c041c2aa044950cbaf50e7d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 14 Aug 2025 17:43:05 +0200 Subject: [PATCH 09/38] Fix test on front-end --- Control/test/public/page-new-environment-mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Control/test/public/page-new-environment-mocha.js b/Control/test/public/page-new-environment-mocha.js index b14ff8a52..776f79b1c 100644 --- a/Control/test/public/page-new-environment-mocha.js +++ b/Control/test/public/page-new-environment-mocha.js @@ -438,7 +438,7 @@ describe('`pageNewEnvironment` test-suite', async () => { waitUntil: 'networkidle0', }); const location = await page.evaluate(() => window.location); - assert.strictEqual(location.search, '?page=environment&id=6f6d6387-6577-11e8-993a-f07959157220'); + assert.strictEqual(location.search, '?page=environment&id=6f6d6387-6577-11e8-993a-f07959157220&panel=general'); assert.ok(calls['newEnvironmentAsync']); }); From 4c7f73ca6642e4130035f3674c6e14581bac98f6 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 11:00:52 +0200 Subject: [PATCH 10/38] Add logging for deployment request from user --- Control/lib/controllers/Deployment.controller.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index 6c167e6b5..04ffb40b9 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -56,7 +56,7 @@ class DeploymentController { /** * @type {DeploymentRequest} */ - const { workflowTemplate, selectedConfiguration, userVars } = req.body; + const { workflowTemplate, selectedConfiguration, userVars, detectors } = req.body; if (!workflowTemplate && !selectedConfiguration) { updateAndSendExpressResponseFromNativeError( @@ -69,6 +69,14 @@ class DeploymentController { const { personid, name, username } = req.session || {}; const user = new User(username, name, personid); + const logMessage = 'New deployment request by ' + + `user ${user.username} with ` + + `workflow template ${workflowTemplate} ` + + `and detectors ${detectors}`; + this._logger.infoMessage(logMessage, { + level: LogLevel.OPERATIONS, + }); + try { const environment = await this._deploymentService.deployEnvironment({ userVars, From 3d2c07af619dd4b2751f8d1b47d3c935513c5de4 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 11:11:57 +0200 Subject: [PATCH 11/38] Remove previous ReqHandler --- Control/lib/api.js | 14 +- Control/lib/control-core/RequestHandler.js | 217 --------------------- Control/test/lib/mocha-request-handler.js | 96 --------- 3 files changed, 6 insertions(+), 321 deletions(-) delete mode 100644 Control/lib/control-core/RequestHandler.js delete mode 100644 Control/test/lib/mocha-request-handler.js diff --git a/Control/lib/api.js b/Control/lib/api.js index 68580101d..028ca29a3 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -115,7 +115,7 @@ module.exports.setup = (http, ws) => { ctrlProxy, apricotService, cacheService, broadcastService, environmentCacheService ); const workflowService = new WorkflowTemplateService(ctrlProxy, apricotService); - const deploymentService = new DeploymentService(environmentService, workflowService); + const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService); /** * Controllers are initialized with the services they depend on. @@ -124,10 +124,6 @@ module.exports.setup = (http, ws) => { const workflowController = new WorkflowTemplateController(workflowService); const deploymentController = new DeploymentController(deploymentService); - const aliecsReqHandler = new AliecsRequestHandler(ctrlService, apricotService); - aliecsReqHandler.setWs(ws); - aliecsReqHandler.workflowService = workflowService; - const bkpService = new BookkeepingService(config.bookkeeping ?? {}); const runService = new RunService(bkpService, apricotService, cacheService); runService.retrieveStaticConfigurations(); @@ -175,9 +171,6 @@ module.exports.setup = (http, ws) => { (method) => http.post(`/${method}`, coreMiddleware, (req, res) => ctrlService.executeCommand(req, res)), ); - http.get('/core/requests', coreMiddleware, (req, res) => aliecsReqHandler.getAll(req, res)); - http.post('/core/removeRequest/:id', coreMiddleware, (req, res) => aliecsReqHandler.remove(req, res)); - http.get('/workflow/template/default/source', workflowController.getDefaultTemplateSource.bind(workflowController)); http.get('/workflow/template/mappings', workflowController.getWorkflowMapping.bind(workflowController)); http.get('/workflow/configuration', workflowController.getWorkflowConfiguration.bind(workflowController)); @@ -208,6 +201,11 @@ module.exports.setup = (http, ws) => { deploymentController.newAsyncDeploymentHandler.bind(deploymentController) ); + http.delete('/deploy/:id', + minimumRoleMiddleware(Role.DETECTOR), + deploymentController.acknowledgeDeploymentFailureHandler.bind(deploymentController) + ); + http.post('/core/environments/configuration/save', (req, res) => apricotService.saveCoreEnvConfig(req, res)); http.post('/core/environments/configuration/update', (req, res) => apricotService.updateCoreEnvConfig(req, res)); diff --git a/Control/lib/control-core/RequestHandler.js b/Control/lib/control-core/RequestHandler.js deleted file mode 100644 index 1ca8b034e..000000000 --- a/Control/lib/control-core/RequestHandler.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. -*/ -const {WebSocketMessage, LogManager, LogLevel} = require('@aliceo2/web-ui'); -const {errorLogger} = require('./../utils.js'); -const CoreUtils = require('./CoreUtils.js'); -const { - RUNTIME_COMPONENT: {COG}, - RUNTIME_KEY: {RUN_TYPE_TO_HOST_MAPPING} -} = require('../common/kvStore/runtime.enum.js'); -const { User } = require('../dtos/User.js'); -const LOG_FACILITY = 'cog/controlrequests'; - -/** - * Handles AliECS create env requests - */ -class RequestHandler { - - /** - * @param {object} ctrlService - Handle to Control service - * @param {ApricotService} apricotService - service to use to interact with A.P.R.I.C.O.T - */ - constructor(ctrlService, apricotService) { - this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/controlrequests`); - this.ctrlService = ctrlService; - this._apricotService = apricotService; - this.requestList = {}; - - /** - * @type {WorkflowService} - */ - this._workflowService = undefined; - } - - /** - * Sets WebSocket instance - * @param {object} ws - */ - setWs(ws) { - this.webSocket = ws; - } - - /** - * Add AliECS request list to "cache", remove it from the "cache" once response comes from AliECS - * @param {Request} req - * @param {Response} res - */ - async add(req, res) { - const index = parseInt(Object.keys(this.requestList).pop()) + 1 || 0; - const {personid, name, username} = req.session; - const user = new User(username, name, personid); - let logMessage = `Creating environment by user(${username}) with: `; - if (req.body.workflowTemplate) { - logMessage += `workflow: ${req.body.workflowTemplate}, `; - } - if (req.body.detectors) { - logMessage += `and detectors: ${req.body.detectors}`; - } - - this._logger.infoMessage(logMessage, {level: LogLevel.SUPPORT, system: 'GUI', facility: LOG_FACILITY}); - - this.requestList[index] = { - id: index, - detectors: req.body.detectors, - workflow: req.body.workflowTemplate, - date: new Date(), - owner: name, - personid, - failed: false - }; - res.json({ok: 1}); - this.broadcast(); - - const {selectedConfiguration} = req.body; - if (selectedConfiguration) { - // workaround for reloading configuration before deployment from global page - try { - const {variables} = await this._workflowService.retrieveWorkflowSavedConfiguration(selectedConfiguration); - variables.hosts = req.body.vars.hosts; - - const {epn_enabled, odc_n_epns} = req.body.vars; - if (epn_enabled === 'true') { - variables.odc_n_epns = odc_n_epns; - } - req.body.vars = variables; - } catch (error) { - errorLogger(`Unable to reload configuration due to: ${error}`, LOG_FACILITY); - } - } - const deploymentRequestedAt = Date.now(); - let creationResponse = null; - - let hostsToIgnoreForRunType = []; - try { - const runType = CoreUtils.getRunType(req.body); - const hostsToIgnoreString = await this._apricotService.getRuntimeEntryByComponent(COG, RUN_TYPE_TO_HOST_MAPPING); - const hostsToIgnoreMap = JSON.parse(hostsToIgnoreString); - if (Array.isArray(hostsToIgnoreMap[runType])) { - hostsToIgnoreForRunType = hostsToIgnoreMap[runType]; - } - } catch (error) { - errorLogger(`Unable to identify FLPs to ignore due to: ${error}`, LOG_FACILITY); - } - try { - const payload = CoreUtils.parseEnvironmentCreationPayload(req.body, hostsToIgnoreForRunType); - payload.requestUser = user.toEcsFormat(); - creationResponse = await this.ctrlService.executeCommandNoResponse('NewEnvironment', payload); - delete this.requestList[index]; - } catch (error) { - if (error.envId) { - this._logger.errorMessage(`Creation of environment failed with: ${error.details}.`, { - level: LogLevel.ERROR, system: 'GUI', facility: LOG_FACILITY, partition: error.envId - }); - let logMessage = `Environment was requested by user: ${username} with`; - if (req.body.workflowTemplate) { - logMessage += `workflow: ${req.body.workflowTemplate}, `; - } - if (req.body.detectors) { - logMessage += `and detectors: ${req.body.detectors}.`; - } - this._logger.errorMessage(logMessage, { - level: LogLevel.ERROR, system: 'GUI', facility: LOG_FACILITY, partition: error.envId - }); - } else { - let logMessage = `Creation of environment failed with: ${error}. `; - logMessage += `User: ${username}, `; - if (req.body.workflowTemplate) { - logMessage += `workflow: ${req.body.workflowTemplate}, `; - } - if (req.body.detectors) { - logMessage += `and detectors: ${req.body.detectors}.`; - } - this._logger.errorMessage(logMessage, { - level: LogLevel.ERROR, system: 'GUI', facility: LOG_FACILITY, - }); - } - - this.requestList[index].failed = true; - this.requestList[index].message = error.details; - if (error.envId) { - this.requestList[index].envId = error.envId; - } - } - const id = creationResponse ? creationResponse.environment.id : ''; - this._logger.debug(`NEW_ENVIRONMENT,${id},,${deploymentRequestedAt},${Date.now()}`); - - this.broadcast(); - } - - /** - * Remove request from "cache". - * @param {Request} req - * @param {Response} res - * @returns {Object} - */ - remove(req, res) { - const index = req.params.id; - this._logger.infoMessage(`User ${req.session.username} acknowledged and removed failed request`, { - level: LogLevel.SUPPORT, system: 'GUI', facility: LOG_FACILITY - }); - delete this.requestList[index]; - return this.getAll(req, res); - } - - /** - * Broadcast list of request - */ - broadcast() { - this.webSocket?.broadcast(new WebSocketMessage().setCommand('requests').setPayload( - this._getAll() - )); - } - - /** - * @returns {Object} Returns request as array and current date - */ - _getAll() { - return { - now: new Date(), - requests: Object.values(this.requestList) - } - } - - /** - * Get all the requests from the "cache" - * @param {Request} req - * @param {Response} res - * @returns {Object} - */ - getAll(req, res) { - return res.json(this._getAll()); - } - - /** - * Getters & Setters - */ - - /** - * Setter for updating workflowService to use - * @param {WorkflowService} - service to be used for retrieving workflow configuration - * @return {void} - */ - set workflowService(service) { - this._workflowService = service; - } -} -module.exports = RequestHandler; diff --git a/Control/test/lib/mocha-request-handler.js b/Control/test/lib/mocha-request-handler.js deleted file mode 100644 index 9257b946e..000000000 --- a/Control/test/lib/mocha-request-handler.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. -*/ - -/* eslint-disable max-len */ -/* eslint-disable require-jsdoc */ - -const ControlService = require('./../../lib/control-core/ControlService.js'); -const RequestHandler = require('./../../lib/control-core/RequestHandler.js'); -const sinon = require('sinon'); -const assert = require('assert'); - -describe('Request Handler of Control Service test suite', () => { - describe('Test request store', () => { - const res = { - json: sinon.fake.returns(true), - status: sinon.fake.returns(), - send: sinon.fake.returns(true) - }; - - const req = { - body: { - detectors: 'TPC', - workflowTemplate: 'readout', - vars: { - hosts: '["host1"]', - } - }, - session: { - name: 'test' - }, - params: { - id: 0 - } - }; - - it('Add request to store', async() => { - const ctrlProxy = { - isConnectionReady: true, - NewEnvironment: sinon.stub().rejects() - }; - const ctrl = new ControlService(ctrlProxy); - const handler = new RequestHandler(ctrl); - handler.add(req, res); - const storedRequest = handler.requestList[0]; - assert.strictEqual(Object.keys(handler.requestList).length, 1); - assert.strictEqual(storedRequest.detectors, 'TPC'); - assert.strictEqual(storedRequest.workflow, 'readout'); - }); - - it('Add request and remove it as control promise rejects', async() => { - const ctrlProxy = { - isConnectionReady: true, - NewEnvironment: sinon.stub().rejects() - }; - const ctrl = new ControlService(ctrlProxy); - const handler = new RequestHandler(ctrl); - assert.rejects(async() => await handler.add(req, res)); - }); - - it('Add request and remove it, check empty list', async() => { - const ctrlProxy = { - isConnectionReady: true, - NewEnvironment: sinon.stub().rejects() - }; - const ctrl = new ControlService(ctrlProxy); - const handler = new RequestHandler(ctrl); - await handler.add(req, res); - await handler.remove(req, res); - assert.strictEqual(Object.keys(handler.requestList).length, 0); - }); - - it('Add request to the store', async() => { - const ctrlProxy = { - isConnectionReady: true, - NewEnvironment: sinon.stub().resolves({}) - }; - const ctrl = new ControlService(ctrlProxy); - const handler = new RequestHandler(ctrl); - handler.add(req, res); - assert.strictEqual(Object.keys(handler.requestList).length, 1); - await Promise.resolve(); - assert.strictEqual(Object.keys(handler.requestList).length, 0); - }); - }); -}); From 4d55f0fbf967d42e919e6ae61ebf0037c2e2e4f9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 11:42:33 +0200 Subject: [PATCH 12/38] Add option to remove environment by ID from cache --- .../environment/EnvironmentCache.service.js | 15 +++++++++ .../mocha-environment-cache.service.test.js | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index 6f5dbb1a4..53b2af951 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -58,6 +58,21 @@ class EnvironmentCacheService { return this._environments; } + /** + * Method to remove an environment from the cache by its id + * @param {string} id - the id of the environment to be removed + * @returns {void} + */ + removeEnvironmentById(id, shouldBroadcast = true) { + if (this._environments.has(id)) { + this._environments.delete(id); + if (shouldBroadcast) { + this._broadcastService.broadcast(ENVIRONMENTS_OVERVIEW, [...this._environments.values()]); + } + this._lastUpdate = Date.now(); + } + } + /** * Update an environment in the cache by its id * @param {string} id - the id of the environment to be updated diff --git a/Control/test/lib/services/environment/mocha-environment-cache.service.test.js b/Control/test/lib/services/environment/mocha-environment-cache.service.test.js index d90a92a92..3463bbdb8 100644 --- a/Control/test/lib/services/environment/mocha-environment-cache.service.test.js +++ b/Control/test/lib/services/environment/mocha-environment-cache.service.test.js @@ -113,6 +113,38 @@ describe(`'EnvironmentCacheService' - test suite`, () => { }); }); + describe('`removeEnvironmentById` tests', () => { + it('should remove an environment by id and broadcast if shouldBroadcast is true', () => { + const env = { id: 'env1', state: 'active', deploymentError: 'Error'}; + environmentCacheService.addOrUpdateEnvironment(env); + const beforeUpdate = environmentCacheService._lastUpdate; + environmentCacheService.removeEnvironmentById('env1', true); + assert.strictEqual(environmentCacheService._environments.has('env1'), false); + assert.strictEqual(broadcastServiceMock.broadcast.calledOnce, true); + assert.strictEqual(broadcastServiceMock.broadcast.firstCall.args[0], ENVIRONMENTS_OVERVIEW); + assert.deepStrictEqual(broadcastServiceMock.broadcast.firstCall.args[1], []); + assert.ok(environmentCacheService._lastUpdate >= beforeUpdate); + }); + + it('should remove an environment by id and not broadcast if shouldBroadcast is false', () => { + const env = { id: 'env2', state: 'inactive' }; + environmentCacheService.addOrUpdateEnvironment(env); + broadcastServiceMock.broadcast.resetHistory(); + environmentCacheService.removeEnvironmentById('env2', false); + assert.strictEqual(environmentCacheService._environments.has('env2'), false); + assert.strictEqual(broadcastServiceMock.broadcast.called, false); + }); + + it('should do nothing if id does not exist', () => { + broadcastServiceMock.broadcast.resetHistory(); + const beforeUpdate = environmentCacheService._lastUpdate; + environmentCacheService.removeEnvironmentById('nonexistent', true); + assert.strictEqual(broadcastServiceMock.broadcast.called, false); + assert.strictEqual(environmentCacheService._environments.size, 0); + assert.strictEqual(environmentCacheService._lastUpdate, beforeUpdate); + }); + }); + it('should initialize class with an empty environment cache map', () => { assert.strictEqual(environmentCacheService._environments.size, 0); }); From 03b7e0232ed4a60ebc749619c4d5fe84c6fd4ec6 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 11:59:07 +0200 Subject: [PATCH 13/38] Add function to acknowledge environments in error in cache --- Control/lib/services/Deployment.service.js | 29 +++++++++++-- .../services/mocha-deployment.service.test.js | 41 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Control/lib/services/Deployment.service.js b/Control/lib/services/Deployment.service.js index 597794ae4..a51c81423 100644 --- a/Control/lib/services/Deployment.service.js +++ b/Control/lib/services/Deployment.service.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -const {LogManager} = require('@aliceo2/web-ui'); +const {LogManager, LogLevel, NotFoundError} = require('@aliceo2/web-ui'); const CoreUtils = require('./../control-core/CoreUtils.js'); /** @@ -30,9 +30,10 @@ class DeploymentService { * @param {EnvironmentService} environmentService - to use for creating new environments * @param {WorkflowService} workflowService - to use for retrieving template workflow information */ - constructor(environmentService, workflowService) { + constructor(environmentService, workflowService, environmentCacheService) { this._environmentService = environmentService; this._workflowService = workflowService; + this._environmentCacheService = environmentCacheService; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/deployment-service`); } @@ -61,6 +62,28 @@ class DeploymentService { return environment; } + /** + * Method to acknowledge a deployment failure for a given environment. + * A failed deployment is not considered active anymore by ECS, thus it will only be present in the GUI cache + * @param {string} environmentId - the id of the environment to acknowledge the failure + * @param {User} user - the user that acknowledged the failure + * @returns {Promise} - resolves when the environment is acknowledged + * @throws {Error} - if the environment cannot be acknowledged or does not exist in the cache + */ + acknowledgeEnvironmentDeploymentFailure(environmentId, user) { + if (!this._environmentCacheService.environments.has(environmentId)) { + throw new NotFoundError(`Environment (id: ${environmentId}) not found in cache`); + } + const environment = this._environmentCacheService.environments.get(environmentId); + if (!environment.deploymentError) { + throw new Error(`Environment (id: ${environmentId}) does not have a deployment error to acknowledge`); + } + this._environmentCacheService.removeEnvironmentById(environmentId, true); + this._logger.infoMessage(`Environment (${environmentId}) failed deployment acknowledged by user ${user.username}`, + { level: LogLevel.OPERATIONS } + ); + } + /** * @private * If a saved configuration name is provided, then configuration is build as follows: @@ -85,7 +108,7 @@ class DeploymentService { if (savedConfigurationName && savedConfigurationName.trim() !== '') { const { variables } = await this._workflowService.retrieveWorkflowSavedConfiguration(savedConfigurationName); - const { hosts = [], epn_enabled = 'false', odc_n_epns = '0' } = requestedVars; + const { hosts = [], epn_enabled, odc_n_epns } = requestedVars; variables.hosts = hosts; variables.epn_enabled = epn_enabled; variables.odc_n_epns = odc_n_epns; diff --git a/Control/test/lib/services/mocha-deployment.service.test.js b/Control/test/lib/services/mocha-deployment.service.test.js index f5cf38609..4caadade5 100644 --- a/Control/test/lib/services/mocha-deployment.service.test.js +++ b/Control/test/lib/services/mocha-deployment.service.test.js @@ -14,6 +14,7 @@ const assert = require('assert'); const sinon = require('sinon'); +const {NotFoundError} = require('@aliceo2/web-ui'); const {DeploymentService} = require('./../../../lib/services/Deployment.service.js'); describe(`'DeploymentService' test suite`, () => { @@ -46,6 +47,46 @@ describe(`'DeploymentService' test suite`, () => { }); }); + describe('acknowledgeEnvironmentDeploymentFailure - tests', () => { + let deploymentService; + let environmentCacheServiceMock; + let loggerMock; + const user = { username: 'testuser' }; + const environmentId = 'env1'; + + beforeEach(() => { + environmentCacheServiceMock = { + environments: new Map(), + removeEnvironmentById: sinon.stub(), + }; + loggerMock = { + infoMessage: sinon.stub(), + }; + deploymentService = new DeploymentService({}, {}, environmentCacheServiceMock); + deploymentService._logger = loggerMock; + }); + + it('should acknowledge request of deployment failure and remove environment from cache', () => { + environmentCacheServiceMock.environments.set(environmentId, { deploymentError: true }); + deploymentService.acknowledgeEnvironmentDeploymentFailure(environmentId, user); + sinon.assert.calledWith(environmentCacheServiceMock.removeEnvironmentById, environmentId, true); + sinon.assert.calledWith(loggerMock.infoMessage, sinon.match.string, { level: sinon.match.any }); + }); + + it('should throw NotFoundError if environment does not exist in cache', () => { + assert.throws(() => { + deploymentService.acknowledgeEnvironmentDeploymentFailure('notfound', user); + }, new NotFoundError('Environment (id: notfound) not found in cache')); + }); + + it('should throw Error if environment exists in cache but does not have deploymentError', () => { + environmentCacheServiceMock.environments.set(environmentId, {}); + assert.throws(() => { + deploymentService.acknowledgeEnvironmentDeploymentFailure(environmentId, user); + }, new Error(`Environment (id: ${environmentId}) does not have a deployment error to acknowledge`)); + }); + }); + describe(`'_buildUserVarsBasedOnSavedToIgnore' test suite`, async () => { it('should successfully build userVars based on configuration input', async () => { const deploymentService = new DeploymentService({}, { From e1caf5dc5837e4703bbe284866ec1929ffd2781c Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 12:09:39 +0200 Subject: [PATCH 14/38] Add handler for new endpoint on acknowledge on deployment --- Control/lib/api.js | 1 - .../lib/controllers/Deployment.controller.js | 20 +++++++++++ .../mocha-deployment.controller.js | 35 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Control/lib/api.js b/Control/lib/api.js index 028ca29a3..fc1df0bdd 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -61,7 +61,6 @@ const {NotificationService, ConsulService} = require('@aliceo2/web-ui'); // AliECS Core const { AliEcsSynchronizer } = require('./kafka/AliEcsSynchronizer.js'); -const AliecsRequestHandler = require('./control-core/RequestHandler.js'); const ApricotService = require('./control-core/ApricotService.js'); const ControlService = require('./control-core/ControlService.js'); const GrpcServiceClient = require('./control-core/GrpcServiceClient.js'); diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index 04ffb40b9..a4f13254c 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -90,6 +90,26 @@ class DeploymentController { updateAndSendExpressResponseFromNativeError(res, error); } } + + /** + * API - DELETE endpoint for acknowledging an environment deployment failure + * @param {Request} req - HTTP Request object which expects an `id` as mandatory parameter + * @param {string} req.params.id - the id of the environment to be acknowledged + * @param {Response} res - HTTP Response object with result of the acknowledgement + * @returns {void} + */ + async acknowledgeDeploymentFailureHandler(req, res) { + const { id } = req.params; + const { personid, name, username } = req.session || {}; + const user = new User(username, name, personid); + + if (!id) { + updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing environment ID parameter')); + return; + } + this._envService.acknowledgeEnvironmentDeploymentFailure(id, user); + res.status(204).json({ message: 'Environment deployment failure acknowledged' }); + } } module.exports = { DeploymentController }; diff --git a/Control/test/lib/controllers/mocha-deployment.controller.js b/Control/test/lib/controllers/mocha-deployment.controller.js index b9c259069..08cc96c91 100644 --- a/Control/test/lib/controllers/mocha-deployment.controller.js +++ b/Control/test/lib/controllers/mocha-deployment.controller.js @@ -82,4 +82,39 @@ describe('DeploymentController test suite', function() { title: 'Unknown Error' })); }); + + describe('acknowledgeDeploymentFailureHandler - tests', function() { + let envServiceMock, res; + beforeEach(function() { + envServiceMock = { acknowledgeEnvironmentDeploymentFailure: sinon.stub() }; + deploymentController._envService = envServiceMock; + req.params = { id: 'env1' }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub() + }; + }); + + it('should return status 400 and not found message if id is missing from request params', async function() { + req.params = {}; + await deploymentController.acknowledgeDeploymentFailureHandler({params: {}}, res); + assert.ok(res.status.calledWith(400)); + }); + + it('should call acknowledgeEnvironmentDeploymentFailure with correct arguments', async function() { + await deploymentController.acknowledgeDeploymentFailureHandler({ + params: { id: 'env1' }, + session: { username: 'testuser', name: 'Test User', personid: '123' } + }, res); + + assert.ok(envServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[0], 'env1'); + assert.ok(envServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[1] instanceof User); + }); + + it('should return 204 and acknowledgement message on success', async function() { + await deploymentController.acknowledgeDeploymentFailureHandler(req, res); + assert.ok(res.status.calledWith(204)); + assert.ok(res.json.calledWith({ message: 'Environment deployment failure acknowledged' })); + }); + }); }); From c8d85c071d60e9ee25fb1925eb063907861265dd Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 12:10:28 +0200 Subject: [PATCH 15/38] Remove decomissioned feature tests --- .../test/public/page-environments-mocha.js | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/Control/test/public/page-environments-mocha.js b/Control/test/public/page-environments-mocha.js index 329231ecc..c8ec2468a 100644 --- a/Control/test/public/page-environments-mocha.js +++ b/Control/test/public/page-environments-mocha.js @@ -64,37 +64,6 @@ describe('`pageEnvironments` test-suite', () => { assert.strictEqual(location.search, '?page=environments'); }); }); - - describe('Test new environment request', async () => { - it('create failed environment request', async () => { - await page.goto(url + '?page=newEnvironmentAdvanced'); - const location = await page.evaluate(() => window.location); - assert.ok(location.search === '?page=newEnvironmentAdvanced'); - - // select workflow from list of templates - await page.waitForSelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div:nth-child(2) > div > div > div > div:nth-child(2) > div:nth-child(3) > div > div > a'); - await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div:nth-child(2) > div > div > div > div:nth-child(2) > div:nth-child(3) > div > div > a').click()); - - // first detector is already locked (should be MID) - // select earlier locked detector which will automatically select all available hosts - await page.locator('.m1 > div:nth-child(1) > div > a:nth-child(2)') - .setTimeout(500) - .click(); - - await page.locator('#deploy-env') - .setTimeout(500) - .click(); - }); - - it('verify request fields', async () => { - - await waitForEnvRequest(page, 100); - const detector = await page.evaluate(() => document.querySelector('body > div.flex-column.absolute-fill > div.flex-grow.flex-row > div.flex-grow.relative > div > table > tbody > tr > td:nth-child(2)').innerText); - const state = await page.evaluate(() => document.querySelector('body > div.flex-column.absolute-fill > div.flex-grow.flex-row > div.flex-grow.relative > div > table > tbody > tr > td:nth-child(6)').innerText); - assert.strictEqual(detector, 'MID'); - assert.strictEqual(state, 'FAILED'); - }); - }); }); /** From 7d8f8a9eb50ac414db45ecb24a3742c4805da72a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 12:15:11 +0200 Subject: [PATCH 16/38] Rename page as per standard --- .../pages/Environment/components/environmentNavigationTabs.js | 2 +- .../Environments/EnvironmentsPage.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Control/public/{environment/environmentsPage.js => pages/Environments/EnvironmentsPage.js} (100%) diff --git a/Control/public/pages/Environment/components/environmentNavigationTabs.js b/Control/public/pages/Environment/components/environmentNavigationTabs.js index 1cf193f61..ecee8fd69 100644 --- a/Control/public/pages/Environment/components/environmentNavigationTabs.js +++ b/Control/public/pages/Environment/components/environmentNavigationTabs.js @@ -15,7 +15,7 @@ import {h} from '/js/src/index.js'; import {currentPageAndParameters} from '../../../utilities/currentPageAndParameters.js'; import {environmentConfigurationTable} from './environmentConfigurationTable.js'; -import {isGlobalRun} from '../../../environment/environmentsPage.js'; +import {isGlobalRun} from '../../../environment/EnvironmentsPage.js'; import {miniCard} from '../../../common/card/miniCard.js'; import {parseObject, parseOdcStatusPerEnv} from '../../../common/utils.js'; import {rowForCard} from '../../../common/card/rowForCard.js'; diff --git a/Control/public/environment/environmentsPage.js b/Control/public/pages/Environments/EnvironmentsPage.js similarity index 100% rename from Control/public/environment/environmentsPage.js rename to Control/public/pages/Environments/EnvironmentsPage.js From cb6a9b5325c390ad98f0febc1f24ca8128e225ed Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 12:26:34 +0200 Subject: [PATCH 17/38] Extract function for reuse --- .../components/environmentNavigationTabs.js | 2 +- Control/public/utilities/isGlobalRun.js | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Control/public/utilities/isGlobalRun.js diff --git a/Control/public/pages/Environment/components/environmentNavigationTabs.js b/Control/public/pages/Environment/components/environmentNavigationTabs.js index ecee8fd69..303073ddd 100644 --- a/Control/public/pages/Environment/components/environmentNavigationTabs.js +++ b/Control/public/pages/Environment/components/environmentNavigationTabs.js @@ -15,7 +15,7 @@ import {h} from '/js/src/index.js'; import {currentPageAndParameters} from '../../../utilities/currentPageAndParameters.js'; import {environmentConfigurationTable} from './environmentConfigurationTable.js'; -import {isGlobalRun} from '../../../environment/EnvironmentsPage.js'; +import {isGlobalRun} from '../../../utilities/isGlobalRun.js'; import {miniCard} from '../../../common/card/miniCard.js'; import {parseObject, parseOdcStatusPerEnv} from '../../../common/utils.js'; import {rowForCard} from '../../../common/card/rowForCard.js'; diff --git a/Control/public/utilities/isGlobalRun.js b/Control/public/utilities/isGlobalRun.js new file mode 100644 index 000000000..1d312a305 --- /dev/null +++ b/Control/public/utilities/isGlobalRun.js @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +/** + * Checks if a run is considered global as per the definition: + * * trg_enabled is true + * * trg_global_run_enabled is true + * @param {JSON} userVars - the user variables present in the environment + * @param {string} userVars.trg_enabled - indicates if the TRG is enabled + * @param {string} userVars.trg_global_run_enabled - indicates if the global run + * @returns {boolean} + */ +export const isGlobalRun = ({trg_enabled, trg_global_run_enabled}) => { + return trg_enabled === 'true' && trg_global_run_enabled === 'true'; +} From 0472a9743a2f0923b0c78094d3bb6fb0399598df Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 12:43:41 +0200 Subject: [PATCH 18/38] Extract reusable components --- .../informationRedirectActionPanel.js | 0 .../components/environmentActionPanel.js | 2 +- .../buttonToAcknowledgeDeployment.js | 35 +++++++++++++++++++ Control/public/view.js | 10 +++--- 4 files changed, 41 insertions(+), 6 deletions(-) rename Control/public/{pages/Environment/components => common/environment}/informationRedirectActionPanel.js (100%) create mode 100644 Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js diff --git a/Control/public/pages/Environment/components/informationRedirectActionPanel.js b/Control/public/common/environment/informationRedirectActionPanel.js similarity index 100% rename from Control/public/pages/Environment/components/informationRedirectActionPanel.js rename to Control/public/common/environment/informationRedirectActionPanel.js diff --git a/Control/public/pages/Environment/components/environmentActionPanel.js b/Control/public/pages/Environment/components/environmentActionPanel.js index 9e7818b20..f59a53fc3 100644 --- a/Control/public/pages/Environment/components/environmentActionPanel.js +++ b/Control/public/pages/Environment/components/environmentActionPanel.js @@ -18,7 +18,7 @@ import {miniCard} from '../../../common/card/miniCard.js'; import {controlEnvironmentPanel} from './controlEnvironmentPanel.js'; import {ROLES} from '../../../workflow/constants.js'; import {isUserAllowedRole} from '../../../common/userRole.js'; -import {informationRedirectActionPanel} from './informationRedirectActionPanel.js'; +import {informationRedirectActionPanel} from './../../../common/environment/informationRedirectActionPanel.js'; /** * Build a panel with multiple mini cards which contain actions allowed to the user for the environment diff --git a/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js b/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js new file mode 100644 index 000000000..1c7aac8dc --- /dev/null +++ b/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +import { h } from '/js/src/index.js'; +import { di } from '../../../utilities/di.js'; +import { isUserAllowedRole } from '../../../common/userRole.js'; +import { ROLES } from '../../../workflow/constants.js'; + +/** + * Button to acknowledge a failed deployment and remove it from the cache + * The button is only visible to users with Admin role or the person who attempted the deployment + * @param {string} environmentId - ID of the failed deployment (environment) to acknowledge + * @param {Number} personid - ID of the person who attempted the deployment + * @param {Function} acknowledgeCallback - Callback function to call when the button is clicked + * @return {vnode} - Virtual node representing the button + */ +export const buttonToAcknowledgeDeployment = (environmentId, personid, acknowledgeCallback) => { + if (isUserAllowedRole(ROLES.Admin) || di.session.personid === personid) { + return h('button.btn.btn-danger', { + title: 'Acknowledge failed deployment and remove it from the cache', + onclick: () => acknowledgeCallback(environmentId) + }, 'Acknowledge'); + } +}; diff --git a/Control/public/view.js b/Control/public/view.js index 39201e2bd..000a0a7c3 100644 --- a/Control/public/view.js +++ b/Control/public/view.js @@ -39,9 +39,9 @@ import { CalibrationRunsContent } from './pages/CalibrationRuns/CalibrationRuns.page.js'; import { - content as environmentsContent, - header as environmentsHeader -} from './environment/environmentsPage.js'; + EnvironmentsPageHeader, + EnvironmentsPageContent, +} from './pages/Environments/Environments.page.js'; import {header as statusHeader} from './about/header.js'; import {content as statusContent} from './about/content.js'; import { @@ -94,7 +94,7 @@ const header = (model) => h('.bg-white flex-row p2 shadow-level2 level2', [ newEnvironmentAdvanced: workflowsHeader, newEnvironment: EnvironmentCreationHeader, calibrationRuns: CalibrationRunsHeader, - environments: environmentsHeader, + environments: EnvironmentsPageHeader, environment: EnvironmentPageHeader, about: statusHeader, configuration: configurationHeader, @@ -125,7 +125,7 @@ const content = (model) => { newEnvironmentAdvanced: workflowsContent, newEnvironment: EnvironmentCreationPage, calibrationRuns: CalibrationRunsContent, - environments: environmentsContent, + environments: EnvironmentsPageContent, environment: EnvironmentPageContent, about: statusContent, configuration: configurationContent, From d681bfd2d3eca3a5803117596ada23287e57280e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 14:03:06 +0200 Subject: [PATCH 19/38] Fix incorrect call in deployment controller --- Control/lib/controllers/Deployment.controller.js | 8 ++++++-- .../lib/controllers/mocha-deployment.controller.js | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index a4f13254c..57e71316c 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -107,8 +107,12 @@ class DeploymentController { updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing environment ID parameter')); return; } - this._envService.acknowledgeEnvironmentDeploymentFailure(id, user); - res.status(204).json({ message: 'Environment deployment failure acknowledged' }); + try { + this._deploymentService.acknowledgeEnvironmentDeploymentFailure(id, user); + res.status(204).json({ message: 'Environment deployment failure acknowledged' }); + } catch (error) { + updateAndSendExpressResponseFromNativeError(res, error); + } } } diff --git a/Control/test/lib/controllers/mocha-deployment.controller.js b/Control/test/lib/controllers/mocha-deployment.controller.js index 08cc96c91..21b8726b8 100644 --- a/Control/test/lib/controllers/mocha-deployment.controller.js +++ b/Control/test/lib/controllers/mocha-deployment.controller.js @@ -84,10 +84,10 @@ describe('DeploymentController test suite', function() { }); describe('acknowledgeDeploymentFailureHandler - tests', function() { - let envServiceMock, res; + let deploymentServiceMock, res; beforeEach(function() { - envServiceMock = { acknowledgeEnvironmentDeploymentFailure: sinon.stub() }; - deploymentController._envService = envServiceMock; + deploymentServiceMock = { acknowledgeEnvironmentDeploymentFailure: sinon.stub() }; + deploymentController._deploymentService = deploymentServiceMock; req.params = { id: 'env1' }; res = { status: sinon.stub().returnsThis(), @@ -107,8 +107,8 @@ describe('DeploymentController test suite', function() { session: { username: 'testuser', name: 'Test User', personid: '123' } }, res); - assert.ok(envServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[0], 'env1'); - assert.ok(envServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[1] instanceof User); + assert.ok(deploymentServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[0], 'env1'); + assert.ok(deploymentServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[1] instanceof User); }); it('should return 204 and acknowledgement message on success', async function() { From 4fae0f5cad466bc861cc6dde40fc61d2c36a5dac Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 14:03:51 +0200 Subject: [PATCH 20/38] Remove previous requests components --- Control/public/common/detectorModal.js | 2 +- Control/public/environment/Environment.js | 22 +- .../pages/Environments/Environments.page.js | 221 ++++++++++++++++++ .../pages/Environments/EnvironmentsPage.js | 209 ----------------- .../buttonToAcknowledgeDeployment.js | 6 +- 5 files changed, 230 insertions(+), 230 deletions(-) create mode 100644 Control/public/pages/Environments/Environments.page.js delete mode 100644 Control/public/pages/Environments/EnvironmentsPage.js diff --git a/Control/public/common/detectorModal.js b/Control/public/common/detectorModal.js index 3bdb1b144..3776a17cd 100644 --- a/Control/public/common/detectorModal.js +++ b/Control/public/common/detectorModal.js @@ -55,7 +55,7 @@ const detectorsModal = (model) => /** * Build a wrapped list of detector buttons * @param {Object} model - * @param {List} list + * @param {Array} list * @returns {vnode} */ const detectorsList = (model, list) => diff --git a/Control/public/environment/Environment.js b/Control/public/environment/Environment.js index 70ac012e7..d3fb18cb1 100644 --- a/Control/public/environment/Environment.js +++ b/Control/public/environment/Environment.js @@ -62,25 +62,13 @@ export default class Environment extends Observable { * Remove environment request */ async removeEnvironmentRequest(id) { - this.requests = RemoteData.loading(); - this.notify(); - - const {result, ok} = await this.model.loader.post(`/api/core/removeRequest/${id}`); - this.requests = !ok ? RemoteData.failure(result.message) : RemoteData.success(result); - this.notify(); + try { + await jsonDelete(`/api/deploy/${id}`, {body: {id}}); + } catch (error) { + this.model.notification.show(error.message, 'danger', 5000); + } } - /** - * Get environments requests - */ - async getEnvironmentRequests() { - this.requests = RemoteData.loading(); - this.notify(); - - const {result, ok} = await this.model.loader.get(`/api/core/requests`); - this.requests = !ok ? RemoteData.failure(result.message) : RemoteData.success(result); - this.notify(); - } /** * Load one environment into `item` as RemoteData * @param {Object} body - See protobuf definition for properties diff --git a/Control/public/pages/Environments/Environments.page.js b/Control/public/pages/Environments/Environments.page.js new file mode 100644 index 000000000..b1a65fdcb --- /dev/null +++ b/Control/public/pages/Environments/Environments.page.js @@ -0,0 +1,221 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +import {h} from '/js/src/index.js'; +import {buttonToAcknowledgeDeployment} from './components/buttonToAcknowledgeDeployment.js'; +import {detectorHeader} from '../../common/detectorHeader.js'; +import {environmentReadinessStatus} from '../../common/environment/environmentReadinessStatus.js'; +import {informationRedirectActionPanel} from '../../common/environment/informationRedirectActionPanel.js'; +import {isGlobalRun} from '../../utilities/isGlobalRun.js'; +import {parseObject, parseOdcStatusPerEnv} from '../../common/utils.js'; +import errorPage from '../../common/errorPage.js'; +import pageLoading from '../../common/pageLoading.js'; + +/** + * @file Page to show a list of environments (content and header) + */ + +/** + * Header of page showing list of environments + * With one button to create a new environment and page title + * @return {vnode} + */ +export const EnvironmentsPageHeader = () => [ + h('.w-100 text-center', [ + h('h4', 'Environments') + ]), +]; + +/** + * Scrollable list of environments or page loading/error otherwise + * @param {Object} model + * @return {vnode} + */ +export const EnvironmentsPageContent = (model) => { + const { environment: environmentModel } = model + const { list: environmentsRemoteData } = environmentModel; + + return h('.scroll-y.absolute-fill.text-center', [ + detectorHeader(model), + environmentsRemoteData.match({ + NotAsked: () => null, + Loading: () => pageLoading(), + Success: ({ environments }) => environmentsTablesVerticalComponent(environments, model), + Failure: (error) => errorPage(error), + }), + ]); +}; + +/** + * Component to render the environments tables as active and deployments(ongoing or failed) + * @param {Array} environments - Map of environment objects + * @param {Model} model - Root model of the application + * @returns {vnode} - Component with environments tables + */ +const environmentsTablesVerticalComponent = (environments, model) => { + if (environments.length === 0) { + return h('h3.m4', ['No environments found.']); + } + const [activeEnvironments, deployments] = environments.reduce( + /** + * @param {Array} active - List of active environments to be built + * @param {Array} requests - List of deployments to be built + * @param {EnvironmentInfo} environment - Environment object to be processed + * @returns {Array} - Tuple with active environments and deployment requests + */ + ([active, requests], environment) => { + if (!environment.deploymentError) { + requests.push(environment); + active.push(environment); + } else { + requests.push(environment); + } + return [active, requests]; + }, + [[], []] + ); + return [ + h('.scroll-auto', environmentsTable(activeEnvironments, model)), + deployments.length > 0 && h('.scroll-auto', deploymentsTable(deployments, model)) + ]; +} + +/** + * Renders table of deployments based on cached backend information + * @param {Array} deployments - list of ongoing or failed deployments + * @param {Model} model - Root model of the application + * @return {vnode} - Component with table of deployments + */ +const deploymentsTable = (deployments, model) => { + const tableHeaders = ['ID', 'Detectors', 'Created by', 'Created', 'Message', 'Action']; + + return h('table.table', [ + h('thead', [ + h('tr.white.bg-danger', h('th', { colspan: tableHeaders.length }, 'Failed Deployments')), + h('tr', [tableHeaders.map((header) => + h('th', { style: 'text-align: center;' }, header) + )]) + ]), + h('tbody', [ + deployments.map((environment) => { + const { environment: { removeEnvironmentRequest } } = model; + const { deploymentError, id, includedDetectors, userVars, createdWhen } = environment; + return h('tr', { style: { background: deploymentError ? 'rgba(214, 38, 49, 0.2)' : '' } }, [ + h('td', {style: 'text-align: center;'}, + h('a', { + href: `?page=environment&id=${id}`, + onclick: (e) => model.router.handleLinkEvent(e), + }, id + ) + ), + h('td', { style: 'text-align: center;' }, + includedDetectors?.length > 0 ? includedDetectors.sort().join(' ') : '-' + ), + h('td', { style: 'text-align: center;' }, getUserFromUserVars(userVars).name || '-'), + h('td', { style: 'text-align: center;' }, parseObject(createdWhen, 'createdWhen')), + h('td.f6', { style: 'text-align: center;' }, deploymentError ?? '-'), + h('td', { style: 'text-align: center;' }, buttonToAcknowledgeDeployment(id, getUserFromUserVars(userVars), removeEnvironmentRequest.bind(model.environment))) + ]) + }) + ]) + ]); +}; + +/** + * Component to create the table of active environments + * @param {Array} environments - List of environments as retrieved from backend + * @param {Model} model - Root model of the application + * @return {vnode} + */ +const environmentsTable = (environments, model) => { + const tableHeaders = [ + 'Run', 'State', 'ID', 'Detectors', 'Run Type', 'Created', 'Started', 'Ended', 'FLPs', 'EPNs', 'DCS', 'TRG', 'CTP Readout', + 'ODC', 'InfoLogger' + ]; + + return h('table.table', [ + h('thead', [ + h('tr.table-primary', h('th', {colspan: tableHeaders.length}, 'Active Environments')), + h('tr', [tableHeaders.map((header) => h('th', {style: 'text-align: center;'}, header))]) + ]), + h('tbody', [ + environments.map((item) => { + const {state: odcState, styleClass: odcStyle} = parseOdcStatusPerEnv(item); + return h('tr', { + class: isGlobalRun(item?.userVars ?? {}) ? 'bg-global-run' : '' + }, [ + runColumn(item, model), + h('td', { + class: (item.state === 'RUNNING' ? + 'success' + : (item.state === 'CONFIGURED' ? 'primary' : (item.state === 'ERROR' ? 'danger' : ''))), + style: 'font-weight: bold; text-align: center;' + }, item.state + ), + h('td', {style: 'text-align: center;'}, + h('a', { + href: `?page=environment&id=${item.id}`, + onclick: (e) => model.router.handleLinkEvent(e), + }, item.id + ) + ), + h('td', {style: 'text-align: center;'}, [ + item.includedDetectors && item.includedDetectors.length > 0 ? + item.includedDetectors.sort().map((detector) => `${detector} `) + : '-' + ]), + h('td', {style: 'text-align: center;'}, item.userVars.run_type ? item.userVars.run_type : '-'), + h('td', {style: 'text-align: center;'}, parseObject(item.createdWhen, 'createdWhen')), + h('td', {style: 'text-align: center;'}, parseObject(item.userVars['run_start_time_ms'], 'run_start_time_ms')), + h('td', {style: 'text-align: center;'}, parseObject(item.userVars['run_end_time_ms'], 'run_end_time_ms')), + h('td', {style: 'text-align: center;'}, item.numberOfFlps ? item.numberOfFlps : '-'), + h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'odc_n_epns')), + h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'dcs_enabled')), + h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'trg_enabled')), + h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'ctp_readout_enabled')), + h('td', {style: 'text-align: center;', class: odcStyle}, odcState), + h('td', {style: 'text-align: center;'}, informationRedirectActionPanel(item, true)) + ]); + }), + ]), + ]); +}; + +/** + * Build a cell for displaying the state of a RUN based on conditions environment readiness + * @param {EnvironmentDTO} item - Environment object + * @param {Object} model - Model object + * @returns {vnode} + */ +const runColumn = (item, model) => { + const {statusComponent, styleClasses} = environmentReadinessStatus(item, model); + return h('td', {style: 'text-align: center;'}, + h('.badge.f4', { class: styleClasses }, statusComponent()) + ); +} + +/** + * Extracts the user information from the userVars object + * @param {Object} userVars - The userVars object containing user information + */ +const getUserFromUserVars = ({last_request_user}) => { + if (!last_request_user) { + return null; + } + try { + return JSON.parse(last_request_user); + } catch (error) { + return null; + } +}; diff --git a/Control/public/pages/Environments/EnvironmentsPage.js b/Control/public/pages/Environments/EnvironmentsPage.js deleted file mode 100644 index 5f431e3b2..000000000 --- a/Control/public/pages/Environments/EnvironmentsPage.js +++ /dev/null @@ -1,209 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. -*/ - -import {h} from '/js/src/index.js'; -import pageLoading from '../common/pageLoading.js'; -import errorPage from '../common/errorPage.js'; -import {parseObject, parseOdcStatusPerEnv} from './../common/utils.js'; -import {detectorHeader} from '../common/detectorHeader.js'; -import {ROLES} from './../workflow/constants.js'; -import {isUserAllowedRole} from './../common/userRole.js'; -import {informationRedirectActionPanel} from '../pages/Environment/components/informationRedirectActionPanel.js'; -import {environmentReadinessStatus} from '../common/environment/environmentReadinessStatus.js'; - -/** - * @file Page to show a list of environments (content and header) - */ - -/** - * Header of page showing list of environments - * With one button to create a new environment and page title - * @param {Object} model - * @return {vnode} - */ -export const header = (model) => [ - h('.w-100 text-center', [ - h('h4', 'Environments') - ]), -]; - -/** - * Scrollable list of environments or page loading/error otherwise - * @param {Object} model - * @return {vnode} - */ -export const content = (model) => h('.scroll-y.absolute-fill.text-center', [ - detectorHeader(model), - model.environment.list.match({ - NotAsked: () => null, - Loading: () => pageLoading(), - Success: (data) => showContent(model, data.environments), - Failure: (error) => errorPage(error), - }), - model.environment.requests.match({ - NotAsked: () => null, - Loading: () => pageLoading(), - Success: (data) => showRequests(model, data.requests), - Failure: (error) => errorPage(error) - }) -]); - -/** - * Show a list of environments with a button to edit each of them - * Print a message if the list is empty. - * @param {Object} model - * @param {Array.} list - * @return {vnode} - */ -const showContent = (model, list) => - (list && Object.keys(list).length > 0) - ? h('.scroll-auto', environmentsTable(model, list)) - : h('h3.m4', ['No environments found.']); - -/** - * Show list of create env request to AliECS core as a table - * @param {Object} model - * @param {array} requests List of requested stored in the backend - */ -const showRequests = (model, requests) => - (requests && requests.length > 0) && [h('hr.m4.bg-gray-light'), requestsTable(model, requests)]; - -/** - * Renders table of requests based on backend info - * @param {Object} model - * @param {array} requests List of requests - */ -const requestsTable = (model, requests) => - h('table.table', [ - h('thead', [ - h('tr.primary.bg-white', - {style: 'border-top: 2px solid var(--color-gray); border-bottom: 1px solid var(--color-gray)'}, - h('th', {colspan: 8}, 'Environment creation requests') - ), - h('tr', [['ID', 'Detectors', 'Workflow', 'Created by', 'When', 'State', 'Message', 'Action'].map((header) => - h('th', {style: 'text-align: center;'}, header) - )]) - ]), - h('tbody', [ - requests.map(item => h('tr', {style: {background: item.failed ? 'rgba(214, 38, 49, 0.2)' : ''}}, [ - h('td', {style: 'text-align: center;'}, item.envId || '-'), - h('td', {style: 'text-align: center;'}, - item.detectors && item.detectors.length > 0 ? item.detectors.sort().join(' ') : '-' - ), - h('td', {style: 'text-align: center;'}, item.workflow.substring( - item.workflow.lastIndexOf('/') + 1, item.workflow.indexOf('@') - )), - h('td', {style: 'text-align: center;'}, item.owner), - h('td', {style: 'text-align: center;'}, new Date(item.date).toLocaleString()), - h('td', {style: 'text-align: center;font-weight: bold;'}, item.failed ? 'FAILED' : 'ONGOING'), - h('td.f6', {style: 'text-align: center;'}, item.failed && item.message), - h('td', {style: 'text-align: center;'}, item.failed && buttonRemoveRequest(model, item.id, item.personid)) - ])) - ]) - ]); - -/** - * Button to remove request from the table - * @param {Object} model - * @param {Number} id Request id (server side generated) - * @param {Number} personid Person ID - */ -const buttonRemoveRequest = (model, id, personid) => - (isUserAllowedRole(ROLES.Admin) || model.session.personid == personid) && - h('button.btn.btn-danger', { - title: 'Clear failed environemnt from the list', - onclick: () => model.environment.removeEnvironmentRequest(id) - }, 'Acknowledge'); - -/** - * Create the table of environments - * @param {Object} model - * @param {Array} list - * @return {vnode} - */ -const environmentsTable = (model, list) => { - const tableHeaders = [ - 'Run', 'ID', 'Detectors', 'Run Type', 'Created', 'Started', 'Ended', 'FLPs', 'EPNs', 'DCS', 'TRG', 'CTP Readout', - 'ODC', 'State', 'InfoLogger' - ]; - - return h('table.table', [ - h('thead', [ - h('tr.table-primary', h('th', {colspan: tableHeaders.length}, 'Active Environments')), - h('tr', [tableHeaders.map((header) => h('th', {style: 'text-align: center;'}, header))]) - ]), - h('tbody', [ - list.map((item) => { - const {state: odcState, styleClass: odcStyle} = parseOdcStatusPerEnv(item); - return h('tr', { - class: isGlobalRun(item?.userVars ?? {}) ? 'bg-global-run' : '' - }, [ - runColumn(item, model), - h('td', {style: 'text-align: center;'}, - h('a', { - href: `?page=environment&id=${item.id}`, - onclick: (e) => model.router.handleLinkEvent(e), - }, item.id - ) - ), - h('td', {style: 'text-align: center;'}, [ - item.includedDetectors && item.includedDetectors.length > 0 ? - item.includedDetectors.sort().map((detector) => `${detector} `) - : '-' - ]), - h('td', {style: 'text-align: center;'}, item.userVars.run_type ? item.userVars.run_type : '-'), - h('td', {style: 'text-align: center;'}, parseObject(item.createdWhen, 'createdWhen')), - h('td', {style: 'text-align: center;'}, parseObject(item.userVars['run_start_time_ms'], 'run_start_time_ms')), - h('td', {style: 'text-align: center;'}, parseObject(item.userVars['run_end_time_ms'], 'run_end_time_ms')), - h('td', {style: 'text-align: center;'}, item.numberOfFlps ? item.numberOfFlps : '-'), - h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'odc_n_epns')), - h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'dcs_enabled')), - h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'trg_enabled')), - h('td', {style: 'text-align: center;'}, parseObject(item.userVars, 'ctp_readout_enabled')), - h('td', {style: 'text-align: center;', class: odcStyle}, odcState), - h('td', { - class: (item.state === 'RUNNING' ? - 'success' - : (item.state === 'CONFIGURED' ? 'primary' : (item.state === 'ERROR' ? 'danger' : ''))), - style: 'font-weight: bold; text-align: center;' - }, item.state - ), - h('td', {style: 'text-align: center;'}, informationRedirectActionPanel(item, true)) - ]); - }), - ]), - ]); -}; - -/** - * Build a cell for displaying the state of a RUN based on conditions environment readiness - * @param {EnvironmentDTO} item - Environment object - * @param {Object} model - Model object - * @returns {vnode} - */ -const runColumn = (item, model) => { - const {statusComponent, styleClasses} = environmentReadinessStatus(item, model); - return h('td', {style: 'text-align: center;'}, - h('.badge.f4', { class: styleClasses }, statusComponent()) - ); -} - -/** - * Checks if a run is considered global - * @param {JSON} vars - * @returns {boolean} - */ -export const isGlobalRun = (vars) => { - return vars?.trg_enabled === 'true' && vars?.trg_global_run_enabled === 'true'; -} diff --git a/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js b/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js index 1c7aac8dc..f2d880d54 100644 --- a/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js +++ b/Control/public/pages/Environments/components/buttonToAcknowledgeDeployment.js @@ -21,12 +21,12 @@ import { ROLES } from '../../../workflow/constants.js'; * Button to acknowledge a failed deployment and remove it from the cache * The button is only visible to users with Admin role or the person who attempted the deployment * @param {string} environmentId - ID of the failed deployment (environment) to acknowledge - * @param {Number} personid - ID of the person who attempted the deployment + * @param {{externalId: number, name: string}} user - ID of the person who attempted the deployment * @param {Function} acknowledgeCallback - Callback function to call when the button is clicked * @return {vnode} - Virtual node representing the button */ -export const buttonToAcknowledgeDeployment = (environmentId, personid, acknowledgeCallback) => { - if (isUserAllowedRole(ROLES.Admin) || di.session.personid === personid) { +export const buttonToAcknowledgeDeployment = (environmentId, user, acknowledgeCallback) => { + if (isUserAllowedRole(ROLES.Admin) || di.session.personid === user.externalId) { return h('button.btn.btn-danger', { title: 'Acknowledge failed deployment and remove it from the cache', onclick: () => acknowledgeCallback(environmentId) From f63dac65a25e469589e00021f42866d2fc436990 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 14:04:18 +0200 Subject: [PATCH 21/38] Remove testing case --- Control/public/pages/Environments/Environments.page.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Control/public/pages/Environments/Environments.page.js b/Control/public/pages/Environments/Environments.page.js index b1a65fdcb..a84a30f5d 100644 --- a/Control/public/pages/Environments/Environments.page.js +++ b/Control/public/pages/Environments/Environments.page.js @@ -76,7 +76,6 @@ const environmentsTablesVerticalComponent = (environments, model) => { */ ([active, requests], environment) => { if (!environment.deploymentError) { - requests.push(environment); active.push(environment); } else { requests.push(environment); From f7056dfa771c86b8d9f55a628103b4f73e81ad96 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 17:32:18 +0200 Subject: [PATCH 22/38] Update Enums --- Control/lib/common/broadcastKeys.enum.js | 1 - Control/lib/common/environmentState.enum.js | 1 + Control/public/common/enums/BroadcastKeys.enum.js | 1 - Control/public/common/enums/EnvironmentState.enum.js | 1 + 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Control/lib/common/broadcastKeys.enum.js b/Control/lib/common/broadcastKeys.enum.js index a1459ca3f..8739e74c0 100644 --- a/Control/lib/common/broadcastKeys.enum.js +++ b/Control/lib/common/broadcastKeys.enum.js @@ -20,7 +20,6 @@ const BroadcastKeys = Object.freeze({ NOTIFICATION: 'notification', RESOURCES_CLEANUP: 'resources-cleanup', O2_ROC_CONFIG: 'o2-roc-config', - REQUESTS: 'requests', COMPONENT_STATUS: 'component-STATUS', CALIBRATION_RUNS_BY_DETECTOR: 'CALIBRATION_RUNS_BY_DETECTOR', CALIBRATION_RUNS_REQUESTS: 'CALIBRATION_RUNS_REQUESTS', diff --git a/Control/lib/common/environmentState.enum.js b/Control/lib/common/environmentState.enum.js index d507a5536..e0d39e32e 100644 --- a/Control/lib/common/environmentState.enum.js +++ b/Control/lib/common/environmentState.enum.js @@ -21,6 +21,7 @@ const EnvironmentState = Object.freeze({ DEPLOYED: 'DEPLOYED', CONFIGURED: 'CONFIGURED', RUNNING: 'RUNNING', + PENDING: 'PENDING', ERROR: 'ERROR', UNKNOWN: 'UNKNOWN' }); diff --git a/Control/public/common/enums/BroadcastKeys.enum.js b/Control/public/common/enums/BroadcastKeys.enum.js index a4864db42..2f5b707bb 100644 --- a/Control/public/common/enums/BroadcastKeys.enum.js +++ b/Control/public/common/enums/BroadcastKeys.enum.js @@ -20,7 +20,6 @@ export const BroadcastKeys = Object.freeze({ NOTIFICATION: 'notification', RESOURCES_CLEANUP: 'resources-cleanup', O2_ROC_CONFIG: 'o2-roc-config', - REQUESTS: 'requests', COMPONENT_STATUS: 'component-STATUS', CALIBRATION_RUNS_BY_DETECTOR: 'CALIBRATION_RUNS_BY_DETECTOR', CALIBRATION_RUNS_REQUESTS: 'CALIBRATION_RUNS_REQUESTS', diff --git a/Control/public/common/enums/EnvironmentState.enum.js b/Control/public/common/enums/EnvironmentState.enum.js index e4cd98e3c..2cdcc2cc0 100644 --- a/Control/public/common/enums/EnvironmentState.enum.js +++ b/Control/public/common/enums/EnvironmentState.enum.js @@ -21,6 +21,7 @@ export const EnvironmentState = Object.freeze({ DEPLOYED: 'DEPLOYED', CONFIGURED: 'CONFIGURED', RUNNING: 'RUNNING', + PENDING: 'PENDING', ERROR: 'ERROR', UNKNOWN: 'UNKNOWN' }); From cd761a29a3dba3ec2402e961c2c7d6db146a2ac9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 17:33:54 +0200 Subject: [PATCH 23/38] Update logic for deployments to be kept if in error --- .../fromEcsEventToEnvironmentEvent.js | 6 +- Control/lib/services/Environment.service.js | 15 ++-- .../environment/EnvironmentCache.service.js | 86 ++++++++++++------- 3 files changed, 69 insertions(+), 38 deletions(-) diff --git a/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js b/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js index 66937c090..dff46393c 100644 --- a/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js +++ b/Control/lib/kafka/adapters/fromEcsEventToEnvironmentEvent.js @@ -20,13 +20,17 @@ exports.fromEcsEventToEnvironmentEvent = ({ environmentEvent }) => { const { environmentId: id, - state, error, message, transition, transitionStep, transitionStatus, + runNumber, + state, + error, message, + transition, transitionStep, transitionStatus, } = environmentEvent; return { id, state, error, message, + runNumber, transition: { name: transition, step: transitionStep, diff --git a/Control/lib/services/Environment.service.js b/Control/lib/services/Environment.service.js index 0be3e2aa5..bf9573611 100644 --- a/Control/lib/services/Environment.service.js +++ b/Control/lib/services/Environment.service.js @@ -76,7 +76,6 @@ class EnvironmentService { return []; } const activeEnvironmentList = []; - const cachedEnvironmentIds = [...this._environmentCacheService.environments.keys()]; for (const { id } of environments) { let environment; try { @@ -91,21 +90,21 @@ class EnvironmentService { } activeEnvironmentList.push(environment); } - } // Remove environments from cache that are not in the retrieved list and that are not in deploying state - // Environments that are `isDeploying` should not be removed. If deployment failed, ECS will delete it + // Environments that are `isDeploying` should not be removed. If deployment failed, ECS will delete it // but we need to keep it until user acknowledges the failure // and removes it from the cache manually + const cachedEnvironmentIds = [...this._environmentCacheService.environments.keys()]; for (const cachedEnvironmentId of cachedEnvironmentIds) { - if (!activeEnvironmentList.some(env => env.id === cachedEnvironmentId)) { + if (!activeEnvironmentList.some((env) => env.id === cachedEnvironmentId)) { const environmentPotentiallyToRemove = this._environmentCacheService.environments.get(cachedEnvironmentId); if (environmentPotentiallyToRemove.isDeploying || environmentPotentiallyToRemove.deploymentError) { // If the environment is deploying or has a deployment error, we still consider it active // and we do not remove it from the cache activeEnvironmentList.push(environmentPotentiallyToRemove); } else { - this._environmentCacheService.environments.delete(cachedEnvironmentId); + this._environmentCacheService.removeEnvironmentById(cachedEnvironmentId); } } } @@ -143,6 +142,8 @@ class EnvironmentService { if (retrieveEvents && this._environmentCacheService.environments.has(id)) { const cachedEnvironment = this._environmentCacheService.environments.get(id); environmentInfo.events = [...cachedEnvironment.events]; + environmentInfo.isDeploying = cachedEnvironment.isDeploying; + environmentInfo.deploymentError = cachedEnvironment.deploymentError; } return environmentInfo; } @@ -208,7 +209,6 @@ class EnvironmentService { const detectorsAll = this._apricotGrpc.detectors ?? []; const hostsByDetector = this._apricotGrpc.hostsByDetector ?? {}; - const environmentInfo = EnvironmentInfoAdapter.toEntity(environment, '', detectorsAll, hostsByDetector); /** * Transition is not yet started as per ECS, but we set the state to DEPLOYING to ensure that the UI * is updated accordingly. The state will be updated once the environment is created and the transition @@ -216,7 +216,8 @@ class EnvironmentService { * @type {EnvironmentInfo} * @property {string} currentTransition - the current transition of the environment */ - environmentInfo.isDeploying = true; + environment.isDeploying = true; + const environmentInfo = EnvironmentInfoAdapter.toEntity(environment, '', detectorsAll, hostsByDetector); this._environmentCacheService.addOrUpdateEnvironment(environmentInfo, true); return environmentInfo; } diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index 53b2af951..1d58fd73e 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -74,7 +74,10 @@ class EnvironmentCacheService { } /** - * Update an environment in the cache by its id + * Update an environment in the cache by its id. This method can be used by either + * * ECS GUI results of a transition - which should contain `isDeploying` and `deploymentError` properties + * * Heartbeat calls (GetEnvironment/GetEnvironments) - which will NOT contain `isDeploying` and `deploymentError` properties + * * Cache caught events - which should contain `isDeploying` and `deploymentError` properties * @param {string} id - the id of the environment to be updated * @param {EnvironmentInfo} environment - the new environment information to be set * @returns {void} @@ -84,8 +87,11 @@ class EnvironmentCacheService { if (this._environments.has(id)) { const cachedEnvironment = this._environments.get(id); const { events = [] } = cachedEnvironment; + const {isDeploying, deploymentError } = cachedEnvironment; const updatedEnvironment = Object.assign({}, cachedEnvironment, environment); updatedEnvironment.events = [...events]; + updatedEnvironment.isDeploying = isDeploying; + updatedEnvironment.deploymentError = deploymentError; this._environments.set(id, updatedEnvironment); } else { this._environments.set(id, { ...environment, events: environment.events ?? [] }); @@ -135,35 +141,7 @@ class EnvironmentCacheService { /** * @param {EnvironmentEvent} environmentEvent - the event object containing the payload and environmentId */ - this._eventEmitter.on(ENVIRONMENTS_TRACK, (environmentEvent) => { - const { id, state, message, error } = environmentEvent; - - const cachedEnvironment = this._environments.has(id) - ? this._environments.get(id) - : { id, events: [] }; - - if (cachedEnvironment.isDeploying && error) { - // If the environment is deploying and there is an error, environment will not be active in ECS anymore but - // we still want to keep the information in the cache until a user acknowledges the error - cachedEnvironment.isDeploying = false; - cachedEnvironment.deploymentError = error; - } - if ( - state === EnvironmentState.CONFIGURED && - message === ECS_TRANSITION_DONE_MESSAGE - // OCTRL-1038 - currently comparing to hardcoded string, but this should be replaced with transition status - ) { - // Once the environment is configured and ongoing transition is done, we can set the isDeploying to false - // This can happen when the environment also goes form RUNNING to CONFIGURED but it is already marked as not deploying anymore - cachedEnvironment.isDeploying = false; - } - - cachedEnvironment.events.push(environmentEvent); - cachedEnvironment.lastUpdate = environmentEvent.timestamp; - this._environments.set(id, cachedEnvironment); - this._broadcastService.broadcast(ENVIRONMENT_EVENTS, cachedEnvironment); - this._lastUpdate = Date.now(); - }); + this._eventEmitter.on(ENVIRONMENTS_TRACK, this._handleEnvironmentEvent.bind(this)); this._eventEmitter.on(INTEGRATED_SERVICES_TRACK.ODC.ENVIRONMENT_STATE_CHANGE, /** @@ -227,6 +205,54 @@ class EnvironmentCacheService { this._broadcastService.broadcast(NOTIFICATION, event); } } + + /** + * Handles the environment event by: + * * updating the cache with interested changes + * * broadcasting the event information + * It does not: + * * broadcast the overview of environments + * @private + * @param {EnvironmentEvent} environmentEvent - the event object containing the payload and environmentId + * @returns {void} + */ + _handleEnvironmentEvent(environmentEvent) { + const { id, state, transition, message, error, runNumber } = environmentEvent; + const cachedEnvironment = this._environments.has(id) + ? this._environments.get(id) + : { id, events: [] }; + + if (state === EnvironmentState.PENDING) { + // If the environment is pending, we set isDeploying to true as it is the first event we receive for this environment + // and we want to ensure that the UI is updated accordingly. + cachedEnvironment.isDeploying = true; + } + if (cachedEnvironment.isDeploying && error) { + // If the environment is deploying and there is an error, environment will not be active in ECS anymore but + // we still want to keep the information in the cache until a user acknowledges the error + cachedEnvironment.isDeploying = false; + cachedEnvironment.deploymentError = error; + } + + if ( + state === EnvironmentState.CONFIGURED && + message === ECS_TRANSITION_DONE_MESSAGE + // OCTRL-1038 - currently comparing to hardcoded string, but this should be replaced with transition status + ) { + // Once the environment is configured and ongoing transition is done, we can set the isDeploying to false + // This can happen when the environment also goes form RUNNING to CONFIGURED but it is already marked as not deploying anymore + cachedEnvironment.isDeploying = false; + } + cachedEnvironment.state = state; + cachedEnvironment.currentTransition = transition.name; + cachedEnvironment.currentRunNumber = runNumber; + cachedEnvironment.events.push(environmentEvent); + cachedEnvironment.lastUpdate = environmentEvent.timestamp; + this._environments.set(id, cachedEnvironment); + + this._broadcastService.broadcast(ENVIRONMENT_EVENTS, cachedEnvironment); + this._lastUpdate = Date.now(); + } } module.exports = {EnvironmentCacheService}; From 6a40ac02b54687e707bd93a0bf371fa69fd4bb4a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 17:34:25 +0200 Subject: [PATCH 24/38] Update front-end from events rather than heartbit --- Control/public/Model.js | 30 +++++++++++-------- Control/public/environment/Environment.js | 1 - .../pages/Environments/Environments.page.js | 18 +++++------ .../test/public/page-environments-mocha.js | 21 ------------- 4 files changed, 26 insertions(+), 44 deletions(-) diff --git a/Control/public/Model.js b/Control/public/Model.js index 1e3050b75..ac5cd03a0 100644 --- a/Control/public/Model.js +++ b/Control/public/Model.js @@ -184,15 +184,6 @@ export default class Model extends Observable { case BroadcastKeys.O2_ROC_CONFIG: this.configuration.setConfigurationRequest(message.payload); break; - case BroadcastKeys.ENVIRONMENTS_OVERVIEW: - this.environment.list = RemoteData.success({ environments: message.payload ?? [] }); - this.environment.updateItemEnvironment(message.payload, this.router.params?.panel ?? ''); - this.notify(); - break; - case BroadcastKeys.REQUESTS: - this.environment.requests = RemoteData.success(message.payload); - this.notify(); - break; case BroadcastKeys.COMPONENT_STATUS: if (message?.payload[STATUS_COMPONENTS_KEYS.GENERAL_SYSTEM_KEY]) { this.about.updateComponentStatus('system', message.payload[STATUS_COMPONENTS_KEYS.GENERAL_SYSTEM_KEY]); @@ -219,15 +210,29 @@ export default class Model extends Observable { this.cache.dcs.sor = message.payload; this.notify(); break; + case BroadcastKeys.ENVIRONMENTS_OVERVIEW: + this.environment.list = RemoteData.success({ environments: message.payload ?? [] }); + this.environment.updateItemEnvironment(message.payload, this.router.params?.panel ?? ''); + this.notify(); + break; case BroadcastKeys.ENVIRONMENT_EVENTS: if (this.environment.item.isSuccess()) { const { id } = this.environment.item.payload; - const { id: eventsId, events } = message.payload; - if (id === eventsId) { - this.environment.item.payload.events = events; + const eventPayload = message.payload; + if (id === eventPayload.id) { + Object.assign(this.environment.item.payload, eventPayload); this.environment.notify(); } } + if (this.environment.list.isSuccess()) { + const environmentEvent = message.payload; + this.environment.list.payload.environments.forEach((environment) => { + if (environment.id === environmentEvent.id) { + Object.assign(environment, environmentEvent); + this.notify(); + } + }); + } break; } } @@ -275,7 +280,6 @@ export default class Model extends Observable { switch (this.router.params.page) { case 'environments': this.environment.getEnvironments(); - this.environment.getEnvironmentRequests(); break; case 'environment': if (!this.router.params.id) { diff --git a/Control/public/environment/Environment.js b/Control/public/environment/Environment.js index d3fb18cb1..01c5a7a87 100644 --- a/Control/public/environment/Environment.js +++ b/Control/public/environment/Environment.js @@ -32,7 +32,6 @@ export default class Environment extends Observable { this.taskTableModel.bubbleTo(model); this.model = model; - this.requests = RemoteData.notAsked(); this.list = RemoteData.notAsked(); this.item = RemoteData.notAsked(); this.itemControl = RemoteData.notAsked(); diff --git a/Control/public/pages/Environments/Environments.page.js b/Control/public/pages/Environments/Environments.page.js index a84a30f5d..dc3d322ba 100644 --- a/Control/public/pages/Environments/Environments.page.js +++ b/Control/public/pages/Environments/Environments.page.js @@ -67,26 +67,26 @@ const environmentsTablesVerticalComponent = (environments, model) => { if (environments.length === 0) { return h('h3.m4', ['No environments found.']); } - const [activeEnvironments, deployments] = environments.reduce( + const [activeEnvironments, failedDeployments] = environments.reduce( /** - * @param {Array} active - List of active environments to be built - * @param {Array} requests - List of deployments to be built + * @param {Array} environments - List of active environments to be built + * @param {Array} failedDeployments - List of deployments to be built * @param {EnvironmentInfo} environment - Environment object to be processed - * @returns {Array} - Tuple with active environments and deployment requests + * @returns {Array} - Tuple with active environments and failed deployments */ - ([active, requests], environment) => { + ([environments, failedDeployments], environment) => { if (!environment.deploymentError) { - active.push(environment); + environments.push(environment); } else { - requests.push(environment); + failedDeployments.push(environment); } - return [active, requests]; + return [environments, failedDeployments]; }, [[], []] ); return [ h('.scroll-auto', environmentsTable(activeEnvironments, model)), - deployments.length > 0 && h('.scroll-auto', deploymentsTable(deployments, model)) + failedDeployments.length > 0 && h('.scroll-auto', deploymentsTable(failedDeployments, model)) ]; } diff --git a/Control/test/public/page-environments-mocha.js b/Control/test/public/page-environments-mocha.js index c8ec2468a..a79c14819 100644 --- a/Control/test/public/page-environments-mocha.js +++ b/Control/test/public/page-environments-mocha.js @@ -65,24 +65,3 @@ describe('`pageEnvironments` test-suite', () => { }); }); }); - -/** - * Wait for response create env request to fail - * @param {Object} page - * @param {number} iterations - number of times to wait for default timeout - * @return {Promise} - */ -async function waitForEnvRequest(page, iterations = 10, timeout = 1000) { - return new Promise(async (resolve) => { - let i = 0; - while (i++ < iterations) { - const requestFailed = await page.evaluate(() => window.model?.environment?.requests?.payload?.requests[0]?.failed); - if (requestFailed) { - await new Promise((r) => setTimeout((r), timeout)) - resolve(); - } else { - await new Promise((r) => setTimeout((r), timeout)) - } - } - }); -} From 86c9d1aba3f5a43b15d143e00d3a242dc24622db Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 15 Aug 2025 17:45:33 +0200 Subject: [PATCH 25/38] Improve code safety --- .../environment/EnvironmentCache.service.js | 2 +- .../pages/Environments/Environments.page.js | 25 ++++++++++++++----- .../mocha-environment-cache.service.test.js | 3 ++- .../test/public/page-environments-mocha.js | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index 1d58fd73e..f6b57740c 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -244,7 +244,7 @@ class EnvironmentCacheService { cachedEnvironment.isDeploying = false; } cachedEnvironment.state = state; - cachedEnvironment.currentTransition = transition.name; + cachedEnvironment.currentTransition = transition?.name ?? '-'; cachedEnvironment.currentRunNumber = runNumber; cachedEnvironment.events.push(environmentEvent); cachedEnvironment.lastUpdate = environmentEvent.timestamp; diff --git a/Control/public/pages/Environments/Environments.page.js b/Control/public/pages/Environments/Environments.page.js index dc3d322ba..dd7fc118c 100644 --- a/Control/public/pages/Environments/Environments.page.js +++ b/Control/public/pages/Environments/Environments.page.js @@ -16,6 +16,7 @@ import {h} from '/js/src/index.js'; import {buttonToAcknowledgeDeployment} from './components/buttonToAcknowledgeDeployment.js'; import {detectorHeader} from '../../common/detectorHeader.js'; import {environmentReadinessStatus} from '../../common/environment/environmentReadinessStatus.js'; +import {EnvironmentState} from '../../common/enums/EnvironmentState.enum.js'; import {informationRedirectActionPanel} from '../../common/environment/informationRedirectActionPanel.js'; import {isGlobalRun} from '../../utilities/isGlobalRun.js'; import {parseObject, parseOdcStatusPerEnv} from '../../common/utils.js'; @@ -124,7 +125,15 @@ const deploymentsTable = (deployments, model) => { h('td', { style: 'text-align: center;' }, getUserFromUserVars(userVars).name || '-'), h('td', { style: 'text-align: center;' }, parseObject(createdWhen, 'createdWhen')), h('td.f6', { style: 'text-align: center;' }, deploymentError ?? '-'), - h('td', { style: 'text-align: center;' }, buttonToAcknowledgeDeployment(id, getUserFromUserVars(userVars), removeEnvironmentRequest.bind(model.environment))) + h( + 'td', + { style: 'text-align: center;' }, + buttonToAcknowledgeDeployment( + id, + getUserFromUserVars(userVars), + removeEnvironmentRequest.bind(model.environment) + ) + ) ]) }) ]) @@ -139,8 +148,8 @@ const deploymentsTable = (deployments, model) => { */ const environmentsTable = (environments, model) => { const tableHeaders = [ - 'Run', 'State', 'ID', 'Detectors', 'Run Type', 'Created', 'Started', 'Ended', 'FLPs', 'EPNs', 'DCS', 'TRG', 'CTP Readout', - 'ODC', 'InfoLogger' + 'Run', 'State', 'ID', 'Detectors', 'Run Type', 'Created', 'Started', 'Ended', 'FLPs', 'EPNs', 'DCS', 'TRG', + 'CTP Readout', 'ODC', 'InfoLogger' ]; return h('table.table', [ @@ -155,10 +164,14 @@ const environmentsTable = (environments, model) => { class: isGlobalRun(item?.userVars ?? {}) ? 'bg-global-run' : '' }, [ runColumn(item, model), - h('td', { - class: (item.state === 'RUNNING' ? + h('td', { + class: (item.state === EnvironmentState.RUNNING ? 'success' - : (item.state === 'CONFIGURED' ? 'primary' : (item.state === 'ERROR' ? 'danger' : ''))), + : (item.state === EnvironmentState.CONFIGURED + ? 'primary' + : (item.state === EnvironmentState.ERROR ? 'danger' : '') + ) + ), style: 'font-weight: bold; text-align: center;' }, item.state ), diff --git a/Control/test/lib/services/environment/mocha-environment-cache.service.test.js b/Control/test/lib/services/environment/mocha-environment-cache.service.test.js index 3463bbdb8..15df42c49 100644 --- a/Control/test/lib/services/environment/mocha-environment-cache.service.test.js +++ b/Control/test/lib/services/environment/mocha-environment-cache.service.test.js @@ -62,6 +62,8 @@ describe(`'EnvironmentCacheService' - test suite`, () => { assert.strictEqual(environmentCacheService._environments.size, 1); assert.deepStrictEqual(environmentCacheService._environments.get('abc123'), { id: 'abc123', + isDeploying: undefined, + deploymentError: undefined, state: 'inactive', events: [] }); @@ -163,7 +165,6 @@ describe(`'EnvironmentCacheService' - test suite`, () => { it('should update an existing environment in the cache and broadcast it', () => { const initialEvent = { id: 'abc135', timestamp: Date.now() - 1000 }; const updatedEvent = { id: 'abc135', timestamp: Date.now() }; - const transformedEvent = { id: 'abc135', events: [], lastUpdate: updatedEvent.timestamp }; // Emit initial event eventEmitter.emit(ENVIRONMENTS_TRACK, initialEvent); diff --git a/Control/test/public/page-environments-mocha.js b/Control/test/public/page-environments-mocha.js index a79c14819..b66a0d536 100644 --- a/Control/test/public/page-environments-mocha.js +++ b/Control/test/public/page-environments-mocha.js @@ -50,7 +50,7 @@ describe('`pageEnvironments` test-suite', () => { }); it('should successfully navigate to environment page when clicking on environment ID', async () => { - await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > table > tbody > tr > td:nth-child(2) > a').click()); + await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > table > tbody > tr > td:nth-child(3) > a').click()); assert.ok(calls['getEnvironment']); const location = await page.evaluate(() => window.location); assert.strictEqual(location.search, '?page=environment&id=6f6d6387-6577-11e8-993a-f07959157220&panel=general'); From 622b54c07ff1297cd312b1079a30e767c979eb55 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 10:34:51 +0200 Subject: [PATCH 26/38] Update based on review comments --- Control/lib/control-core/GrpcServiceClient.js | 1 - Control/test/lib/controllers/mocha-deployment.controller.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Control/lib/control-core/GrpcServiceClient.js b/Control/lib/control-core/GrpcServiceClient.js index 219f28d5a..d24e28b26 100644 --- a/Control/lib/control-core/GrpcServiceClient.js +++ b/Control/lib/control-core/GrpcServiceClient.js @@ -100,7 +100,6 @@ class GrpcServiceClient { this.client[methodName](args, options, (error, response) => { if (error) { reject(error); - return } resolve(response); }); diff --git a/Control/test/lib/controllers/mocha-deployment.controller.js b/Control/test/lib/controllers/mocha-deployment.controller.js index 21b8726b8..41443da9d 100644 --- a/Control/test/lib/controllers/mocha-deployment.controller.js +++ b/Control/test/lib/controllers/mocha-deployment.controller.js @@ -107,7 +107,7 @@ describe('DeploymentController test suite', function() { session: { username: 'testuser', name: 'Test User', personid: '123' } }, res); - assert.ok(deploymentServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[0], 'env1'); + assert.strictEqual(deploymentServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[0], 'env1'); assert.ok(deploymentServiceMock.acknowledgeEnvironmentDeploymentFailure.firstCall.args[1] instanceof User); }); From 364d245e20074ac4d8ec5420c3ac71276c247a41 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 10:48:33 +0200 Subject: [PATCH 27/38] Remove from cache if env was successfully destroyed --- Control/lib/common/environmentState.enum.js | 1 + Control/lib/common/environmentTransitionType.enum.js | 1 + .../services/environment/EnvironmentCache.service.js | 12 +++++++++++- Control/public/common/enums/EnvironmentState.enum.js | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Control/lib/common/environmentState.enum.js b/Control/lib/common/environmentState.enum.js index e0d39e32e..69bb36c9e 100644 --- a/Control/lib/common/environmentState.enum.js +++ b/Control/lib/common/environmentState.enum.js @@ -19,6 +19,7 @@ const EnvironmentState = Object.freeze({ STANDBY: 'STANDBY', DEPLOYED: 'DEPLOYED', + DONE: 'DONE', CONFIGURED: 'CONFIGURED', RUNNING: 'RUNNING', PENDING: 'PENDING', diff --git a/Control/lib/common/environmentTransitionType.enum.js b/Control/lib/common/environmentTransitionType.enum.js index bb96ed11b..823684040 100644 --- a/Control/lib/common/environmentTransitionType.enum.js +++ b/Control/lib/common/environmentTransitionType.enum.js @@ -23,6 +23,7 @@ const EnvironmentTransitionType = Object.freeze({ RESET: 'RESET', GO_ERROR: 'GO_ERROR', DEPLOY: 'DEPLOY', + DESTROY: 'DESTROY', }); exports.EnvironmentTransitionType = EnvironmentTransitionType; diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index f6b57740c..23c3767f3 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -23,9 +23,11 @@ const { } = require('./../../common/emitterKeys.enum.js'); const { EnvironmentState } = require('../../common/environmentState.enum.js'); const { TaskState } = require('../../common/taskState.enum.js'); +const { EnvironmentTransitionType } = require('../../common/environmentTransitionType.enum.js'); const EPN_PATH_IN_ENVIRONMENT_INFO = 'hardware.epn.info'; const ECS_TRANSITION_DONE_MESSAGE = 'transition completed successfully'; +const ECS_DESTROY_TRANSITION_DONE_MESSAGE = 'environment teardown complete'; /** * @class @@ -243,13 +245,21 @@ class EnvironmentCacheService { // This can happen when the environment also goes form RUNNING to CONFIGURED but it is already marked as not deploying anymore cachedEnvironment.isDeploying = false; } - cachedEnvironment.state = state; cachedEnvironment.currentTransition = transition?.name ?? '-'; cachedEnvironment.currentRunNumber = runNumber; cachedEnvironment.events.push(environmentEvent); cachedEnvironment.lastUpdate = environmentEvent.timestamp; this._environments.set(id, cachedEnvironment); + if ( + transition.name === EnvironmentTransitionType.DESTROY && + state === EnvironmentState.DONE && + message === ECS_DESTROY_TRANSITION_DONE_MESSAGE + ) { + // That is, if the environment successfully ended the DESTROY transition + this.removeEnvironmentById(id); + } + this._broadcastService.broadcast(ENVIRONMENT_EVENTS, cachedEnvironment); this._lastUpdate = Date.now(); } diff --git a/Control/public/common/enums/EnvironmentState.enum.js b/Control/public/common/enums/EnvironmentState.enum.js index 2cdcc2cc0..d99854b7c 100644 --- a/Control/public/common/enums/EnvironmentState.enum.js +++ b/Control/public/common/enums/EnvironmentState.enum.js @@ -19,6 +19,7 @@ export const EnvironmentState = Object.freeze({ STANDBY: 'STANDBY', DEPLOYED: 'DEPLOYED', + DONE: 'DONE', CONFIGURED: 'CONFIGURED', RUNNING: 'RUNNING', PENDING: 'PENDING', From 9ec3defb4ac1993ffb2cf0377abfa777225c6df0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:00:00 +0200 Subject: [PATCH 28/38] Add test on event for env cache --- .../environment/EnvironmentCache.service.js | 4 +-- .../mocha-environment-cache.service.test.js | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index 23c3767f3..119f8a842 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -219,7 +219,7 @@ class EnvironmentCacheService { * @returns {void} */ _handleEnvironmentEvent(environmentEvent) { - const { id, state, transition, message, error, runNumber } = environmentEvent; + const { id, state, transition = {}, message, error, runNumber } = environmentEvent; const cachedEnvironment = this._environments.has(id) ? this._environments.get(id) : { id, events: [] }; @@ -252,7 +252,7 @@ class EnvironmentCacheService { this._environments.set(id, cachedEnvironment); if ( - transition.name === EnvironmentTransitionType.DESTROY && + transition?.name === EnvironmentTransitionType.DESTROY && state === EnvironmentState.DONE && message === ECS_DESTROY_TRANSITION_DONE_MESSAGE ) { diff --git a/Control/test/lib/services/environment/mocha-environment-cache.service.test.js b/Control/test/lib/services/environment/mocha-environment-cache.service.test.js index 15df42c49..a009f00ff 100644 --- a/Control/test/lib/services/environment/mocha-environment-cache.service.test.js +++ b/Control/test/lib/services/environment/mocha-environment-cache.service.test.js @@ -18,8 +18,11 @@ const EventEmitter = require('events'); const { EnvironmentCacheService } = require('../../../../lib/services/environment/EnvironmentCache.service.js'); const { EmitterKeys: { ENVIRONMENTS_TRACK, INTEGRATED_SERVICES_TRACK: { ODC }, TASKS_TRACK, -} } = require('../../../../lib/common/emitterKeys.enum.js'); +}, +EmitterKeys} = require('../../../../lib/common/emitterKeys.enum.js'); const { BroadcastKeys: {ENVIRONMENT_EVENTS, ENVIRONMENTS_OVERVIEW} } = require('../../../../lib/common/broadcastKeys.enum.js'); +const { EnvironmentState } = require('../../../../lib/common/environmentState.enum.js'); +const { EnvironmentTransitionType } = require('../../../../lib/common/environmentTransitionType.enum.js'); describe(`'EnvironmentCacheService' - test suite`, () => { let broadcastServiceMock; @@ -361,5 +364,26 @@ describe(`'EnvironmentCacheService' - test suite`, () => { assert.deepStrictEqual(env.firstTaskInError, firstTaskInErrorEventSent, 'firstTaskInError should still be the first task in error'); assert.strictEqual(broadcastServiceMock.broadcast.callCount, 1, 'Broadcast should not be made again when subsequent task in ERROR/ERROR_CRITICAL is received'); }); + + it('should successfully remove environment from cache on successful DESTROY transition', () => { + const initialEnvironment = { + id: 'env1', + state: EnvironmentState.CONFIGURED + }; + environmentCacheService.addOrUpdateEnvironment(initialEnvironment); + + eventEmitter.emit(EmitterKeys.ENVIRONMENTS_TRACK, { + id: 'env1', + transition: { + name: EnvironmentTransitionType.DESTROY, + }, + message: 'environment teardown complete', + state: EnvironmentState.DONE + }); + + const env = environmentCacheService._environments.get('env1'); + assert.ok(!env, 'Environment "env1" should be removed from the cache'); + assert.strictEqual(broadcastServiceMock.broadcast.callCount, 2, 'Broadcast (event and overview) should be made when environment is removed'); + }); }); }); From f7eaff3d58df3819d530d48770477a51c5e25d22 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:13:22 +0200 Subject: [PATCH 29/38] Wait for network before button --- Control/test/public/page-environment-mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index c3427ff77..460e6e6b3 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -115,10 +115,10 @@ describe('`pageEnvironment` test-suite', async () => { describe('Check transition from CONFIGURED to RUNNING and presence of buttons in RUNNING state', async () => { it('should click START button to move states (CONFIGURED -> RUNNING)', async () => { + await page.waitForNetworkIdle(); await page.locator('#buttonToSTART') .setTimeout(1000) .click(); - await page.waitForNetworkIdle(); const state = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(state, 'RUNNING'); From d5b19362842413b51df791b9483fe7f159f0884a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:20:03 +0200 Subject: [PATCH 30/38] Increase timeout for lock and detector selection --- Control/test/public/page-new-environment-mocha.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Control/test/public/page-new-environment-mocha.js b/Control/test/public/page-new-environment-mocha.js index 776f79b1c..0a3b4aa5e 100644 --- a/Control/test/public/page-new-environment-mocha.js +++ b/Control/test/public/page-new-environment-mocha.js @@ -396,11 +396,11 @@ describe('`pageNewEnvironment` test-suite', async () => { it('should successfully lock, select a detector and request a list of hosts for that detector', async () => { await page.locator('.m1 > div:nth-child(1) > div > div:nth-child(1)') - .setTimeout(500) + .setTimeout(1000) .click(); await page.locator('.m1 > div:nth-child(1) > div > a:nth-child(2)') - .setTimeout(500) + .setTimeout(1000) .click(); const selectedDet = await page.evaluate(() => window.model.workflow.flpSelection.selectedDetectors); From c5bd1d3d2bbc63422a637dfa5586081df386d150 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:32:32 +0200 Subject: [PATCH 31/38] Use old library instead of locator --- Control/test/public/page-environment-mocha.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index 460e6e6b3..3ca604cff 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -105,21 +105,14 @@ describe('`pageEnvironment` test-suite', async () => { assert.strictEqual(configureButtonTitle, `'CONFIGURE' cannot be used in state 'CONFIGURED'`); assert.deepStrictEqual(configureButtonStyle, {0: 'display'}); }); - - // it('should have one button for RESET in state CONFIGURED', async () => { - // await page.waitForSelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > div > button:nth-child(4)', {timeout: 5000}); - // const configuredStateButtons = await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) >div >div >div >div >button:nth-child(4)').title); - // assert.strictEqual(configuredStateButtons, 'RESET'); - // }); }); describe('Check transition from CONFIGURED to RUNNING and presence of buttons in RUNNING state', async () => { it('should click START button to move states (CONFIGURED -> RUNNING)', async () => { + await page.waitForSelector('#buttonToSTART', {timeout: 5000}); + // click STOP + await page.evaluate(() => document.querySelector('#buttonToSTART').click()); await page.waitForNetworkIdle(); - await page.locator('#buttonToSTART') - .setTimeout(1000) - .click(); - const state = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(state, 'RUNNING'); assert.ok(calls['controlEnvironment']); From 46fcdce92186e2f287a09eeb7b3567d4a46ce58d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:45:15 +0200 Subject: [PATCH 32/38] Use waitForFunction instead --- Control/test/public/page-environment-mocha.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index 3ca604cff..abd8b6696 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -112,7 +112,8 @@ describe('`pageEnvironment` test-suite', async () => { await page.waitForSelector('#buttonToSTART', {timeout: 5000}); // click STOP await page.evaluate(() => document.querySelector('#buttonToSTART').click()); - await page.waitForNetworkIdle(); + await page.waitForFunction(() => window.model.environment.item.payload.state === 'RUNNING'); + const state = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(state, 'RUNNING'); assert.ok(calls['controlEnvironment']); From 04d56d5a8ac03bd7e3ec995de6681e89558b74fa Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:56:53 +0200 Subject: [PATCH 33/38] Add util waitForTimeout for specific test --- Control/test/public/page-environment-mocha.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index abd8b6696..7b39b3e62 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -15,6 +15,7 @@ /* eslint-disable max-len */ const assert = require('assert'); const test = require('../mocha-index'); +const waitForTimeout = (timeout) => new Promise((res) => setTimeout(res, timeout)); describe('`pageEnvironment` test-suite', async () => { let url; @@ -112,7 +113,7 @@ describe('`pageEnvironment` test-suite', async () => { await page.waitForSelector('#buttonToSTART', {timeout: 5000}); // click STOP await page.evaluate(() => document.querySelector('#buttonToSTART').click()); - await page.waitForFunction(() => window.model.environment.item.payload.state === 'RUNNING'); + await waitForTimeout(2000) const state = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(state, 'RUNNING'); From 7ad2465b5453cf08b6e5752d87491507489faf39 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 11:58:58 +0200 Subject: [PATCH 34/38] Use await for rest of scenarios in transition --- Control/test/public/page-environment-mocha.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index 7b39b3e62..8e35e08ee 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -113,7 +113,7 @@ describe('`pageEnvironment` test-suite', async () => { await page.waitForSelector('#buttonToSTART', {timeout: 5000}); // click STOP await page.evaluate(() => document.querySelector('#buttonToSTART').click()); - await waitForTimeout(2000) + await waitForTimeout(1000) const state = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(state, 'RUNNING'); @@ -156,12 +156,12 @@ describe('`pageEnvironment` test-suite', async () => { await page.waitForSelector('#buttonToSTOP', {timeout: 5000}); // click STOP await page.evaluate(() => document.querySelector('#buttonToSTOP').click()); - await page.waitForNetworkIdle(); + await waitForTimeout(1000); const configuredState = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(configuredState, 'CONFIGURED'); // click RESET await page.evaluate(() => document.querySelector('#buttonToRESET').click()); - await page.waitForNetworkIdle(); + await waitForTimeout(1000); const standbyState = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(standbyState, 'DEPLOYED'); }); From dc2c7667d1d64c0054b81d3aae0483e2c1c27f7f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 12:05:11 +0200 Subject: [PATCH 35/38] Update tests to use timeout in waiting for page to render --- Control/test/config/core-grpc.js | 12 ----------- Control/test/mocha-index.js | 6 +++--- Control/test/public/page-environment-mocha.js | 4 ++-- .../test/public/page-new-environment-mocha.js | 2 ++ Control/test/utils/puppeteerUtils.js | 21 +++++++++++++++++++ 5 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 Control/test/utils/puppeteerUtils.js diff --git a/Control/test/config/core-grpc.js b/Control/test/config/core-grpc.js index 42c4e4078..01bf269e5 100644 --- a/Control/test/config/core-grpc.js +++ b/Control/test/config/core-grpc.js @@ -136,18 +136,6 @@ const envTest = { dd_enabled: 'true', run_type: 'run' }, - vars: { - odc_enabled: 'true', - mid_enabled: 'false', - mid_something: 'test', - dd_enabled: 'true', - run_type: 'run' - }, - defaults: { - dcs_topology: 'test', - dd_enabled: 'true', - run_type: 'run' - }, integratedServices: { } }, diff --git a/Control/test/mocha-index.js b/Control/test/mocha-index.js index 33f16f813..3ae66739a 100644 --- a/Control/test/mocha-index.js +++ b/Control/test/mocha-index.js @@ -73,10 +73,10 @@ describe('Control', function() { }); page.on('console', (msg) => { for (let i = 0; i < msg.args().length; ++i) { - console.log(` ${msg.args()[i]}`); + console.log(`[browser] ${msg.args()[i]}`); } }); - await page.setViewport({width: 1200, height: 770}); + await page.setViewport({ width: 1920, height: 1080 }); exports.page = page; const helpers = {url, calls, apricotCalls}; exports.helpers = helpers; @@ -165,7 +165,7 @@ describe('Control', function() { require('./public/page-about-mocha'); require('./public/page-environment-mocha'); require('./public/page-environments-mocha'); - // require('./public/page-configuration-mocha'); + require('./public/page-configuration-mocha'); require('./public/page-tasks-mocha'); require('./public/page-hardware-mocha'); require('./public/page-lock-mocha'); diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index 8e35e08ee..5bd141814 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -15,7 +15,7 @@ /* eslint-disable max-len */ const assert = require('assert'); const test = require('../mocha-index'); -const waitForTimeout = (timeout) => new Promise((res) => setTimeout(res, timeout)); +const {waitForTimeout} = require('../utils/puppeteerUtils.js'); describe('`pageEnvironment` test-suite', async () => { let url; @@ -113,7 +113,7 @@ describe('`pageEnvironment` test-suite', async () => { await page.waitForSelector('#buttonToSTART', {timeout: 5000}); // click STOP await page.evaluate(() => document.querySelector('#buttonToSTART').click()); - await waitForTimeout(1000) + await waitForTimeout(1000); const state = await page.evaluate(() => window.model.environment.item.payload.state); assert.strictEqual(state, 'RUNNING'); diff --git a/Control/test/public/page-new-environment-mocha.js b/Control/test/public/page-new-environment-mocha.js index 0a3b4aa5e..6b2d2f5b2 100644 --- a/Control/test/public/page-new-environment-mocha.js +++ b/Control/test/public/page-new-environment-mocha.js @@ -15,6 +15,7 @@ /* eslint-disable max-len */ const assert = require('assert'); const test = require('../mocha-index'); +const {waitForTimeout} = require('../utils/puppeteerUtils.js'); let url; let page; @@ -173,6 +174,7 @@ describe('`pageNewEnvironment` test-suite', async () => { }); it('should have error of missing revisions for this repository', async () => { + await waitForTimeout(1000); const errorMessage = await page.locator('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div:nth-child(2) > div > div > div > div > div:nth-child(2) > div > div') .setTimeout(1000) .map((element) => element.innerText) diff --git a/Control/test/utils/puppeteerUtils.js b/Control/test/utils/puppeteerUtils.js new file mode 100644 index 000000000..f1e69cdc3 --- /dev/null +++ b/Control/test/utils/puppeteerUtils.js @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +/** + * Wait for a specific timeout period. To be used in puppeteer tests as the `waitForFunction` and `waitForNetworkIdle` are not reliable + * Puppeteer used to have a built-in `waitForTimeout` function, but it was removed in later versions. + * @param {number} timeout - The time to wait in milliseconds. + * @returns {Promise} - A promise that resolves after the specified timeout. + */ +export const waitForTimeout = (timeout) => new Promise((res) => setTimeout(res, timeout)); From eb1a4091780bf1f16bfe17eb95183cc14913a706 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 12:07:09 +0200 Subject: [PATCH 36/38] Remove deprecates tests --- Control/test/mocha-index.js | 1 - .../test/public/page-configuration-mocha.js | 179 ------------------ 2 files changed, 180 deletions(-) delete mode 100644 Control/test/public/page-configuration-mocha.js diff --git a/Control/test/mocha-index.js b/Control/test/mocha-index.js index 3ae66739a..d6c44ac62 100644 --- a/Control/test/mocha-index.js +++ b/Control/test/mocha-index.js @@ -165,7 +165,6 @@ describe('Control', function() { require('./public/page-about-mocha'); require('./public/page-environment-mocha'); require('./public/page-environments-mocha'); - require('./public/page-configuration-mocha'); require('./public/page-tasks-mocha'); require('./public/page-hardware-mocha'); require('./public/page-lock-mocha'); diff --git a/Control/test/public/page-configuration-mocha.js b/Control/test/public/page-configuration-mocha.js deleted file mode 100644 index 4714bf1fa..000000000 --- a/Control/test/public/page-configuration-mocha.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. -*/ - -const assert = require('assert'); -const test = require('../mocha-index'); - -describe('`pageConfiguration` test-suite', async () => { - let url; - let page; - - before(async () => { - url = test.helpers.url; - page = test.page; - }); - - describe('loading page with no consul connection', () => { - it('should load configuration page', async () => { - await page.goto(url + '?page=configuration', {waitUntil: 'networkidle0'}); - const location = await page.evaluate(() => window.location); - assert.strictEqual(location.search, '?page=configuration'); - }); - it('should display an error icon and message due to bad connection of consul', async () => { - const [errorIcon, errorMessage] = await page.evaluate(() => { - const errorIcon = document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > span').classList; - const errorMessage = document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > p').innerText; - return [errorIcon, errorMessage]; - }); - assert.deepStrictEqual(errorIcon, {0: 'errorPage'}); - assert.strictEqual(errorMessage, 'Request to server failed (502 Bad Gateway): connect ECONNREFUSED 127.0.0.1:8550'); - }); - }); - - describe('loading page with consul returning 404 due to missing key', () => { - before(async () => { - await page.setRequestInterception(true); - page.on('request', keyNotFound); - }); - - after(async () => { - await page.setRequestInterception(false); - await page.removeListener('request', keyNotFound); - }); - - it('should load configuration page', async () => { - await page.goto(url + '?page=configuration', {waitUntil: 'networkidle0'}); - const location = await page.evaluate(() => window.location); - assert.strictEqual(location.search, '?page=configuration'); - }); - - it('should display an error icon and message due to key not found', async () => { - const [errorIcon, errorMessage] = await page.evaluate(() => { - const errorIcon = document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > span').classList; - const errorMessage = document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > p').innerText; - return [errorIcon, errorMessage]; - }); - assert.deepStrictEqual(errorIcon, {0: 'errorPage'}); - assert.strictEqual(errorMessage, 'Request to server failed (404 Not Found): Could not find any Readout Cards by key test/o2/hardware/flps'); - }); - }); - - describe('loading page with consul returning 200 but no ReadoutCards', () => { - before(async () => { - await page.setRequestInterception(true); - page.on('request', readoutCardsEmpty); - }); - - after(async () => { - await page.setRequestInterception(false); - await page.removeListener('request', readoutCardsEmpty); - }); - - it('should load configuration page', async () => { - await page.goto(url + '?page=configuration', {waitUntil: 'networkidle0'}); - const location = await page.evaluate(() => window.location); - assert.strictEqual(location.search, '?page=configuration'); - }); - - it('should contain an action panel', async () => { - const tableMessage = await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > div > h4').innerText); - assert.strictEqual(tableMessage, 'Action:'); - }); - - it('should contain a table with a single unified row due to no ReadoutCards', async () => { - const tableMessage = await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div:nth-child(3) > table > tbody > tr > td').innerText); - assert.strictEqual(tableMessage, 'No data found'); - }); - }); - - describe('loading page with consul returning 200 and a list of ReadoutCards', () => { - before(async () => { - await page.setRequestInterception(true); - page.on('request', readoutCardList); - }); - - after(async () => { - await page.setRequestInterception(false); - await page.removeListener('request', readoutCardList); - }); - - it('should load configuration page', async () => { - await page.goto(url + '?page=configuration', {waitUntil: 'networkidle0'}); - const location = await page.evaluate(() => window.location); - assert.strictEqual(location.search, '?page=configuration'); - }); - - it('should contain an action panel', async () => { - const tableMessage = await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > div > h4').innerText); - assert.strictEqual(tableMessage, 'Action:'); - }); - }); -}); - -/** - * Method intercept consul request and return 404 - * @param {Request} request - */ -function keyNotFound(request) { - if (request.url().includes('/api/consul/crus')) { - request.respond({status: 404, contentType: 'application/json', body: JSON.stringify({message: 'Could not find any Readout Cards by key test/o2/hardware/flps'})}); - } else { - request.continue(); - } -} - -/** - * Method intercept consul request and return 200 - * @param {Request} request - */ -function readoutCardsEmpty(request) { - if (request.url().includes('/api/consul/crus')) { - request.respond({status: 200, contentType: 'application/json', body: JSON.stringify({})}); - } else { - request.continue(); - } -} - -/** - * Method intercept consul request and return 200 - * @param {Request} request - */ -function readoutCardList(request) { - if (request.url().includes('/api/consul/crus')) { - request.respond({status: 200, contentType: 'application/json', body: JSON.stringify(readoutCards)}); - } else { - request.continue(); - } -} - -const readoutCards = { - hostOne: { - 0: { - type: 'CRORC', - pciAddress: 'd8: 00.0', - serial: 30176, - endpoint: 0, - numa: 1, - firmware: 'alpha', - }, - 1: { - type: 'CRORC', - pciAddress: 'd8: 00.0', - serial: 30176, - endpoint: 0, - numa: 1, - firmware: 'alpha', - } - } -}; From f0df2102b541d7b12208c41810dec566d40f2c14 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 12:09:48 +0200 Subject: [PATCH 37/38] Use waitfor shutdown as well --- Control/test/public/page-environment-mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index 5bd141814..770408c41 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -205,7 +205,7 @@ describe('`pageEnvironment` test-suite', async () => { }); await page.waitForSelector('#buttonToSHUTDOWN', {timeout: 5000}); await page.evaluate(() => document.querySelector('#buttonToSHUTDOWN').click()); - await page.waitForNetworkIdle(); + await waitForTimeout(1000); const location = await page.evaluate(() => window.location); assert.strictEqual(location.search, '?page=environments'); assert.ok(calls['destroyEnvironment']); From 548571d8b0b30ab0a72cbe1732f4f68f6c3599da Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 18 Aug 2025 12:28:19 +0200 Subject: [PATCH 38/38] Add docs about env deployments --- Control/README.md | 2 ++ .../docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md | 28 +++++++++++++++++++ .../common/environmentTransitionType.enum.js | 3 +- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Control/docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md diff --git a/Control/README.md b/Control/README.md index 816bb55ad..9bfe68f09 100644 --- a/Control/README.md +++ b/Control/README.md @@ -7,6 +7,7 @@ - [Description](#description) - [Requirements](#requirements) - [Installation](#installation) + - [Business logic for Developers to know](#business-logic-for-developers-to-know) - [Configuration](#configuration) - [O2Control gRPC](#o2control-grpc) - [Apricot gRPC](#apricot-grpc) @@ -53,6 +54,7 @@ It communicates with [Control agent](https://github.com/AliceO2Group/Control) ov 6. Start web app: `npm start` 7. Open browser and navigate to http://localhost:8080 +## [Business logic for Developers to know](./docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md) ## Configuration ### O2Control gRPC * `hostname` - gRPC hostname diff --git a/Control/docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md b/Control/docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md new file mode 100644 index 000000000..19a9cb65d --- /dev/null +++ b/Control/docs/BUSINESS_FOR_DEVELOPER_TO_KNOW.md @@ -0,0 +1,28 @@ +# Business logic for developers to know + +ALICE's Experiment Control System ([ECS](https://github.com/AliceO2Group/Control)) uses entities named environments to enable users to take data in ALICE. These environments are a set of software and hardware configurations used for preparation and during data-taking operations. + +## Environment States and Transitions +Environments can transition ([link](https://github.com/AliceO2Group/Control/blob/master/core/protos/o2control.proto#L228)) between multiple states. These states can be: +- state machines - [link](https://github.com/AliceO2Group/Control/blob/master/core/environment/environment.go#L153-L160) +- virtual states (states that are not real but used for software logic such as DESTROY or PENDING) + +Thus, for the GUI to provide a good user experience, the ECS GUI introduces a few logic items that are to be known when developing for ECS GUI: + +## Deployment and ECS GUI private attributes +A deployment is a set of operations that prepares hardware and software for data taking operations. A deployment is a definition in ECS GUI that covers an environment first transition from: +- `PENDING` (virtual state) -> `STANDBY` -> `DEPLOY` -> `CONFIGURED` + +For this sequence of actions, ECS GUI assigns a private attribute `isDeploying` and `deploymentError` to the environment so that it can display to the User all the information needed. +- `isDeploying` set to: + - `true` - only by: + - the response of the `NewEnvironmentAsync` + - the first event from ECS via Kafka of state `PENDING` + - `false` - only if: + - an event with state `CONFIGURED` and message `'transition completed successfully'` is received + - an event with an error message is received during when `isDeploying` is `true` already (TBD) +- `deploymentError` is updated every time there is an event with an error (TBD) + +These attributes allow the GUI to display all necessary deployment information to the user. + +If a deployment fails, the ECS will remove it from the active environments list. Thus, the GUI uses `deploymentError` to make sure the environment is kept in-memory until the user acknowledges the error. diff --git a/Control/lib/common/environmentTransitionType.enum.js b/Control/lib/common/environmentTransitionType.enum.js index 823684040..2ee446c00 100644 --- a/Control/lib/common/environmentTransitionType.enum.js +++ b/Control/lib/common/environmentTransitionType.enum.js @@ -13,7 +13,8 @@ */ /** - * Available environment transitions as per: https://github.com/AliceO2Group/Control/blob/master/core/protos/o2control.proto#L228 + * Available environment transitions as per: + * @link https://github.com/AliceO2Group/Control/blob/master/core/protos/o2control.proto#L228 */ const EnvironmentTransitionType = Object.freeze({ NOOP: 'NOOP',