Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Control/docs/LOCKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions Control/public/common/enums/DetectorState.enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
13 changes: 11 additions & 2 deletions Control/public/lock/lockButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
}
Expand Down
49 changes: 39 additions & 10 deletions Control/public/lock/lockPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand 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),
Expand Down Expand Up @@ -78,7 +82,7 @@ export const content = (model) => {
],
]),
],
detectorLocksTable(model, detectorsLocksState)
detectorLocksTable(model, detectorsLocksState, detectorsService.activeDetectors)
])
})
])
Expand All @@ -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<String, DetectorLock>} 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));
Expand All @@ -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',
Expand All @@ -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.'
Expand All @@ -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')
Expand All @@ -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
})
}
109 changes: 20 additions & 89 deletions Control/public/services/DetectorService.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export default class DetectorService extends Observable {
this.hostsByDetectorRemote = RemoteData.notAsked();
this._selected = '';

this._activeDetectors = RemoteData.notAsked();

/**
* @type {Object<String, Detector>}
*/
Expand All @@ -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,
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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<Array<DetectorAvailability>>} - 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<DetectorAvailability>}
*/
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
Expand Down Expand Up @@ -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<Array<String>>}
*/
get activeDetectors() {
return this._activeDetectors;
}

/**
* Given a list of detectors, return if all are available for specified property (PFR/SOR)
* @param {Array<String>} detectors - list of detectors to check
Expand Down
16 changes: 8 additions & 8 deletions Control/public/workflow/panels/flps/FlpSelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>}
*/
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<void>}
*/
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();
}

Expand Down
2 changes: 1 addition & 1 deletion Control/public/workflow/panels/flps/detectorsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down