From bcc0a42f73b53f88717e84cf43006156896d2e77 Mon Sep 17 00:00:00 2001 From: "Vyacheslav.Sviridov" Date: Sun, 26 Jun 2022 21:09:48 +0600 Subject: [PATCH] bulk import creation --- bin/enm-cli.js | 8 +- .../AutoProvisioning/AutoProvisioning.js | 10 +- .../AutoProvisioning/inputHandler.js | 21 +-- lib/applications/AutoProvisioning/nodes.js | 64 +++---- lib/applications/AutoProvisioning/projects.js | 22 ++- lib/applications/BulkImport/BulkImport.js | 81 ++++++++ lib/applications/BulkImport/inputHandler.js | 105 +++++++++++ lib/applications/BulkImport/job.js | 68 +++++++ lib/applications/BulkImport/jobs.js | 173 ++++++++++++++++++ lib/applications/BulkImport/operations.js | 68 +++++++ .../TopologyBrowser/TopologyBrowser.js | 2 +- .../TopologyBrowser/commands/get.js | 6 +- .../TopologyBrowser/commands/goToFdn.js | 2 + .../commands/nextAttributes.js | 15 +- .../TopologyBrowser/commands/nextObjects.js | 14 +- .../TopologyBrowser/inputHandler.js | 14 +- lib/components/ENM.js | 4 +- lib/components/SpinnerWithCounter.js | 5 + util/createNext.js | 15 +- util/logAttributes.js | 2 +- util/logError.js | 12 ++ util/logJob.js | 113 ++++++++++++ util/logOperation.js | 64 +++++++ util/logProject.js | 2 +- util/validation.js | 9 + 25 files changed, 813 insertions(+), 86 deletions(-) create mode 100755 lib/applications/BulkImport/BulkImport.js create mode 100755 lib/applications/BulkImport/inputHandler.js create mode 100755 lib/applications/BulkImport/job.js create mode 100755 lib/applications/BulkImport/jobs.js create mode 100755 lib/applications/BulkImport/operations.js create mode 100755 util/logJob.js create mode 100755 util/logOperation.js diff --git a/bin/enm-cli.js b/bin/enm-cli.js index 2159ba5..489de1a 100755 --- a/bin/enm-cli.js +++ b/bin/enm-cli.js @@ -11,6 +11,7 @@ require('dotenv').config({ path: [...__dirname.split(path.sep).slice(0,-1), '.en const AutoProvisioning = require('../lib/applications/AutoProvisioning/AutoProvisioning') const TopologyBrowser = require('../lib/applications/TopologyBrowser/TopologyBrowser') +const BulkImport = require('../lib/applications/BulkImport/BulkImport') const logError = require('../util/logError') @@ -30,7 +31,12 @@ const applications = [ id: 'prvn', appClass: AutoProvisioning, name: 'Auto Provisioning', - } + }, + { + id: 'bulk', + appClass: BulkImport, + name: 'Bulk Import', + }, ] const appIds = applications.map(item => item.id) diff --git a/lib/applications/AutoProvisioning/AutoProvisioning.js b/lib/applications/AutoProvisioning/AutoProvisioning.js index b1af52a..c7d11c9 100755 --- a/lib/applications/AutoProvisioning/AutoProvisioning.js +++ b/lib/applications/AutoProvisioning/AutoProvisioning.js @@ -20,9 +20,11 @@ class AutoProvisioning extends ENM { this.appUrl = '/auto-provisioning/v1' this.projects = null - this.projectIndex = -1 + this.projectId = null + // this.projectIndex = -1 this.nodes = null - this.nodeIndex = -1 + // this.nodeIndex = -1 + this.nodeId = null this.prompt = '' this.help = 'No results...' } @@ -43,6 +45,10 @@ class AutoProvisioning extends ENM { await deleteProject.call(this) } + async deleteNode() { + // await deleteNode.call(this) + } + async getNode() { return await getNode.call(this) } diff --git a/lib/applications/AutoProvisioning/inputHandler.js b/lib/applications/AutoProvisioning/inputHandler.js index f572344..61f39d2 100755 --- a/lib/applications/AutoProvisioning/inputHandler.js +++ b/lib/applications/AutoProvisioning/inputHandler.js @@ -9,22 +9,19 @@ inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')) async function commandOther(prvn, command) { - const choosedIndex = prvn.choices.indexOf(command) - if (choosedIndex !== -1) { - if (prvn.nodes) { - prvn.nodeIndex = choosedIndex - prvn.getNode() - } else { - prvn.projectIndex = choosedIndex - await prvn.getProjectData() - } + if (prvn.nodes) { + prvn.nodeId = command + prvn.getNode() + } else { + prvn.projectId = command + await prvn.getProjectData() } } async function handleCommand(prvn, command) { - const [, cmd] = command.match(/\[(\w+)\]/) || [, command] - switch (cmd) { + // const [, cmd] = command.match(/\[(\w+)\]/) || [, command] + switch (command) { case 'exit': prvn.prompt = '' @@ -36,7 +33,7 @@ async function handleCommand(prvn, command) { prvn.nodeIndex ? await prvn.getProjects() : await prvn.getProjectData() break case 'delete': - prvn.nodeIndex ? prvn.deleteNode() : await prvn.deleteProject() + prvn.nodeIndex ? await prvn.deleteNode() : await prvn.deleteProject() break case 'status': diff --git a/lib/applications/AutoProvisioning/nodes.js b/lib/applications/AutoProvisioning/nodes.js index 3c859b6..d06a147 100755 --- a/lib/applications/AutoProvisioning/nodes.js +++ b/lib/applications/AutoProvisioning/nodes.js @@ -30,22 +30,22 @@ const nodeCommandsHelp = [ async function getNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] this.commands = nodeCommands this.help = nodeCommandsHelp.join('\n ') this.choices = [] // this.nodes.filter(item => item !== nodeId) - this.prompt = `${projectId} (${nodeId}) ` + this.prompt = `${this.projectId} (${this.nodeId}) ` } async function getNodeStatus() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] const axiosConfig = { - text: `Getting ${nodeId}'s status...`, + text: `Getting ${this.nodeId}'s status...`, method: 'get', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}` + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}` } const { data: { statusEntries } } = await this.httpClient.request(axiosConfig) logNodeStatus(statusEntries) @@ -53,12 +53,12 @@ async function getNodeStatus() { async function getNodeProperties() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] const axiosConfig = { - text: `Getting ${nodeId}'s properties...`, + text: `Getting ${this.nodeId}'s properties...`, method: 'get', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}?filter=properties` + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}?filter=properties` } const { data: { attributes, attributeGroups } } = await this.httpClient.request(axiosConfig) logNodeProperties(attributes, attributeGroups) @@ -75,12 +75,12 @@ async function bindNode() { validate: input => isValidHardwareId(input), } ]) - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] const axiosConfig = { - text: `Binding ${hardwareId} to ${nodeId}...`, + text: `Binding ${hardwareId} to ${this.nodeId}...`, method: 'put', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/bind`, + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}/actions/bind`, data: { hardwareId, }, @@ -91,12 +91,12 @@ async function bindNode() { async function cancelNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] const axiosConfig = { - text: `Canceling ${nodeId}...`, + text: `Canceling ${this.nodeId}...`, method: 'post', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/cancel`, + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}/actions/cancel`, } const { statusText } = await this.httpClient.request(axiosConfig) console.log(chalk.bgGreen(`Canceling ${statusText}`)) @@ -104,12 +104,12 @@ async function cancelNode() { async function resumeNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] const axiosConfig = { - text: `Resuming ${nodeId}...`, + text: `Resuming ${this.nodeId}...`, method: 'post', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/actions/resume`, + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}/actions/resume`, } const { statusText } = await this.httpClient.request(axiosConfig) console.log(chalk.bgGreen(`Resuming ${statusText}`)) @@ -117,8 +117,8 @@ async function resumeNode() { async function configurationsNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] const fileNameInput = await inquirer.prompt([{ type: 'file-tree-selection', name: 'nodeFile', @@ -127,9 +127,9 @@ async function configurationsNode() { const formData = new FormData() formData.append('file', fs.createReadStream(fileNameInput.nodeFile)) const axiosConfig = { - text: `Uploading ${nodeId} configuration to ${projectId}...`, + text: `Uploading ${this.nodeId} configuration to ${this.projectId}...`, method: 'put', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/configurations`, + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}/configurations`, headers: formData.getHeaders(), data: formData } @@ -139,13 +139,13 @@ async function configurationsNode() { async function siteinstallNode() { - const { projectId } = this.projects[this.projectIndex] - const nodeId = this.nodes[this.nodeIndex] - const saveFileName = path.join(process.cwd(), `Site_Install_${nodeId}.xml`) + // const { projectId } = this.projects[this.projectIndex] + // const nodeId = this.nodes[this.nodeIndex] + const saveFileName = path.join(process.cwd(), `Site_Install_${this.nodeId}.xml`) const axiosConfig = { - text: `Downloading site install file ${nodeId}...`, + text: `Downloading site install file ${this.nodeId}...`, method: 'get', - url: `${this.appUrl}/projects/${projectId}/nodes/${nodeId}/configurations/siteinstall`, + url: `${this.appUrl}/projects/${this.projectId}/nodes/${this.nodeId}/configurations/siteinstall`, } const { data } = await this.httpClient.request(axiosConfig) await fsPromises.writeFile(saveFileName, data) diff --git a/lib/applications/AutoProvisioning/projects.js b/lib/applications/AutoProvisioning/projects.js index 858f8eb..8b99c85 100755 --- a/lib/applications/AutoProvisioning/projects.js +++ b/lib/applications/AutoProvisioning/projects.js @@ -39,14 +39,14 @@ async function getProjects() { async function getProjectData() { - const { projectId } = this.projects[this.projectIndex] + // const { projectId } = this.projects[this.projectIndex] const axiosConfig = { - text: `Getting ${projectId}'s status...`, + text: `Getting ${this.projectId}'s status...`, method: 'get', - url: `${this.appUrl}/projects/${projectId}` + url: `${this.appUrl}/projects/${this.projectId}` } const { data: { nodeSummary } } = await this.httpClient.request(axiosConfig) - axiosConfig.text = `Getting ${projectId}'s properties...` + axiosConfig.text = `Getting ${this.projectId}'s properties...` axiosConfig.url += '?filter=properties' const { data } = await this.httpClient.request(axiosConfig) logProject(data, nodeSummary) @@ -55,7 +55,7 @@ async function getProjectData() { this.commands = projectCommands this.help = projectCommandsHelp.join('\n ') this.nodeIndex = -1 - this.prompt = projectId + this.prompt = this.projectId } @@ -81,11 +81,11 @@ async function newProject() { async function deleteProject() { - const { projectId } = this.projects[this.projectIndex] + // const { projectId } = this.projects[this.projectIndex] const axiosConfig = { - text: `Deleting project ${projectId}...`, + text: `Deleting project ${this.projectId}...`, method: 'delete', - url: `${this.appUrl}/projects/${projectId}` + url: `${this.appUrl}/projects/${this.projectId}` } const { statusText } = await this.httpClient.request(axiosConfig) console.log(chalk.bgGreen(`Delete ${statusText}`)) @@ -114,7 +114,11 @@ function projectsChoices(projects) { failed && failed + '⛔', ] const space = ' '.repeat(Math.max(14, projectId.length + 2) - projectId.length) - return `${chalk.bold(projectId)}${space}(${numberOfNodes}) ${statusList.filter(item => item).join(' ')}` + return { + value: projectId, + short: projectId, + name: `${chalk.bold(projectId)}${space}(${numberOfNodes}) ${statusList.filter(item => item).join(' ')}`, + } }) } diff --git a/lib/applications/BulkImport/BulkImport.js b/lib/applications/BulkImport/BulkImport.js new file mode 100755 index 0000000..d291b9b --- /dev/null +++ b/lib/applications/BulkImport/BulkImport.js @@ -0,0 +1,81 @@ +const chalk = require('chalk') + +const ENM = require('../../components/ENM') +const { + newJob, + getJobs, + pageJobs, +} = require('./jobs') +const { + job, + deleteJob, +} = require('./job') +const { getOperations } = require('./operations') +const inputHandler = require('./inputHandler') +const createNext = require('../../../util/createNext') + +class BulkImport extends ENM { + constructor(username, password, url) { + super(username, password, url) + + this.appUrl = '/bulk-configuration/v1/import-jobs/jobs' + this.jobs = null + this.jobsLinks = null + this.jobId = null + this.username = username + this.onlyMy = false + this.jobsOffset = 0 + this.jobsLimit = 50 + this.operations = null + this.operationsId = null + this.operationsOffset = 0 + this.operationsLimit = 50 + this.operationsLinks = null + this.prompt = '' + this.help = 'No results...' + } + + async getJobs() { + await getJobs.call(this) + } + + async pageJobs(page) { + await pageJobs.call(this, page) + } + + async deleteJob() { + await deleteJob.call(this) + } + + job(jobId) { + job.call(this, jobId) + } + + async my(){ + this.onlyMy = !this.onlyMy + console.log(chalk.yellowBright(`Show ${this.onlyMy ? 'only my' : 'all'} jobs❗`)) + this.jobsOffset = 0 + this.jobsLimit = 50 + await this.getJobs() + } + + async newJob() { + await newJob.call(this) + } + + async getOperations() { + await getOperations.call(this) + } + + async next(input) { + return createNext.call(this, input ? input : '') + } + + async inputHandler() { + await inputHandler.call(this) + } + +} + + +module.exports = BulkImport \ No newline at end of file diff --git a/lib/applications/BulkImport/inputHandler.js b/lib/applications/BulkImport/inputHandler.js new file mode 100755 index 0000000..f73bce5 --- /dev/null +++ b/lib/applications/BulkImport/inputHandler.js @@ -0,0 +1,105 @@ +const inquirer = require('inquirer') +const chalk = require('chalk') + +const logError = require('../../../util/logError') +const { logOperation } = require('../../../util/logOperation') +const { isEmpty } = require('../../../util/validation') + + +inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')) + + +async function commandOther(bulk, command) { + if (bulk.operations) { + const operation = bulk.operations.find(i => i.id === command) + logOperation(operation) + } + if (!bulk.jobId) { + bulk.job(command) + } +} + + +async function handleCommand(bulk, command) { + switch (command) { + case 'exit': + bulk.prompt = '' + break + case 'next': + case 'last': + case 'first': + case 'prev': + await bulk.pageJobs(command) + break + case 'operations': + case 'failures': + await bulk.getOperations() + break + case 'my': + await bulk.my() + break + case 'new': + await bulk.newJob() + break + case 'back': + bulk.operations ? await bulk.job() : await bulk.getJobs() + break + case 'delete': + await bulk.deleteJob() + break + + // case 'status': + // await bulk.getNodeStatus() + // break + // case 'properties': + // await bulk.getNodeProperties() + // break + // case 'bind': + // await bulk.bindNode() + // break + // case 'cancel': + // await bulk.cancelNode() + // break + // case 'resume': + // await bulk.resumeNode() + // break + // case 'configurations': + // await bulk.configurationsNode() + // break + // case 'siteinstall': + // await bulk.siteinstallNode() + // break + + default: + await commandOther(bulk, command) + } +} + + +async function inputHandler() { + await this.getJobs() + while (true) { + try { + const input = await inquirer.prompt([ + { + type: 'autocomplete', + name: 'command', + message: chalk.bold.blue(this.prompt), + pageSize: 10, + prefix: '', + suffix: chalk.bold.blue('>'), + validate: isEmpty, + source: async (answers, input) => await this.next(input), + emptyText: this.help, + } + ]) + await handleCommand(this, input.command) + if (!this.prompt) break + } catch (error) { + logError(error) + } + } +} + + +module.exports = inputHandler \ No newline at end of file diff --git a/lib/applications/BulkImport/job.js b/lib/applications/BulkImport/job.js new file mode 100755 index 0000000..0dc9dd3 --- /dev/null +++ b/lib/applications/BulkImport/job.js @@ -0,0 +1,68 @@ +const inquirer = require('inquirer') +const chalk = require('chalk') +const inquirerFileTreeSelection = require('inquirer-file-tree-selection-prompt') +const fs = require('fs') +const FormData = require('form-data') + +const { logJob } = require('../../../util/logJob') + +inquirer.registerPrompt('file-tree-selection', inquirerFileTreeSelection) + + +const jobCommands = ['operations', 'failures', 'delete', 'back', 'exit'] +const jobCommandsHelp = [ + '[delete] - Deletes import job.', + '[back] - Return to jobs.', + '[exit] - Exit this app.', + 'Or choose a node from list...', +] + + +function job(jobId) { + const job = this.jobs.find(job => job.id === jobId) + if (!job) { + throw new Error('Job not Found❗') + } + this.jobId = jobId + this.operations = null + logJob(job) + this.commands = [...jobCommands] + this.help = jobCommandsHelp.join('\n ') + this.choices = [] + this.prompt = job.name +} + + +async function deleteJob() { + if (!this.jobId) { + throw new Error('Job Is not selected❗') + } + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Still want to delete❓', + default: true, + }, + ]) + if (!confirm) return + const axiosConfig = { + text: `Deleting job ${this.jobId} ...`, + method: 'delete', + url: this.appUrl, + params: { + jobId: this.jobId, + } + } + const { data } = await this.httpClient.request(axiosConfig) + data.errors.forEach(err => { + console.log(`${err.type === 'INFO' ? chalk.green(err.message) : chalk.red(err.code + ': ' + err.message)}`) + }) + await this.getJobs() +} + + +module.exports = { + job, + deleteJob, +} \ No newline at end of file diff --git a/lib/applications/BulkImport/jobs.js b/lib/applications/BulkImport/jobs.js new file mode 100755 index 0000000..150e6b3 --- /dev/null +++ b/lib/applications/BulkImport/jobs.js @@ -0,0 +1,173 @@ +const chalk = require('chalk') +const inquirer = require('inquirer') + +const { statusPic } = require('../../../util/logJob') +const { isEmpty } = require('../../../util/validation') + +const jobsCommands = ['new', 'my', 'exit'] +const jobsCommandsHelp = [ + '[new] - Creates a new import job.', + '[exit] - Exit this app.', + 'Or choose a job from list...', +] + + +async function newJob() { + const { + name, + validationPolicy, + executionPolicy, + unsynchNodes, + executionOrder + } = await inquirer.prompt([ + { + type: 'input', + name: 'name', + message: 'Type a job\'s name:', + validate: isEmpty, + }, + { + type: 'checkbox', + name: 'validationPolicy', + message: 'Validation Policy:', + choices: [ + { + name: 'Skip MO Instance Validation', + value: 'no-instance-validation', + }, + { + name: 'Skip Node Based Validation', + value: 'no-node-based-validation', + checked: true, + }, + ], + }, + { + type: 'list', + name: 'executionPolicy', + message: 'Execution Error Handling:', + choices: [ + { + name: 'Stop', + value: 'stop-on-error', + }, + { + name: 'Skip to next node', + value: 'continue-on-error-node', + }, + { + name: 'Skip to next operation', + value: 'continue-on-error-operation', + }, + ], + default: 'stop-on-error', + }, + { + type: 'list', + name: 'unsynchNodes', + message: 'Unsynchronized Nodes Policy:', + choices: [ + { + name: 'Import skips operation execution on unsynchronized nodes. Recommended for faster performance.', + value: 'skip-unsync-nodes', + }, + { + name: 'Import will be agnostic towards node synchronization state, implying import attempts operation execution on unsychronized nodes as well.', + value: 'exec-unsync-nodes', + }, + ], + default: 'skip-unsync-nodes', + }, + { + type: 'list', + name: 'executionOrder', + message: 'Execution Order:', + choices: [ + { + name: 'Sequential', + value: 'sequential', + }, + { + name: 'Parallel', + value: 'parallel', + }, + ], + default: 'parallel', + when: input => !input.executionPolicy.includes('stop-on-error') + }, + ]) + const axiosConfig = { + text: 'Creating job...', + method: 'post', + url: this.appUrl, + data: { + name, + validationPolicy: validationPolicy.length + ? validationPolicy + : ['instance-validation', 'node-based-validation'], + executionPolicy: [ + executionPolicy, + unsynchNodes, + executionOrder ?? 'sequential', + ] + }, + } + const { data } = await this.httpClient.request(axiosConfig) + this.jobs.unshift(data) + this.job(data.id) +} + + +async function getJobs() { + const params =new URLSearchParams() + params.append('offset', this.jobsOffset) + params.append('limit', this.jobsLimit) + params.append('expand', 'summary') + params.append('expand', 'files') + if (this.onlyMy) { + params.append('createdBy', this.username) + } + const axiosConfig = { + text: 'Getting jobs...', + method: 'get', + url: this.appUrl, + params, + } + const { data } = await this.httpClient.request(axiosConfig) + this.jobs = data.jobs + this.jobId = null + this.jobsLinks = data._links + this.choices = this.jobs.map(item => ({ + name: `${statusPic(item.status)} ${item.name} ${chalk.dim(item.userId)}`, + value: item.id, + short: item.id + })) + this.commands = [...jobsCommands] + Object.values(this.jobsLinks).forEach(item => item.rel !== 'self' && this.commands.push(item.rel)) + this.help = jobsCommandsHelp.join('\n ') + this.prompt = `${data.totalCount} jobs (${this.jobsOffset}-${Math.min(this.jobsOffset + this.jobsLimit, data.totalCount)})` +} + + +function setJobsPagination(href) { + const url = new URL(href) + const searchParams = new URLSearchParams(url.search) + this.jobsLimit = +searchParams.get('limit') + this.jobsOffset = +searchParams.get('offset') +} + + +async function pageJobs(page) { + if (!this.jobsLinks[page]) { + throw new Error(`No ${page} jobs❗`) + } + setJobsPagination.call(this, this.jobsLinks[page].href) + await this.getJobs() +} + + +module.exports = { + newJob, + getJobs, + pageJobs, +} \ No newline at end of file diff --git a/lib/applications/BulkImport/operations.js b/lib/applications/BulkImport/operations.js new file mode 100755 index 0000000..e73d28e --- /dev/null +++ b/lib/applications/BulkImport/operations.js @@ -0,0 +1,68 @@ +const chalk = require("chalk") + +const operationsCommands = ['print', 'back', 'exit'] +const operationsCommandsHelp = [ + '[print] - Print all operations.', + '[back] - Return to jobs.', + '[exit] - Exit this app.', + 'Or choose a node from list...', +] + + +async function getOperations() { + const params =new URLSearchParams() + params.append('offset', this.operationsOffset) + params.append('limit', this.operationsLimit) + params.append('expand', 'attributes') + params.append('expand', 'persistentCurrentAttributes') + params.append('expand', 'failures') + params.append('expand', 'warnings') + // params.append('status', 'invalid') + // params.append('status', 'execution-error') + const axiosConfig = { + text: 'Getting operations...', + method: 'get', + url: `${this.appUrl}/${this.jobId}/operations/`, + params, + } + const { data } = await this.httpClient.request(axiosConfig) + this.operations = data.operations + this.operationsId = null + this.operationsLinks = data._links + this.choices = this.operations.map(item => ({ + name: `${item.status === 'EXECUTED' ? chalk.green(item.type) : chalk.red(item.type)} ${item.fdn.slice(-79)}`, + value: item.id, + short: item.id + })) + this.commands = [...operationsCommands] + Object.values(this.operationsLinks).forEach(item => { + if (item.rel !== 'self' || item.rel !== 'job') { + this.commands.push(item.rel) + } + }) + this.help = operationsCommandsHelp.join('\n ') + this.prompt = `${data.totalCount} operations (${this.operationsOffset}-${Math.min(this.operationsOffset + this.operationsLimit, data.totalCount)})` +} + + +function setOperationsPagination(href) { + const url = new URL(href) + const searchParams = new URLSearchParams(url.search) + this.operationsLimit = +searchParams.get('limit') + this.operationsOffset = +searchParams.get('offset') +} + + +async function pageOperations(page) { + if (!this.operationsLinks[page]) { + throw new Error(`No ${page} jobs❗`) + } + setOperationsPagination.call(this, this.operationsLinks[page].href) + await this.getOperations() +} + + +module.exports = { + getOperations, + pageOperations, +} \ No newline at end of file diff --git a/lib/applications/TopologyBrowser/TopologyBrowser.js b/lib/applications/TopologyBrowser/TopologyBrowser.js index f7a9a80..b9ab00b 100755 --- a/lib/applications/TopologyBrowser/TopologyBrowser.js +++ b/lib/applications/TopologyBrowser/TopologyBrowser.js @@ -110,7 +110,7 @@ class TopologyBrowser extends ENM { } check() { - logAttributes(this.fdn.replace(/\((\w+)\)/g, ''), this.configSet) + logAttributes(this.fdn, this.configSet) } home() { diff --git a/lib/applications/TopologyBrowser/commands/get.js b/lib/applications/TopologyBrowser/commands/get.js index 979676d..10ed81b 100755 --- a/lib/applications/TopologyBrowser/commands/get.js +++ b/lib/applications/TopologyBrowser/commands/get.js @@ -13,11 +13,11 @@ function get() { if (!attribute) { throw new Error(`Attribute not Found: ${this.attribute}`) } - const attributeData = this.attributesData.find(item => item.key === this.attribute) + const { constraints, defaultValue, type } = this.attributesData.find(item => item.key === this.attribute) logAttributes(this.fdn, [attribute]) - console.log(` ${chalk.green('Type: ') + attributeData['type']} ${attributeData['defaultValue'] ? chalk.yellow('Default: ') + attributeData['defaultValue'] : ''} + console.log(` ${chalk.green('Type: ') + type} ${defaultValue && chalk.yellow('Default: ') + defaultValue} `) - if (attributeData.constraints && attributeData.constraints.orderingConstraint) banner(attributeData) + if (constraints && constraints.orderingConstraint) banner(constraints) } module.exports = get diff --git a/lib/applications/TopologyBrowser/commands/goToFdn.js b/lib/applications/TopologyBrowser/commands/goToFdn.js index 6d6604a..3c4cb76 100755 --- a/lib/applications/TopologyBrowser/commands/goToFdn.js +++ b/lib/applications/TopologyBrowser/commands/goToFdn.js @@ -26,4 +26,6 @@ async function goToFdn(targetFdn) { } this.fdn = fdn } + + module.exports = goToFdn diff --git a/lib/applications/TopologyBrowser/commands/nextAttributes.js b/lib/applications/TopologyBrowser/commands/nextAttributes.js index 30ebedc..d3b3cbf 100755 --- a/lib/applications/TopologyBrowser/commands/nextAttributes.js +++ b/lib/applications/TopologyBrowser/commands/nextAttributes.js @@ -1,13 +1,18 @@ +const chalk = require('chalk') async function nextAttributes(input) { - const filter = input ? input : '' + // const filter = input ? input : '' this.commands = this.configCommands - .filter(item => item.toLowerCase().includes(filter.toLowerCase())) + // .filter(item => item.toLowerCase().includes(filter.toLowerCase())) this.choices = this.attributes - .map(item => item.key) - .filter(item => item.toLowerCase().includes(filter.toLowerCase())) - .sort((a, b) => a > b ? 1 : -1) + .map(({ key, value }) => ({ + value: key, + short: key, + name: `${key} ${chalk.dim(typeof value !== 'object' || value === null ? value : '...')}` + })) + // .filter(item => item.name.toLowerCase().includes(filter.toLowerCase())) + .sort((a, b) => a.value > b.value ? 1 : -1) } diff --git a/lib/applications/TopologyBrowser/commands/nextObjects.js b/lib/applications/TopologyBrowser/commands/nextObjects.js index 31f28a3..f7f034f 100755 --- a/lib/applications/TopologyBrowser/commands/nextObjects.js +++ b/lib/applications/TopologyBrowser/commands/nextObjects.js @@ -40,17 +40,21 @@ async function nextObjects(input) { this.poIds.push(this.currentPoId) await networkRequest.call(this) } - this.commands = otherCommands.filter(cmd => cmd.toLowerCase().includes(filter.toLowerCase())) + this.commands = otherCommands //.filter(cmd => cmd.toLowerCase().includes(filter.toLowerCase())) this.choices = this.childrens .map(child => { const st = getSyncStatus(child) const rt = getRadioAccessTechnology(child) const ne = child.neType ? ' ' + chalk.dim.gray(child.neType) : '' - return `${child.moType}=${child.moName}${st}${rt}${ne}` + return { + name: `${child.moType}=${child.moName}${st}${rt}${ne}`, + value: `${child.moType}=${child.moName}`, + short: `${child.moType}=${child.moName}`, + } }) - .filter(child => child.toLowerCase().includes(filter.toLowerCase())) - .concat(filter.startsWith('show') ? [filter] : []) - .concat(filter.startsWith('fdn') ? [filter] : []) + // .filter(child => child.name.toLowerCase().includes(filter.toLowerCase())) + .concat(filter.startsWith('show') ? [{ name: filter, value: filter, short: filter }] : []) + .concat(filter.startsWith('fdn') ? [{ name: filter, value: filter, short: filter }] : []) } diff --git a/lib/applications/TopologyBrowser/inputHandler.js b/lib/applications/TopologyBrowser/inputHandler.js index 4ea3e0a..dfbf764 100755 --- a/lib/applications/TopologyBrowser/inputHandler.js +++ b/lib/applications/TopologyBrowser/inputHandler.js @@ -21,15 +21,15 @@ function commandOther(tplg, command) { async function handleCommand(tplg, command) { - const [cmd, param] = command.split(/\s+/) - const cmdMatch = cmd.match(/\[(\w+)\]/) - const cmdName = cmdMatch ? cmdMatch[1] : cmd - switch (cmdName) { + const [cmd, param = ''] = command.split(/\s+/) + // const cmdMatch = cmd.match(/\[(\w+)\]/) + // const cmdName = cmdMatch ? cmdMatch[1] : cmd + switch (cmd) { case 'exit': tplg.fdn = '' break case 'show': - await tplg.show(param ? param.trim() : '') + await tplg.show(param) break case 'config': await tplg.config() @@ -65,7 +65,7 @@ async function handleCommand(tplg, command) { tplg.persistent() break case 'fdn': - await tplg.goToFdn(param ? param.trim() : '') + await tplg.goToFdn(param) break case 'alarms': await tplg.alarms() @@ -91,7 +91,7 @@ async function inputHandler() { message: chalk.blue(this.getPrompt()), pageSize: 10, prefix: '', - suffix: this.isConfig ? chalk.blue('#') : chalk.blue('>'), + suffix: chalk.blue(this.isConfig ? '#': '>'), validate: isEmpty, source: async (answers, input) => await this.next(input), emptyText: this.help, diff --git a/lib/components/ENM.js b/lib/components/ENM.js index e995025..0956263 100755 --- a/lib/components/ENM.js +++ b/lib/components/ENM.js @@ -4,8 +4,8 @@ class ENM { constructor(username, password, url) { this.logoutUrl = '/logout' this.loginUrl = `/login?IDToken1=${username}&IDToken2=${password}` - this.commands = null - this.choices = null + this.commands = [] + this.choices = [] this.httpClient = axiosHttpClient(url) } diff --git a/lib/components/SpinnerWithCounter.js b/lib/components/SpinnerWithCounter.js index c574cec..be5e68c 100755 --- a/lib/components/SpinnerWithCounter.js +++ b/lib/components/SpinnerWithCounter.js @@ -1,4 +1,8 @@ const ora = require('ora') +const { isXMas } = require('../../util/validation') + +const defaultSpinner = process.env.SPINNER || 'clock' + class SpinnerWithCounter { constructor() { @@ -9,6 +13,7 @@ class SpinnerWithCounter { start(text) { if (!this.spinner) { this.spinner = ora(text) + this.spinner.spinner = isXMas() ? 'christmas' : defaultSpinner this.spinner.start() } this.counter = ++this.counter diff --git a/util/createNext.js b/util/createNext.js index 2dbb01f..55829db 100755 --- a/util/createNext.js +++ b/util/createNext.js @@ -4,16 +4,21 @@ const inquirer = require('inquirer') function createNext(filter) { const separator = new inquirer.Separator() const commands = this.commands.filter(cmd => cmd.toLowerCase().includes(filter.toLowerCase())) - const choices = this.choices.filter(choice => choice.toLowerCase().includes(filter.toLowerCase())) + const choices = this.choices.filter(choice => choice.name.toLowerCase().includes(filter.toLowerCase())) let result = [ ...choices, separator, - ...commands.map(cmd => `[${cmd}]`), + ...commands.map(cmd => ({ + name: `[${cmd}]`, + value: cmd, + short: cmd, + })), separator, ] - if (result.includes([`${filter}`])) { - result = result.filter(item => item !== [`${filter}`]) - result.unshift([`${filter}`]) + const findFilter = result.find(item => item.value === filter) + if (findFilter) { + result = result.filter(item => item.value !== filter) + result.unshift(findFilter) } return result } diff --git a/util/logAttributes.js b/util/logAttributes.js index bce52c5..a920af5 100755 --- a/util/logAttributes.js +++ b/util/logAttributes.js @@ -26,7 +26,7 @@ function colorize(attributes) { function logAttributes(fdn, attributes) { const output = ` - ${chalk.yellow.bold('FDN')}: ${chalk.bold(fdn)} + ${chalk.yellow.bold('FDN')}: ${chalk.bold(fdn.replace(/\((\w+)\)/g, ''))} ${colorize(attributes)}` console.log(output) } diff --git a/util/logError.js b/util/logError.js index 5f05715..b80c54b 100755 --- a/util/logError.js +++ b/util/logError.js @@ -1,6 +1,7 @@ const chalk = require('chalk') function logError(err) { + // console.dir(err) try { if (!err.response) { const { @@ -39,6 +40,17 @@ function logError(err) { errorBody = data.body errorDetails = data.detail } + //bulk error + if (data.errors) { + errorBody = `Total Errors Count: ${data.totalCount}` + errorDetails = data.errors.map(err => { + return ` + ${err.type}: ${err.code} + ${err.message} + ${err.parameters && typeof err.parameters === 'object' ? JSON.stringify(err.parameters) : err.parameters} + ` + }) + } console.log(` ⚠️ ${chalk.bold.bgRed(errorTitle)} ${chalk.yellow(errorBody)}${errorDetails ? '\n' + errorDetails.toString() : ''} diff --git a/util/logJob.js b/util/logJob.js new file mode 100755 index 0000000..4894935 --- /dev/null +++ b/util/logJob.js @@ -0,0 +1,113 @@ +const chalk = require('chalk') + + +function statusPic(status) { + switch (status) { + case 'CREATED': + return '📄' + case 'PARSING': + return '📂' + case 'PARSED': + return '📁' + case 'VALIDATING': + return '🎛' + case 'VALIDATED': + return '📊' + case 'EXECUTING': + return '⏳' + case 'EXECUTED': + return '✅' + case 'CANCELLING': + return '❌' + case 'CANCELLED': + return '⛔️' + + default: + return '❓' + } +} + + +function statusColor(status) { + switch (status) { + case 'EXECUTED': + return chalk.green(status) + case 'CANCELLED': + return chalk.red(status) + + default: + return chalk.yellow(status) + } +} + + +function validationPolicy(job) { + return [ + `${job.validationPolicy.includes('instance-validation') ? '✅' : '❌'} Instance Validation`, + `${job.validationPolicy.includes('node-based-validation') ? '✅' : '❌'} Node Based Validation`, + ] +} + + +function executionPolicy(job) { + return [ + `${job.executionPolicy.includes('stop-on-error') ? '✅' : '❌'} Stop`, + `${job.executionPolicy.includes('continue-on-error-node') ? '✅' : '❌'} Skip to Next Node`, + `${job.executionPolicy.includes('continue-on-error-operation') ? '✅' : '❌'} Skip to Next Operation`, + `${job.executionPolicy.includes('parallel') ? '✅ Parallel' : '✅ Sequential'}`, + `${job.executionPolicy.includes('skip-unsync-nodes') ? '✅' : '❌'} Skip Unsync Nodes`, + ] +} + + +function timeAttributes(job) { + Object.entries(job) + .forEach(([key, value]) => { + if (['created', 'lastValidation', 'lastExecution'].includes(key)) { + const title = key.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()) + const formattingSpaces = ' '.repeat(18 - title.length) + console.log(` ${chalk.yellowBright.bold(title + ':')}${formattingSpaces} ${new Date(value).toLocaleString()}`) + } + }) +} + + +function optionalAttributes(job) { + if (job.failureReason) { + console.log(` ${chalk.yellowBright.bold('Failure Reason: ')} ${chalk.red(job.failureReason)}`) + } + if (job.files && job.files.find(i => i).name) { + console.log(` ${chalk.yellowBright.bold('Files: ')} + ${job.files.map(f => `${chalk.dim.italic(f.format)} ${f.name}`).join('\n ')}`) + } + if (job.summary) { + console.table(job.summary, ['parsed', 'valid', 'invalid', 'executed', 'executionErrors']) + } +} + + +function logJob(job) { + if (!job) { + throw new Error('No job data❗') + } + console.log(` + ${chalk.yellowBright.bold('Job ID: ')} ${chalk.dim(job.id)} + ${chalk.yellowBright.bold('Job Name: ')} ${job.name} + ${chalk.yellowBright.bold('User: ')} ${chalk.cyanBright.underline(job.userId)} + ${chalk.yellowBright.bold('Configuration: ')} ${chalk.greenBright(job.configuration)} + ${chalk.yellowBright.bold('Status: ')} ${statusColor(job.status)} ${statusPic(job.status)} + ${chalk.yellowBright.bold('Time Total: ')} ${new Date(job.totalElapsedTime * 1000).toISOString().slice(11, -5)}`) + timeAttributes(job) + console.log(` ${chalk.yellowBright.bold('Validation Options:')} + ${chalk.gray(validationPolicy(job).join('\n '))} + ${chalk.yellowBright.bold('Execution Options:')} + ${chalk.gray(executionPolicy(job).join('\n '))}`) + optionalAttributes(job) + console.log('') +} + + +module.exports = { + logJob, + statusPic, +} \ No newline at end of file diff --git a/util/logOperation.js b/util/logOperation.js new file mode 100755 index 0000000..ccae43c --- /dev/null +++ b/util/logOperation.js @@ -0,0 +1,64 @@ +const chalk = require('chalk') + + +function statusColor(status) { + switch (status) { + case 'EXECUTED': + return chalk.green(status) + case 'INVALID': + return chalk.red(status) + + default: + return chalk.yellow(status) + } +} + + +function attributesTransform(attributes, currentAttributes) { + return attributes.reduce((prev, curr) => { + const { currentValue } = currentAttributes.find(i => i.name === curr.name) ?? {} + const { name, suppliedValue } = curr + return { + ...prev, + [name]: { + currentValue: currentValue ? currentValue : '', + suppliedValue, + } + } + }, {}) +} + + +function logOperation(operation) { + if (!operation) { + throw new Error('No operation data❗') + } + const { + id, + type, + status, + updateTime, + fdn, + failures, + attributes = [], + currentAttributes = [], + } = operation + console.log(` + ${chalk.yellowBright.bold('Operation ID:')} ${chalk.dim(id)} + ${chalk.yellowBright.bold('Type: ')} ${type} + ${chalk.yellowBright.bold('Status: ')} ${statusColor(status)} + ${chalk.yellowBright.bold('Update Time: ')} ${new Date(updateTime).toLocaleString()} + ${chalk.yellowBright.bold('FDN: ')} ${chalk.cyanBright.underline(fdn)}`) + if (failures) { + console.log(` ${chalk.yellowBright.bold('Failures: ')}`) + failures.forEach(({ failureReason }) => console.log(` ❌ ${chalk.redBright(failureReason)}`)) + } + if (attributes.length > 0) { + console.table(attributesTransform(attributes, currentAttributes)) + } +} + + +module.exports = { + logOperation, +} \ No newline at end of file diff --git a/util/logProject.js b/util/logProject.js index edb9472..d50a749 100755 --- a/util/logProject.js +++ b/util/logProject.js @@ -3,7 +3,7 @@ const chalk = require('chalk') function logProject(data, nodeSummary) { if (!data || !nodeSummary) { - throw new Error('No project data or node summary!') + throw new Error('No project data or node summary❗') } const { id: projectId, diff --git a/util/validation.js b/util/validation.js index ee157c0..b617ffb 100755 --- a/util/validation.js +++ b/util/validation.js @@ -53,6 +53,14 @@ const isValidNodeName = (input) => { } +const isXMas = () => { + const date = new Date() + if (date.getMonth() === 11 || date.getMonth() === 0) { + return true + } +} + + module.exports = { isEmpty, isValidHardwareId, @@ -60,4 +68,5 @@ module.exports = { isValidString, checkValueRangeConstraints, isValidNodeName, + isXMas, } \ No newline at end of file