diff --git a/Control/public/common/utils.js b/Control/public/common/utils.js index 1db1e0e02..80b1440c5 100644 --- a/Control/public/common/utils.js +++ b/Control/public/common/utils.js @@ -10,8 +10,6 @@ export default function parseObject(item, key) { return item.length; case 'version': return item.productName + ' v' + item.versionStr + '(revision ' + item.build + ')'; - case 'deploymentInfo': - return item.hostname; default: return JSON.stringify(item); } diff --git a/Control/public/environment/Environment.js b/Control/public/environment/Environment.js index f00e742fc..d7bbb9e28 100644 --- a/Control/public/environment/Environment.js +++ b/Control/public/environment/Environment.js @@ -55,11 +55,31 @@ export default class Environment extends Observable { this.notify(); return; } - this.item = RemoteData.success(result); + await result.environment.tasks.forEach((task) => { + this.task.getTaskById({taskId: task.taskId}); + }); + this.item = RemoteData.success(this.parseEnvResult(result)); this.itemControl = RemoteData.notAsked(); // because item has changed this.notify(); } + /** + * Method to remove and parse fields from environment result + * @param {JSON} result + * @return {JSON} + */ + parseEnvResult(result) { + result.environment.tasks.forEach((task) => { + const regex = new RegExp(`tasks/.*@`); + const tasksAndName = task.name.match(regex); + if (tasksAndName) { + task.name = tasksAndName[0].replace('tasks/', '').replace('@', ''); + } + }); + + return result; + } + /** * Control a remote environment, store action result into `itemControl` as RemoteData * @param {Object} body - See protobuf definition for properties diff --git a/Control/public/environment/Task.js b/Control/public/environment/Task.js index 58777202e..a70e5c3bc 100644 --- a/Control/public/environment/Task.js +++ b/Control/public/environment/Task.js @@ -13,56 +13,37 @@ export default class Task extends Observable { this.model = model; this.storage = new BrowserStorage('AliECS'); - this.remoteTasks = RemoteData.notAsked(); - this.openedTasks = []; + this.openedTasks = {}; + this.list = {}; // map of (taskId, RemoteData) } /** - * Load Task into list of tasks - * @param {JSON} body {taskId: } + * Toggle the view of a task by its id + * @param {string} taskId */ - async updateOpenedTasks(body) { - const indexOfSelectedTask = this.getIndexOfTask(body.taskId); - - if (indexOfSelectedTask >= 0) { // if Task is already opened then remove from list - this.openedTasks.splice(indexOfSelectedTask, 1); - } else { - const commandInfo = this.storage.getLocalItem(body.taskId); - if (commandInfo) { - this.openedTasks.push(commandInfo); - } else { - await this.getTaskById(body); - } - } - this.remoteTasks = RemoteData.success(this.openedTasks); + async toggleTaskView(taskId) { + this.openedTasks[taskId] = !this.openedTasks[taskId]; this.notify(); } - /** - * Method to retrieve index of a task in the existing opened task list - * @param {string} taskId - * @return {number} - */ - getIndexOfTask(taskId) { - return this.openedTasks.map((task) => task.taskId).indexOf(taskId); - } - /** * Method to make an HTTP Request to get details about a task by its Id * @param {JSON} body {taskId: string} */ async getTaskById(body) { - this.remoteTasks = RemoteData.loading(); + this.list[body.taskId] = RemoteData.loading(); + this.openedTasks[body.taskId] = false; this.notify(); const {result, ok} = await this.model.loader.post(`/api/GetTask`, body); - if (!ok) { - this.openedTasks.push({taskId: body.taskId, message: result.message}); + if (ok) { + this.list[body.taskId] = RemoteData.failure(result.message); } else { const commandInfo = this.parseTaskCommandInfo(result.task.commandInfo, body.taskId); - this.storage.setLocalItem(body.taskId, commandInfo); - this.openedTasks.push(commandInfo); + commandInfo.className = result.task.classInfo.name; + this.list[body.taskId] = RemoteData.success(commandInfo); } + this.notify(); } /** diff --git a/Control/public/environment/environmentPage.js b/Control/public/environment/environmentPage.js index 3bc1c5a55..b3a3a69ed 100644 --- a/Control/public/environment/environmentPage.js +++ b/Control/public/environment/environmentPage.js @@ -1,7 +1,6 @@ -import {h} from '/js/src/index.js'; +import {h, iconChevronBottom, iconChevronTop, iconCircleX} from '/js/src/index.js'; import pageLoading from '../common/pageLoading.js'; import pageError from '../common/pageError.js'; -import parseObject from './../common/utils.js'; import showTableItem from '../common/showTableItem.js'; /** * @file Page to show 1 environment (content and header) @@ -48,11 +47,13 @@ const showContent = (environment, item) => [ h('.m2.flex-row', { style: 'height: 10em;' - }, [ + }, + [ h('.grafana-font.m1.flex-column', { style: 'width: 15%;' - }, [ + }, + [ h('', {style: 'height:40%'}, 'Run Number'), h('', h('.badge.bg-success.white', @@ -72,22 +73,18 @@ const showContent = (environment, item) => [ ] ), showEnvDetailsTable(item), - h('.m2.p2', [ + h('.m2', [ h('h4', 'Tasks'), h('.flex-row.flex-grow', - h('.flex-grow', {}, - displayTableOfTasks(environment, item.tasks, [ - (event, item) => { - environment.task.updateOpenedTasks({taskId: item.taskId}); - }] - ) + h('.flex-grow', + showEnvTasksTable(environment, item.tasks) ) - ) + ), ]), ]; /** - * Method to display pltos from Graphana + * Method to display plots from Grafana * @param {Array} data * @return {vnode} */ @@ -124,7 +121,7 @@ const showEmbeddedGraphs = (data) => * @return {vnode} table view */ const showEnvDetailsTable = (item) => - h('.pv3.m2', + h('.m2.mv4.shadow-level1', h('table.table', [ h('tbody', [ h('tr', [ @@ -207,61 +204,64 @@ const controlButton = (buttonType, environment, item, label, type, stateToHide) ); /** - * Method to create and display a table with tasks + * Method to create and display a table with tasks details * @param {Object} environment - * @param {Array} list - * @param {Array} actions + * @param {Array} tasks * @return {vnode} */ -const displayTableOfTasks = (environment, list, actions) => h('.scroll-auto', [ - h('table.table.table-sm', [ - h('thead', [ +const showEnvTasksTable = (environment, tasks) => h('.scroll-auto.shadow-level1', [ + h('table.table.table-sm', {style: 'margin:0'}, [ + h('thead', h('tr', [ - list.length > 0 && Object.keys(list[0]).map( - (columnName) => h('th', {style: 'text-align:center'}, columnName)), - actions && h('th.text-center', {style: 'text-align:center'}, 'actions') + ['Name', 'Locked', 'Status', 'State', 'Host Name', 'Args', 'More'] + .map((header) => h('th', {style: 'text-align: center'}, header)) ] ) - ]), - h('tbody', list.map((item) => [h('tr', [ - Object.keys(item).map( - (columnName) => typeof item[columnName] === 'object' - ? h('td', parseObject(item[columnName], columnName)) - : h('td', - columnName === 'state' && { - class: (item[columnName] === 'RUNNING' ? - 'success' : (item[columnName] === 'CONFIGURED' ? 'warning' : '')), - style: 'font-weight: bold;' - }, - item[columnName] - ) - ), - actions && h('td', - h('button.btn.btn-primary', - { - onclick: (event) => actions[0](event, item) - }, - environment.task.getIndexOfTask(item.taskId) >= 0 ? 'Close' : 'More')), - ]), - environment.task.remoteTasks.match({ - NotAsked: () => null, - Loading: () => null, - Success: (data) => environment.task.getIndexOfTask(item.taskId) >= 0 - && displayTaskDetails(data.filter((task) => task.taskId === item.taskId)[0], Object.keys(list[0]).length), - Failure: (error) => pageError(error), - })])), - ] - ) + ), + h('tbody', [ + tasks.map((task) => [h('tr', [ + h('td', {style: 'text-align:left'}, task.name), + h('td', {style: 'text-align:center'}, task.locked), + h('td', {style: 'text-align:center'}, task.status), + h('td', { + class: (task.state === 'RUNNING' ? + 'success' : (task.state === 'CONFIGURED' ? 'warning' : '')), + style: 'font-weight: bold; text-align:center' + }, task.state), + h('td', {style: 'text-align:center'}, task.deploymentInfo.hostname), + environment.task.list[task.taskId] && environment.task.list[task.taskId].match({ + NotAsked: () => null, + Loading: () => h('td', {style: 'font-size: 0.25em;text-align:center'}, pageLoading()), + Success: (data) => h('td', {style: 'text-align:left'}, data.arguments), + Failure: (_error) => h('td', {style: 'text-align:center', title: 'Could not load arguments'}, iconCircleX()), + }), + h('td', {style: 'text-align:center'}, + h('button.btn.btn-default', { + title: 'More Details', + onclick: () => environment.task.toggleTaskView(task.taskId), + }, environment.task.openedTasks[task.taskId] ? iconChevronTop() : iconChevronBottom()) + ), + ]), + environment.task.openedTasks[task.taskId] && environment.task.list[task.taskId] && + addTaskDetailsTable(environment, task), + ]), + ]) + ]) ]); /** - * Method to display an expandable table with details about a selected task - * @param {Object} task - * @param {number} colSpan + * Method to display an expandable table with details about a selected task if request was successful + * Otherwise display loading or error message + * @param {Object} environment + * @param {JSON} task * @return {vnode} */ -const displayTaskDetails = (task, colSpan) => - h('tr.m5', - h('td', {colspan: ++colSpan}, showTableItem(task)) - ); +const addTaskDetailsTable = (environment, task) => h('tr', environment.task.list[task.taskId].match({ + NotAsked: () => null, + Loading: () => h('td.shadow-level3.m5', {style: 'font-size: 0.25em; text-align: center;', colspan: 7}, pageLoading()), + Success: (data) => h('td', {colspan: 7}, showTableItem(data)), + Failure: (_error) => h('td.shadow-level3.m5', + {style: 'text-align: center;', colspan: 7, title: 'Could not load arguments'}, + [iconCircleX(), ' ', _error]), +})); diff --git a/Control/test/public/page-environment-mocha.js b/Control/test/public/page-environment-mocha.js index df752f80e..8fd958548 100644 --- a/Control/test/public/page-environment-mocha.js +++ b/Control/test/public/page-environment-mocha.js @@ -179,4 +179,35 @@ describe('`pageEnvironment` test-suite', async () => { const lockButton = await page.evaluate(() => document.querySelector('body > div:nth-child(2) > div > div > button').title); assert.deepStrictEqual(lockButton, 'Lock is free'); }); + + describe('Utils within Environment class', async () => { + it('should replace task name if regex is matched', async () => { + const tagModified = await page.evaluate(() => { + const result = {}; + result['environment'] = {}; + result.environment.tasks = [{name: 'github.com/AliceO2Group/ControlWorkflows/tasks/readout@4726d80d4bf43fe65133d20d83831752049c8dbe#54c7c9b0-ffbe-11e9-97fb-02163e018d4a'}]; + return window.model.environment.parseEnvResult(result).environment.tasks[0].name; + }); + assert.strictEqual(tagModified, 'readout'); + }); + it('should not replace task name due to regex not matching the name (missing tasks/ group)', async () => { + const tagModified = await page.evaluate(() => { + const result = {}; + result['environment'] = {}; + result.environment.tasks = [{name: 'github.com/AliceO2Group/ControlWorkflows/readout@4726d80d4bf43fe65133d20d83831752049c8dbe#54c7c9b0-ffbe-11e9-97fb-02163e018d4a'}]; + return window.model.environment.parseEnvResult(result).environment.tasks[0].name; + }); + assert.strictEqual(tagModified, 'github.com/AliceO2Group/ControlWorkflows/readout@4726d80d4bf43fe65133d20d83831752049c8dbe#54c7c9b0-ffbe-11e9-97fb-02163e018d4a'); + }); + + it('should not replace task name due to regex not matching the name (missing @ character)', async () => { + const tagModified = await page.evaluate(() => { + const result = {}; + result['environment'] = {}; + result.environment.tasks = [{name: 'github.com/AliceO2Group/ControlWorkflows/tasks/readout4726d80d4bf43fe65133d20d83831752049c8dbe#54c7c9b0-ffbe-11e9-97fb-02163e018d4a'}]; + return window.model.environment.parseEnvResult(result).environment.tasks[0].name; + }); + assert.strictEqual(tagModified, 'github.com/AliceO2Group/ControlWorkflows/tasks/readout4726d80d4bf43fe65133d20d83831752049c8dbe#54c7c9b0-ffbe-11e9-97fb-02163e018d4a'); + }); + }); });