diff --git a/Control/docs/LOCKS.md b/Control/docs/LOCKS.md index 958199e5f..88e30cd02 100644 --- a/Control/docs/LOCKS.md +++ b/Control/docs/LOCKS.md @@ -37,7 +37,7 @@ Acquires a lock on a detector for the current user. ### Release Lock -Releases a lock on a detector. +Releases a lock on a detector. If the detector is currently reported as `Active` by ECS, the release lock button will prompt the user to confirm the action is indeed correct. **Parameters**: - `detectorId`: The detector identifier or `ALL` to release all locks (excluding TST) diff --git a/Control/public/common/enums/DetectorState.enum.js b/Control/public/common/enums/DetectorState.enum.js index c2a4d1630..76daf4d22 100644 --- a/Control/public/common/enums/DetectorState.enum.js +++ b/Control/public/common/enums/DetectorState.enum.js @@ -19,6 +19,7 @@ export const DetectorState = Object.freeze({ UNDEFINED: 'UNDEFINED', // GUI initial set state NULL_STATE: 'NULL_STATE', + ACTIVE: 'ACTIVE', // Custom GUI state; Detector is active in a run, i.e. it is taking data READY: 'READY', RUN_OK: 'RUN_OK', RUN_FAILURE: 'RUN_FAILURE', @@ -45,6 +46,7 @@ export const DetectorState = Object.freeze({ export const DetectorStateStyle = Object.freeze({ UNDEFINED: '', NULL_STATE: '', + ACTIVE: 'warning', READY: 'bg-primary white', RUN_OK: 'bg-success white', RUN_FAILURE: 'bg-danger white', diff --git a/Control/public/lock/lockButton.js b/Control/public/lock/lockButton.js index 6d41935de..76bc69bc6 100644 --- a/Control/public/lock/lockButton.js +++ b/Control/public/lock/lockButton.js @@ -25,8 +25,10 @@ import {DetectorLockAction} from './../common/enums/DetectorLockAction.enum.js'; * @param {LockModel} lockModel - model of the lock state and actions * @param {String} detector - detector name * @param {Object} lockState - lock state of the detector + * @param {Boolean} isIcon - whether to render as an icon or a button + * @param {Boolean} [isActive = false] - whether the detector is active */ -export const detectorLockButton = (lockModel, detector, lockState, isIcon = false) => { +export const detectorLockButton = (lockModel, detector, lockState, isIcon = false, isActive = false) => { const isDetectorLockTaken = lockModel.isLocked(detector); let detectorLockHandler = null; @@ -35,7 +37,14 @@ export const detectorLockButton = (lockModel, detector, lockState, isIcon = fals if (isDetectorLockTaken) { if (lockModel.isLockedByCurrentUser(detector)) { detectorLockButtonClass = '.success'; - detectorLockHandler = () => lockModel.actionOnLock(detector, DetectorLockAction.RELEASE, false); + detectorLockHandler = () => { + if (isActive) { + confirm(`Are you sure you want to release the lock for an ACTIVE ${detector}?`) + && lockModel.actionOnLock(detector, DetectorLockAction.RELEASE, false); + } else { + lockModel.actionOnLock(detector, DetectorLockAction.RELEASE, false); + } + }; } else { detectorLockButtonClass = '.warning.disabled.disabled-item'; } diff --git a/Control/public/lock/lockPage.js b/Control/public/lock/lockPage.js index 98c7cdfda..61a42749f 100644 --- a/Control/public/lock/lockPage.js +++ b/Control/public/lock/lockPage.js @@ -22,8 +22,9 @@ import loading from './../common/loading.js'; import {DetectorLockAction} from '../common/enums/DetectorLockAction.enum.js'; import {isUserAllowedRole} from './../common/userRole.js'; import { getDetectorListWithTstAtEnd, TST_DETECTOR_NAME } from '../common/detectorUtils.js'; +import { DetectorState, DetectorStateStyle } from '../common/enums/DetectorState.enum.js'; -const LOCK_TABLE_HEADER_KEYS = ['Detector', 'Owner']; +const LOCK_TABLE_HEADER_KEYS = ['Detector', 'Owner', 'Active']; const DETECTOR_ALL = 'ALL'; /** @@ -50,7 +51,10 @@ export const content = (model) => { const { padlockState } = lockModel; return [ detectorHeader(model), - h('.text-center.scroll-y.absolute-fill', {style: 'top: 40px'}, [ + h('.text-center.scroll-y.absolute-fill', { + style: 'top: 40px', + oncreate: () => detectorsService.getActiveDetectors(), + }, [ padlockState.match({ NotAsked: () => null, Loading: () => loading(3), @@ -78,7 +82,7 @@ export const content = (model) => { ], ]), ], - detectorLocksTable(model, detectorsLocksState) + detectorLocksTable(model, detectorsLocksState, detectorsService.activeDetectors) ]) }) ]) @@ -89,9 +93,10 @@ export const content = (model) => { * Table with lock status details, buttons to lock them, and admin actions such us "Force release" * @param {Model} model - root model of the application * @param {Object} detectorsLockState - state of the detectors lock + * @param {RemoteData} activeDetectorsRemote - remote data with the list of active detectors * @return {vnode} */ -const detectorLocksTable = (model, detectorLocksState) => { +const detectorLocksTable = (model, detectorLocksState, activeDetectorsRemote) => { const { detectors: detectorsService, lock: lockModel } = model; const isUserGlobal = isUserAllowedRole(ROLES.Global); const detectorKeysWithTstLast = getDetectorListWithTstAtEnd(Object.keys(detectorLocksState)); @@ -104,10 +109,14 @@ const detectorLocksTable = (model, detectorLocksState) => { return (isUserGlobal && isSelectedDetectorViewGlobalOrCurrent) || isUserAllowedDetector; }) .map((detectorName) => { + const detectorActivityState = _getDetectorState(activeDetectorsRemote, detectorName); if (detectorName.toLocaleUpperCase().includes(TST_DETECTOR_NAME)) { - return [emptyRowSeparator(), detectorLockRow(lockModel, detectorName, detectorLocksState[detectorName])]; + return [ + emptyRowSeparator(), + detectorLockRow(lockModel, detectorName, detectorLocksState[detectorName], detectorActivityState) + ]; } else { - return detectorLockRow(lockModel, detectorName, detectorLocksState[detectorName]) + return detectorLockRow(lockModel, detectorName, detectorLocksState[detectorName], detectorActivityState) } }); return h('table.table.table-sm', @@ -121,7 +130,7 @@ const detectorLocksTable = (model, detectorLocksState) => { detectorRows.length > 0 ? detectorRows : h('tr', - h('td.ph2.warning', {colspan: 3}, [ + h('td.ph2.warning', { colspan: LOCK_TABLE_HEADER_KEYS.length } , [ 'Missing Role permissions needed for being allowed to own locks', ' If you have just started your shift, please allow a few minutes for the system ', 'to update before trying again or calling an FLP expert.' @@ -136,20 +145,24 @@ const detectorLocksTable = (model, detectorLocksState) => { * @param {LockModel} lockModel - model of the lock state and actions * @param {String} detector - detector name * @param {DetectorLock} lockState - state of the lock {owner: {fullName: String}, isLocked: Boolean + * @param {DetectorState} detectorActivityState - state of the detector as per AliECS (Active, Inactive, Unknown) * @return {vnode} */ -const detectorLockRow = (lockModel, detector, lockState) => { +const detectorLockRow = (lockModel, detector, lockState, detectorActivityState) => { const ownerName = lockState?.owner?.fullName || '-'; return h('tr', { id: `detector-row-${detector}`, }, [ h('td', h('.flex-row.g2.items-center.f5', [ - detectorLockButton(lockModel, detector, lockState), + detectorLockButton(lockModel, detector, lockState, false, detectorActivityState === DetectorState.ACTIVE), detector ]) ), h('td', ownerName), + h(`td`, { + class: DetectorStateStyle[detectorActivityState] + }, detectorActivityState), isUserAllowedRole(ROLES.Global) && h('td', [ detectorLockActionButton(lockModel, detector, lockState, DetectorLockAction.RELEASE, true, 'Force Release'), detectorLockActionButton(lockModel, detector, lockState, DetectorLockAction.TAKE, true, 'Force Take') @@ -161,4 +174,20 @@ const detectorLockRow = (lockModel, detector, lockState) => { * Empty table row separator vnode * @return {vnode} */ -const emptyRowSeparator = () => h('tr', h('td', {colspan: 3}, h('hr'))); +const emptyRowSeparator = () => h('tr', h('td', {colspan: LOCK_TABLE_HEADER_KEYS.length}, h('hr'))); + +/** + * Helper function to get the state of the detector (Active, Inactive, Unknown) based on the activeDetectorsRemote data + * @param {RemoteData} activeDetectorsRemote - remote data with the list of active detectors + * @param {String} detectorName - name of the detector to get the state for + * @return {String} state of the detector (Active, Inactive, Unknown) + */ +const _getDetectorState = (activeDetectorsRemote, detectorName) => { + return activeDetectorsRemote.match({ + NotAsked: () => DetectorState.UNDEFINED, + Loading: () => DetectorState.UNDEFINED, + Failure: () => DetectorState.ERROR, + Success: (activeDetectors) => + activeDetectors.includes(detectorName) ? DetectorState.ACTIVE : DetectorState.UNDEFINED + }) +} diff --git a/Control/public/services/DetectorService.js b/Control/public/services/DetectorService.js index a73bed424..a5840fe9a 100644 --- a/Control/public/services/DetectorService.js +++ b/Control/public/services/DetectorService.js @@ -37,6 +37,8 @@ export default class DetectorService extends Observable { this.hostsByDetectorRemote = RemoteData.notAsked(); this._selected = ''; + this._activeDetectors = RemoteData.notAsked(); + /** * @type {Object} */ @@ -55,9 +57,7 @@ export default class DetectorService extends Observable { this._listRemote = await this.getDetectorsAsRemoteData(this._listRemote, this); this.notify(); if (this._listRemote.isSuccess()) { - this.hostsByDetectorRemote = await this.getHostsByDetectorsAsRemoteData( - this.hostsByDetectorRemote, this._listRemote.payload, this - ); + this.hostsByDetectorRemote = await this.getHostsByDetectorsAsRemoteData(this.hostsByDetectorRemote, this); for (const detector of this._listRemote.payload) { this._availability[detector] = { pfrAvailability: DetectorState.UNDEFINED, @@ -107,7 +107,7 @@ export default class DetectorService extends Observable { * @param {Object} that * @returns {RemoteData} */ - async getHostsByDetectorsAsRemoteData(item, detectors, that) { + async getHostsByDetectorsAsRemoteData(item, that) { item = RemoteData.loading(); that.notify(); const {ok, result} = await this.model.loader.get(`/api/core/hostsByDetectors`); @@ -136,56 +136,20 @@ export default class DetectorService extends Observable { } /** - * Fetch detectors and return it as a remoteData object - * @param {RemoteData} item - */ - async getAndSetDetectorsAsRemoteData() { - this._listRemote = RemoteData.loading(); - this.notify(); - - const {result, ok} = await this.model.loader.get(`/api/core/detectors`); - if (!ok) { - this._listRemote = RemoteData.failure(result.message); - } else { - this._listRemote = RemoteData.success(result.detectors); - } - this.notify(); - } - - /** - * Fetch detectors and return it as a remoteData object - * @param {RemoteData} item + * Fetch active detectors from AliECS and update the corresponding RemoteData object + * @return {void} */ - async getActiveDetectorsAsRemoteData(item) { - item = RemoteData.loading(); + async getActiveDetectors() { + this._activeDetectors = RemoteData.loading(); this.notify(); - - const {result, ok} = await this.model.loader.post(`/api/GetActiveDetectors`); + const { result, ok } = await this.model.loader.post('/api/GetActiveDetectors', {}); if (!ok) { - item = RemoteData.failure(result.message); + this._activeDetectors = RemoteData.failure(result.message); } else { - item = RemoteData.success(result.detectors); + const { detectors = []} = result || {}; + this._activeDetectors = RemoteData.success(detectors); } this.notify(); - return item; - } - - /** - * Given a detector, it will return a RemoteData objects containing the result of query 'GetHostInventory' - * @param {String} detector - * @return {RemoteData} - */ - async getHostsForDetector(detector, item, that) { - item = RemoteData.loading(); - that.notify(); - const {result, ok} = await this.model.loader.post(`/api/GetHostInventory`, {detector}); - if (!ok) { - item = RemoteData.failure(result.message); - } else { - item = RemoteData.success(result.hosts); - } - that.notify(); - return item; } /** @@ -207,47 +171,6 @@ export default class DetectorService extends Observable { return this._listRemote; } - /** - * Method to return a RemoteData object containing list of detectors fetched from AliECS and their availability - * @param {boolean} [restrictToUser = true] - if the list should be restricted to user permissions only - * @param {RemoteData} item - item in which data should be loaded and notified - * @param {typeof Model} that - model that should be notified after a change in data fetching - * @returns {RemoteData>} - returns the state of the detectors - */ - async getDetectorsAvailabilityAsRemote(restrictToUser = true, item = RemoteData.notAsked(), that = this) { - item = RemoteData.loading(); - that.notify(); - - let {result: {detectors}, ok: detectorsOk} = await this.model.loader.get(`/api/core/detectors`); - const { - result: {detectors: activeDetectors}, - ok: detectorsActivityOk - } = await this.model.loader.post(`/api/GetActiveDetectors`); - const isLockDataOk = this.model.lock.padlockState.isSuccess(); - - if (detectorsOk && detectorsActivityOk && isLockDataOk) { - const padLock = this.model.lock.padlockState.payload; - if (restrictToUser && this.isSingleView()) { - detectors = detectors.filter((detector) => detector === this._selected); - } - /** - * @type {Array} - */ - const detectorsAvailability = detectors.map((detector) => ({ - name: detector, - isActive: activeDetectors.includes(detector), - isLockedBy: padLock.lockedBy[detector], - })); - item = RemoteData.success(detectorsAvailability); - that.notify(); - return item; - } else { - item = RemoteData.failure('Unable to fetch information on detectors state'); - that.notify(); - return item; - } - } - /** * Method to return a RemoteData object containing list of detectors fetched from AliECS * @deprecated as it should be using `getDetectorsAsRemote` instead @@ -295,6 +218,14 @@ export default class DetectorService extends Observable { return this._availability; } + /** + * Return an instance of the current active detectors as per AliECS + * @return {RemoteData>} + */ + get activeDetectors() { + return this._activeDetectors; + } + /** * Given a list of detectors, return if all are available for specified property (PFR/SOR) * @param {Array} detectors - list of detectors to check diff --git a/Control/public/workflow/panels/flps/FlpSelection.js b/Control/public/workflow/panels/flps/FlpSelection.js index b9a7d2cd9..e3b518887 100644 --- a/Control/public/workflow/panels/flps/FlpSelection.js +++ b/Control/public/workflow/panels/flps/FlpSelection.js @@ -58,26 +58,26 @@ export default class FlpSelection extends Observable { this.notify(); await this.getAndSetDetectors(); - /*if (this.workflow.model.detectors.isSingleView() - && this.activeDetectors.isSuccess() - && !this.activeDetectors.payload.detectors.includes(this.workflow.model.detectors.selected) - ) { - // if single view preselect detectors and hosts for users - this.toggleDetectorSelection(this.workflow.model.detectors.selected); - }*/ } /** * Method to request a list of detectors from AliECS and initialized the user form accordingly + * @return {Promise} */ async getAndSetDetectors() { this.detectors = this.workflow.model.detectors.listRemote; + await this.getActiveDetectors(); + } + /** + * Method to retrieve the detectors that are active as per AliECS + * @return {Promise} + */ + async getActiveDetectors() { this.activeDetectors = RemoteData.loading(); this.notify(); const {result, ok} = await this.workflow.model.loader.post('/api/GetActiveDetectors', {}); this.activeDetectors = ok ? RemoteData.success(result) : RemoteData.failure(result.message); - this.notify(); } diff --git a/Control/public/workflow/panels/flps/detectorsPanel.js b/Control/public/workflow/panels/flps/detectorsPanel.js index 898bd8c6a..764dfeedc 100644 --- a/Control/public/workflow/panels/flps/detectorsPanel.js +++ b/Control/public/workflow/panels/flps/detectorsPanel.js @@ -122,7 +122,7 @@ const detectorSelectionPanel = (model, name) => { id: `detector-selection-panel-${name}'`, }, [ h('.flex-row', [ - detectorLockButton(lockModel, name, lockState, true), + detectorLockButton(lockModel, name, lockState, true, isDetectorActive), h('a.menu-item.w-wrapped', { className, id: `detectorSelectionButtonFor${name}`,