From 417a306ba36bbc5c6c2e0d501db41fc896348332 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 4 Aug 2024 20:32:59 +0900 Subject: [PATCH 01/23] Refactored data prep for company report --- cli/mrcli-company.js | 246 ++++++++++++++++++++++------------------ cli/mrcli.js | 2 +- package.json | 2 +- src/cli/common.js | 3 +- src/report/charts.js | 19 +++- src/report/dashboard.js | 122 ++++++++++---------- 6 files changed, 219 insertions(+), 175 deletions(-) diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 9f57601..5c50262 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -21,12 +21,13 @@ import ArchivePackage from '../src/cli/archive.js' import ora from 'ora' import WizardUtils from '../src/cli/commonWizard.js' + // Related object type const objectType = 'Companies' // Environmentals object const environment = new Environmentals( - '3.0', + '3.1.0', `${objectType}`, `Command line interface for mediumroast.io ${objectType} objects.`, objectType @@ -61,95 +62,120 @@ const wutils = new WizardUtils() const companyCtl = new Companies(accessToken, myEnv.gitHubOrg, processName) const interactionCtl = new Interactions(accessToken, myEnv.gitHubOrg, processName) const gitHubCtl = new GitHubFunctions(accessToken, myEnv.gitHubOrg, processName) - // const studyCtl = new Studies(accessToken, myEnv.gitHubOrg, processName) const userCtl = new Users(accessToken, myEnv.gitHubOrg, processName) +function initializeSource() { + return { + company: [], + interactions: [], + competitors: { + leastSimilar: {}, + mostSimilar: {}, + }, + totalInteractions: 0, + totalCompanies: 0, + } +} + +async function fetchData() { + const [intStatus, intMsg, allInteractions] = await interactionCtl.getAll() + const [compStatus, compMsg, allCompanies] = await companyCtl.getAll() + return { allInteractions, allCompanies } +} + +function getSourceCompany(allCompanies, companyName) { + return allCompanies.mrJson.filter(company => company.name === companyName) +} + +function getInteractions(sourceCompany, allInteractions) { + const interactionNames = Object.keys(sourceCompany[0].linked_interactions); + return interactionNames.map(interactionName => + allInteractions.mrJson.find(interaction => interaction.name === interactionName) + ).filter(interaction => interaction !== undefined); +} + +function getCompetitors(sourceCompany, allCompanies) { + const competitorNames = Object.keys(sourceCompany[0].similarity); + const allCompetitors = competitorNames.map(competitorName => + allCompanies.mrJson.find(company => company.name === competitorName) + ).filter(company => company !== undefined); + + const mostSimilar = competitorNames.reduce((mostSimilar, competitorName) => { + const competitor = allCompanies.mrJson.find(company => company.name === competitorName); + if (!competitor) return mostSimilar; + + const similarityScore = sourceCompany[0].similarity[competitorName].similarity; + if (!mostSimilar || similarityScore > mostSimilar.similarity) { + return { ...competitor, similarity: similarityScore }; + } + return mostSimilar; + }, null); + + const leastSimilar = competitorNames.reduce((leastSimilar, competitorName) => { + const competitor = allCompanies.mrJson.find(company => company.name === competitorName); + if (!competitor) return leastSimilar; + + const similarityScore = sourceCompany[0].similarity[competitorName].similarity; + if (!leastSimilar || similarityScore < leastSimilar.similarity) { + return { ...competitor, similarity: similarityScore }; + } + return leastSimilar; + }, null); + + return { allCompetitors, mostSimilar, leastSimilar }; +} + +async function _prepareData(companyName) { + let source = initializeSource() + + const { allInteractions, allCompanies } = await fetchData() + + source.company = getSourceCompany(allCompanies, companyName) + source.totalCompanies = allCompanies.mrJson.length + + source.interactions = getInteractions(source.company, allInteractions); + source.totalInteractions = source.interactions.length + + const { allCompetitors, mostSimilar, leastSimilar } = getCompetitors(source.company, allCompanies) + source.competitors.all = allCompetitors + source.competitors.mostSimilar = mostSimilar + source.competitors.leastSimilar = leastSimilar + + console.log(JSON.stringify(source, null, 2)) + process.exit(0) + + return source +} + // Predefine the results variable let [success, stat, results] = [null, null, null] // Process the cli options // TODO consider moving this out into at least a separate function to make main clean if (myArgs.report) { - console.error('ERROR (%d): Report not implemented.', -1) - process.exit(-1) - // Retrive the interaction by Id - const [comp_success, comp_stat, comp_results] = await companyCtl.findById(myArgs.report) - // Retrive the company by Name - const interactionNames = Object.keys(comp_results[0].linked_interactions) - // Obtain relevant interactions - let interactions = [] - for (const interactionName in interactionNames) { - const [mySuccess, myStat, myInteraction] = await interactionCtl.findByName( - interactionNames[interactionName] - ) - interactions.push(myInteraction[0]) - } - // Obtain the competitors - let competitors = [] - let competitiveInteractions = [] - const competitorIdxs = Object.keys(comp_results[0].comparison) - for (const compIdx in competitorIdxs) { - // const competitor = competitorIds[comp] - // console.log(comp_results[0].comparison[competitor].name) - const competitorIndex = competitorIdxs[compIdx] // Index in the comparison property for the company - const competitorName = comp_results[0].comparison[competitorIndex].name // Actual company name - const [compSuccess, compStat, myCompetitor] = await companyCtl.findByName(competitorName) - const [mostSuccess, mostStat, myMost] = await interactionCtl.findByName( - comp_results[0].comparison[competitorIndex].most_similar.name - ) - const [leastSuccess, leastStat, myLeast] = await interactionCtl.findByName( - comp_results[0].comparison[competitorIndex].least_similar.name - ) - // Format the scores and names - const leastScore = String( - Math.round(comp_results[0].comparison[competitorIndex].least_similar.score * 100) - ) + '%' - const mostScore = String( - Math.round(comp_results[0].comparison[competitorIndex].most_similar.score * 100) - ) + '%' - const leastName = comp_results[0].comparison[competitorIndex].least_similar.name.slice(0,40) + '...' - const mostName = comp_results[0].comparison[competitorIndex].most_similar.name.slice(0,40) + '...' - competitors.push( - { - company: myCompetitor[0], - mostSimilar: { - score: mostScore, - name: comp_results[0].comparison[competitorIndex].most_similar.name, - interaction: myMost[0] - }, - leastSimilar: { - score: leastScore, - name: comp_results[0].comparison[competitorIndex].least_similar.name, - interaction: myLeast[0] - } - } - ) - competitiveInteractions.push(myMost[0], myLeast[0]) - } + // Prepare the data for the report + const reportData = await _prepareData(myArgs.report) // Set the root name to be used for file and directory names in case of packaging - const baseName = comp_results[0].name.replace(/ /g,"_") + const baseName = reportData.company[0].name.replace(/ /g, "_") // Set the directory name for the package const baseDir = myEnv.workDir + '/' + baseName // Define location and name of the report output, depending upon the package switch this will change let fileName = process.env.HOME + '/Documents/' + baseName + '.docx' - // Set up the document controller const docController = new CompanyStandalone( - comp_results[0], // Company to report on - interactions, // The interactions associated to the company - competitors, // Relevant competitors for the company + reportData, myEnv, 'mediumroast.io barrista robot', // The author 'Mediumroast, Inc.' // The authoring company/org ) - - if(myArgs.package) { + + if (myArgs.package) { // Create the working directory const [dir_success, dir_msg, dir_res] = fileSystem.safeMakedir(baseDir + '/interactions') - + // If the directory creations was successful download the interaction - if(dir_success) { + if (dir_success) { fileName = baseDir + '/' + baseName + '_report.docx' /* TODO the below only assumes we're storing data in S3, this is intentionally naive. @@ -160,12 +186,12 @@ if (myArgs.report) { access points, but the tradeoff would be that caffeine would need to run on a system with file system access to these objects. */ - // Append the competitive interactions on the list and download all + // Append the competitive interactions on the list and download all interactions = [...interactions, ...competitiveInteractions] // TODO: We need to rewrite the logic for obtaining the interactions as they are from GitHub // await s3.s3DownloadObjs(interactions, baseDir + '/interactions', sourceBucket) null - // Else error out and exit + // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) process.exit(-1) @@ -201,12 +227,12 @@ if (myArgs.report) { console.error(report_stat, -1) process.exit(-1) } -// NOTICE: For Now we won't have any ids available for companies, so we'll need to use names -/* } else if (myArgs.find_by_id) { - [success, stat, results] = await companyCtl.findById(myArgs.find_by_id) */ + // NOTICE: For Now we won't have any ids available for companies, so we'll need to use names + /* } else if (myArgs.find_by_id) { + [success, stat, results] = await companyCtl.findById(myArgs.find_by_id) */ } else if (myArgs.find_by_name) { [success, stat, results] = await companyCtl.findByName(myArgs.find_by_name) -// TODO: Need to reimplment the below to account for GitHub + // TODO: Need to reimplment the below to account for GitHub } else if (myArgs.find_by_x) { const [myKey, myValue] = Object.entries(JSON.parse(myArgs.find_by_x))[0] const foundObjects = await companyCtl.findByX(myKey, myValue) @@ -215,7 +241,7 @@ if (myArgs.report) { results = foundObjects[2] } else if (myArgs.update) { const lockResp = await companyCtl.checkForLock() - if(lockResp[0]) { + if (lockResp[0]) { console.log(`ERROR: ${lockResp[1].status_msg}`) process.exit(-1) } @@ -224,52 +250,52 @@ if (myArgs.report) { mySpinner.start() const [success, stat, resp] = await companyCtl.updateObj(myCLIObj) mySpinner.stop() - if(success) { + if (success) { console.log(`SUCCESS: ${stat.status_msg}`) process.exit(0) - } else { + } else { console.log(`ERROR: ${stat.status_msg}`) process.exit(-1) } -// TODO: Need to reimplement the below to account for GitHub + // TODO: Need to reimplement the below to account for GitHub } else if (myArgs.delete) { - const lockResp = await companyCtl.checkForLock() - if(lockResp[0]) { - console.log(`ERROR: ${lockResp[1].status_msg}`) - process.exit(-1) - } - // Use operationOrNot to confirm the delete - const deleteOrNot = await wutils.operationOrNot(`Preparing to delete the company [${myArgs.delete}], are you sure?`) - if(!deleteOrNot) { - console.log(`INFO: Delete of [${myArgs.delete}] cancelled.`) - process.exit(0) - } - // If allow_orphans is set log a warning to the user that they are allowing orphaned interactions - if(myArgs.allow_orphans) { - console.log(chalk.bold.yellow(`WARNING: Allowing orphaned interactions to remain in the system.`)) - } - // Delete the object - const mySpinner = new ora(`Deleting company [${myArgs.delete}] ...`) - mySpinner.start() - const [success, stat, resp] = await companyCtl.deleteObj(myArgs.delete, myArgs.allow_orphans) - mySpinner.stop() - if(success) { - console.log(`SUCCESS: ${stat.status_msg}`) - process.exit(0) - } else { - console.log(`ERROR: ${stat.status_msg}`) - process.exit(-1) - } + const lockResp = await companyCtl.checkForLock() + if (lockResp[0]) { + console.log(`ERROR: ${lockResp[1].status_msg}`) + process.exit(-1) + } + // Use operationOrNot to confirm the delete + const deleteOrNot = await wutils.operationOrNot(`Preparing to delete the company [${myArgs.delete}], are you sure?`) + if (!deleteOrNot) { + console.log(`INFO: Delete of [${myArgs.delete}] cancelled.`) + process.exit(0) + } + // If allow_orphans is set log a warning to the user that they are allowing orphaned interactions + if (myArgs.allow_orphans) { + console.log(chalk.bold.yellow(`WARNING: Allowing orphaned interactions to remain in the system.`)) + } + // Delete the object + const mySpinner = new ora(`Deleting company [${myArgs.delete}] ...`) + mySpinner.start() + const [success, stat, resp] = await companyCtl.deleteObj(myArgs.delete, myArgs.allow_orphans) + mySpinner.stop() + if (success) { + console.log(`SUCCESS: ${stat.status_msg}`) + process.exit(0) + } else { + console.log(`ERROR: ${stat.status_msg}`) + process.exit(-1) + } } else if (myArgs.add_wizard) { const lockResp = await companyCtl.checkForLock() - if(lockResp[0]) { + if (lockResp[0]) { console.log(`ERROR: ${lockResp[1].status_msg}`) process.exit(-1) } - myEnv.DEFAULT = {company: 'Unknown'} - const newCompany = new AddCompany(myEnv, {github: gitHubCtl, interaction: interactionCtl, company: companyCtl, user: userCtl}) + myEnv.DEFAULT = { company: 'Unknown' } + const newCompany = new AddCompany(myEnv, { github: gitHubCtl, interaction: interactionCtl, company: companyCtl, user: userCtl }) const result = await newCompany.wizard() - if(result[0]) { + if (result[0]) { console.log(`SUCCESS: ${result[1].status_msg}`) process.exit(0) } else { @@ -280,11 +306,11 @@ if (myArgs.report) { console.error(`WARNING: CLI function not yet implemented for companies: %d`, -1) process.exit(-1) const lockResp = companyCtl.checkForLock() - if(lockResp[0]) { + if (lockResp[0]) { console.log(`ERROR: ${lockResp[1].status_msg}`) process.exit(-1) } -// TODO: Need to reimplement the below to account for GitHub, and this is where we will start to use the new CLIOutput + // TODO: Need to reimplement the below to account for GitHub, and this is where we will start to use the new CLIOutput } else { [success, stat, results] = await companyCtl.getAll() results = results.mrJson diff --git a/cli/mrcli.js b/cli/mrcli.js index d0c4ff5..096e1ba 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.7.0') + .version('0.7.1') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index fc6d52d..fb6662f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.7.0", + "version": "0.7.1", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/cli/common.js b/src/cli/common.js index a8bd792..b15c90c 100644 --- a/src/cli/common.js +++ b/src/cli/common.js @@ -2,12 +2,11 @@ * A class for common functions for all CLIs. * @author Michael Hay * @file common.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. + * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 * @version 1.1.0 */ - // Import required modules import axios from 'axios' import * as fs from 'fs' diff --git a/src/report/charts.js b/src/report/charts.js index ca29e99..6b3870d 100755 --- a/src/report/charts.js +++ b/src/report/charts.js @@ -4,6 +4,9 @@ import axios from 'axios' import docxSettings from './settings.js' import { Utilities as CLIUtilities } from '../cli/common.js' +// Pickup the general settings +const generalStyle = docxSettings.general + function _transformForBubble(objData) { let chartData = [] for(const obj in objData){ @@ -15,6 +18,17 @@ function _transformForBubble(objData) { return chartData } +// TODO this isn't really correct or used yet +function _transformForPie(objData) { + let chartData = [] + for(const obj in objData){ + const objName = objData[obj].name + const objScore = (objData[obj].score * 100).toFixed(2) + chartData.push([objName, objScore]) + } + return chartData +} + async function _postToChartServer(jsonObj, server) { const myURL = server const myHeaders = { @@ -104,7 +118,6 @@ export async function bubbleChart ( const cliUtil = new CLIUtilities() // Pick up the settings including those from the theme - const generalStyle = docxSettings.general const themeStyle = docxSettings[env.theme] // Change the originating data into data aligned to the bubble chart @@ -347,4 +360,8 @@ export async function radarChart ( const putResult = await _postToChartServer(myChart, env.echartsServer) let imageURL = env.echartsServer + '/' + putResult[2].filename return await cliUtil.downloadImage(imageURL, baseDir + '/images', chartFile) +} + +export async function pieChart () { + // TODO } \ No newline at end of file diff --git a/src/report/dashboard.js b/src/report/dashboard.js index 829eb27..99a9f73 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -82,7 +82,6 @@ class Dashboards { } - // Following the _statisticsTable method in the CompanyDashboard class create a similar method for all dashboards /** * * @@ -412,7 +411,7 @@ class InteractionDashboard extends Dashboards { } } -class CompanyDashbord { +class CompanyDashbord extends Dashboards { /** * A high class meant to create an initial dashboard page for an MS Word document company report * @constructor @@ -421,63 +420,66 @@ class CompanyDashbord { * @param {String} theme - Governs the color of the dashboard, be either coffee or latte */ constructor(env) { - this.env = env - this.util = new DOCXUtilities(env) - this.themeStyle = docxSettings[env.theme] // Set the theme for the report - this.generalStyle = docxSettings.general // Pull in all of the general settings - - // Define specifics for table borders - this.noneStyle = { - style: this.generalStyle.noBorderStyle - } - this.borderStyle = { - style: this.generalStyle.tableBorderStyle, - size: this.generalStyle.tableBorderSize, - color: this.themeStyle.tableBorderColor - } - // No borders - this.noBorders = { - left: this.noneStyle, - right: this.noneStyle, - top: this.noneStyle, - bottom: this.noneStyle - } - // Right border only - this.rightBorder = { - left: this.noneStyle, - right: this.borderStyle, - top: this.noneStyle, - bottom: this.noneStyle - } - // Bottom border only - this.bottomBorder = { - left: this.noneStyle, - right: this.noneStyle, - top: this.noneStyle, - bottom: this.borderStyle - } - // Bottom and right borders - this.bottomAndRightBorders = { - left: this.noneStyle, - right: this.borderStyle, - top: this.noneStyle, - bottom: this.borderStyle - } - // Top and right borders - this.topAndRightBorders = { - left: this.noneStyle, - right: this.borderStyle, - top: this.borderStyle, - bottom: this.noneStyle - } - // All borders, helpful for debugging - this.allBorders = { - left: this.borderStyle, - right: this.borderStyle, - top: this.borderStyle, - bottom: this.borderStyle - } + super(env) } + // constructor(env) { + // this.env = env + // this.util = new DOCXUtilities(env) + // this.themeStyle = docxSettings[env.theme] // Set the theme for the report + // this.generalStyle = docxSettings.general // Pull in all of the general settings + + // // Define specifics for table borders + // this.noneStyle = { + // style: this.generalStyle.noBorderStyle + // } + // this.borderStyle = { + // style: this.generalStyle.tableBorderStyle, + // size: this.generalStyle.tableBorderSize, + // color: this.themeStyle.tableBorderColor + // } + // // No borders + // this.noBorders = { + // left: this.noneStyle, + // right: this.noneStyle, + // top: this.noneStyle, + // bottom: this.noneStyle + // } + // // Right border only + // this.rightBorder = { + // left: this.noneStyle, + // right: this.borderStyle, + // top: this.noneStyle, + // bottom: this.noneStyle + // } + // // Bottom border only + // this.bottomBorder = { + // left: this.noneStyle, + // right: this.noneStyle, + // top: this.noneStyle, + // bottom: this.borderStyle + // } + // // Bottom and right borders + // this.bottomAndRightBorders = { + // left: this.noneStyle, + // right: this.borderStyle, + // top: this.noneStyle, + // bottom: this.borderStyle + // } + // // Top and right borders + // this.topAndRightBorders = { + // left: this.noneStyle, + // right: this.borderStyle, + // top: this.borderStyle, + // bottom: this.noneStyle + // } + // // All borders, helpful for debugging + // this.allBorders = { + // left: this.borderStyle, + // right: this.borderStyle, + // top: this.borderStyle, + // bottom: this.borderStyle + // } + // } /** * @@ -930,7 +932,7 @@ class CompanyDashbord { } // Find the closest competitor - // This uses the Euclidean distantce, given points (x1, y1) and (x2, y2) + // This uses the Euclidean distance, given points (x1, y1) and (x2, y2) // d = sqrt((x2 - x1)^2 + (y2 - y1)^2) _getMostSimilarCompany(comparisons, companies) { const x1 = 1 // In this case x1 = 1 and y1 = 1 @@ -1052,7 +1054,7 @@ class CompanyDashbord { } // Compute the descriptive statistics for interactions const myStats = this._computeInteractionStats(company,competitors) - // Create the radard chart from supplied interaction quality data + // TODO change to pie chart const radarChartFile = await radarChart( {company: company, competitors: competitors, stats: myStats}, this.env, From 4f6dc4a011c8d581bfbc0f93fd60bf0fd11a9c28 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Mon, 5 Aug 2024 14:36:46 -0700 Subject: [PATCH 02/23] Incremental checkin of widgets refactor --- src/report/companies.js | 64 ++--- src/report/widgets/Tables.js | 444 +++++++++++++++++++++++++++++++++ src/report/widgets/Text.js | 200 +++++++++++++++ src/report/widgets/Widgets.js | 225 +++++++++++++++++ src/report/widgets/settings.js | 78 ++++++ 5 files changed, 966 insertions(+), 45 deletions(-) create mode 100644 src/report/widgets/Tables.js create mode 100644 src/report/widgets/Text.js create mode 100644 src/report/widgets/Widgets.js create mode 100644 src/report/widgets/settings.js diff --git a/src/report/companies.js b/src/report/companies.js index 73dbc8c..af48aa9 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -13,7 +13,6 @@ import boxPlot from 'box-plot' import DOCXUtilities from './common.js' import { InteractionSection } from './interactions.js' import { CompanyDashbord } from './dashboard.js' -import FilesystemOperators from '../cli/filesystem.js' import { Utilities as CLIUtilities } from '../cli/common.js' import { getMostSimilarCompany } from './tools.js' @@ -82,16 +81,14 @@ class CompanySection extends BaseCompanyReport { // Create a URL to search for patents on Google patents patentRow() { - const patentString = this.company.name + ' Patent Search' - const patentUrl = 'https://patents.google.com/?assignee=' + this.company.name - return this.util.urlRow('Patents', patentString, patentUrl) + const patentString = `${this.company.name} Patent Search` + return this.util.urlRow('Patents', patentString, this.company.google_patents_url) } // Create a URL to search for news on Google news newsRow() { - const newsString = this.company.name + ' Company News' - const newsUrl = 'https://news.google.com/search?q=' + this.company.name - return this.util.urlRow('News', newsString, newsUrl) + const newsString = `${this.company.name} Company News` + return this.util.urlRow('News', newsString, this.company.google_news_url) } // Define the CIK and link it to an EDGAR search if available @@ -336,49 +333,26 @@ class CompanyStandalone extends BaseCompanyReport { * @todo Rename this class as report and rename the file as companyDocx.js * @todo Adapt to settings.js for consistent application of settings, follow dashboard.js */ - constructor(company, interactions, competitors, env, creator, author) { - super(company, env) + constructor(sourceData, env, author='Mediumroast for GitHub') { + super(sourceData.company, env) this.objectType = 'Company' - this.creator = creator + this.creator = author this.author = author - this.authoredBy = 'Mediumroast for GitHub' - this.title = company.name + ' Company Report' - this.interactions = interactions + this.authoredBy = author + this.title = `${this.company.name} Company Report` + this.interactions = this.company.interactions this.competitors = competitors - this.description = 'A Company report summarizing ' + company.name + ' and including relevant company data.' - /* - ChatGPT summary - The mediumroast.io system has meticulously crafted this report to provide you with a comprehensive overview of the Company object and its associated interactions. This document features a robust collection of key metadata that provides valuable insights into the company's operations. Furthermore, to enhance the user experience, if this report is part of a package, the hyperlinks within it are designed to be active, linking to various documents within the local folder with just one click after the package is opened. This makes exploring the details of the company a breeze! - */ - this.introduction = 'The mediumroast.io system automatically generated this document.' + - ' It includes key metadata for this Company object and relevant summaries and metadata from the associated interactions.' + - ' If this report document is produced as a package, instead of standalone, then the' + - ' hyperlinks are active and will link to documents on the local folder after the' + - ' package is opened.' - this.util = new DOCXUtilities(env) - this.fileSystem = new FilesystemOperators() - // this.topics = this.util.rankTags(this.company.topics) - this.comparison = company.comparison, + this.description = `A Company report for ${this.company.name} and including relevant company data.` + this.introduction = `The mediumroast.io system automatically generated this document. + It includes key metadata for this Company object and relevant summaries and metadata from the associated interactions. If this report document is produced as a package, instead of standalone, then the + hyperlinks are active and will link to documents on the local folder after the + package is opened.` + this.similarity = this.company.similarity, this.noInteractions = String(Object.keys(this.company.linked_interactions).length) + this.totalInteractions = this.company.totalInteractions + this.totalCompanies = this.company.totalCompanies } - // - // Local functions - // - - // Basic operations to prepare for report and package creation - _initialize() { - // - const subdirs = ['interactions', 'images'] - for(const myDir in subdirs) { - this.fileSystem.safeMakedir(this.baseDir + '/' + subdirs[myDir]) - } - } - - // - // External functions - // - /** * @async * @function makeDocx @@ -429,7 +403,7 @@ class CompanyStandalone extends BaseCompanyReport { companySection.makeFirmographicsDOCX(), this.util.makeHeading1('Comparison') ], - companySection.makeComparisonDOCX(this.comparison, this.competitors), + companySection.makeComparisonDOCX(this.similarity, this.competitors), [ this.util.makeHeading1('Topics'), this.util.makeParagraph( 'The following topics were automatically generated from all ' + diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js new file mode 100644 index 0000000..f3c60c2 --- /dev/null +++ b/src/report/widgets/Tables.js @@ -0,0 +1,444 @@ +// report/widgets/TableWidget.js +import Widgets from './Widgets.js' +import TextWidgets from './Text.js' +import docx from 'docx' + +class TableWidget extends Widgets { + constructor(env) { + super(env) + // Define specifics for table borders + this.noneStyle = { + style: this.generalStyle.noBorderStyle + } + this.borderStyle = { + style: this.generalStyle.tableBorderStyle, + size: this.generalStyle.tableBorderSize, + color: this.themeStyle.tableBorderColor + } + // No borders + this.noBorders = { + left: this.noneStyle, + right: this.noneStyle, + top: this.noneStyle, + bottom: this.noneStyle + } + // Right border only + this.rightBorder = { + left: this.noneStyle, + right: this.borderStyle, + top: this.noneStyle, + bottom: this.noneStyle + } + // Bottom border only + this.bottomBorder = { + left: this.noneStyle, + right: this.noneStyle, + top: this.noneStyle, + bottom: this.borderStyle + } + // Bottom and right borders + this.bottomAndRightBorders = { + left: this.noneStyle, + right: this.borderStyle, + top: this.noneStyle, + bottom: this.borderStyle + } + // Top and right borders + this.topAndRightBorders = { + left: this.noneStyle, + right: this.borderStyle, + top: this.borderStyle, + bottom: this.noneStyle + } + // All borders, helpful for debugging + this.allBorders = { + left: this.borderStyle, + right: this.borderStyle, + top: this.borderStyle, + bottom: this.borderStyle + } + } + + /** + * @function twoColumnRowBasic + * @description Basic table row to produce a name/value pair table with 2 columns + * @param {Array} cols - an array of 2 strings to be used as the text/prose for the cells + * @param {Object} options - options for the cell to control bolding in the first column and all columns and border styles + * @returns {Object} a new docx TableRow object + * + * @example + * const myRow = twoColumnRowBasic(["Name", "Michael Hay"], {firstColumnBold: true, allColumnsBold: false}) + * + */ + twoColumnRowBasic (cols, options={}) { + let { + firstColumnBold = true, + allColumnsBold = false, + allBorders = false, + bottomBorders = true, + } = options + + // Set the first column to bold if all columns are bold + if (allColumnsBold) { + firstColumnBold = true + } + + // Set the border style + let borderStyle = this.noBorders + if (allBorders) { + borderStyle = this.allBorders + } else if (bottomBorders) { + borderStyle = this.bottomBorder + } + + // Destructure the cols array + const [col1, col2] = cols + + // return the row + return new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 20, + type: docx.WidthType.PERCENTAGE + }, + children: [TextWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], + borders: borderStyle + }), + new docx.TableCell({ + width: { + size: 80, + type: docx.WidthType.PERCENTAGE + }, + children: [TextWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + borders: borderStyle + }) + ] + }) + } + + /** + * @function twoColumnRowWithHyperlink + * @description Hyperlink table row to produce a name/value pair table with 2 columns and an external hyperlink + @param {Array} cols - an array of 3 strings for the row first column, second column text, and the hyperlink URL + * @param {Object} options - options for the cell to control bolding in the first column and all columns and border styles + * @returns {Object} a new docx TableRow object with an external hyperlink + */ + twoColumnRowWithHyperlink(cols, options={}) { + let { + firstColumnBold = true, + allColumnsBold = false, + allBorders = false, + bottomBorders = true, + } = options + + // Set the first column to bold if all columns are bold + if (allColumnsBold) { + firstColumnBold = true + } + + // Set the border style + let borderStyle = this.noBorders + if (allBorders) { + borderStyle = this.allBorders + } else if (bottomBorders) { + borderStyle = this.bottomBorder + } + + // Destructure the cols array + const [col1, col2, col2Hyperlink] = cols + + // define the link to the target URL + const myUrl = new docx.ExternalHyperlink({ + children: [ + new docx.TextRun({ + text: col2, + style: 'Hyperlink', + font: this.generalSettings.font, + size: this.generalSettings.fullFontSize, + }) + ], + link: col2Hyperlink + }) + + // return the row + return new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 20, + type: docx.WidthType.PERCENTAGE + }, + children: [this.makeParagraph(col1, this.fontFactor * this.fontSize, true)] + }), + new docx.TableCell({ + width: { + size: 80, + type: docx.WidthType.PERCENTAGE + }, + children: [new docx.Paragraph({children:[myUrl]})] + }) + ] + }) + } + + /** + * @function threeColumnRowBasic + * @description Basic table row with 3 columns + * @param {Array} cols - an array of 3 strings to be used as the text/prose for the cells + * * @param {Object} options - options for the cell to control bolding in the first column and all columns and border styles + * @returns {Object} a new 3 column docx TableRow object + */ + threeColumnRowBasic (cols, options={}) { + let { + firstColumnBold = true, + allColumnsBold = false, + allBorders = false, + bottomBorders = true, + } = options + + // Set the first column to bold if all columns are bold + if (allColumnsBold) { + firstColumnBold = true + } + + // Set the border style + let borderStyle = this.noBorders + if (allBorders) { + borderStyle = this.allBorders + } else if (bottomBorders) { + borderStyle = this.bottomBorder + } + // Destructure the cols array + const [col1, col2, col3] = cols + + // return the row + return new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 40, + type: docx.WidthType.PERCENTAGE, + }, + children: [TextWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], + borders: borderStyle + }), + new docx.TableCell({ + width: { + size: 30, + type: docx.WidthType.PERCENTAGE, + }, + children: [TextWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + borders: borderStyle + }), + new docx.TableCell({ + width: { + size: 30, + type: docx.WidthType.PERCENTAGE, + }, + children: [TextWidgets.makeParagraph(col3, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + borders: borderStyle + }), + ] + }) + } + + _tagCell(tag) { + return new docx.TableCell({ + margins: { + top: this.generalSettings.tagMargin, + right: this.generalSettings.tagMargin, + bottom: this.generalSettings.tagMargin, + left: this.generalSettings.tagMargin + }, + borders: { + top: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, // 2 points thick, black color + bottom: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, + left: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, + right: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, + }, + shading: {fill: this.themeSettings.tagColor}, + children: [this.makeParagraph(tag, {fontSize: this.fontSize, fontColor: this.themeSettings.tagFontColor, center: true})], + verticalAlign: docx.AlignmentType.CENTER, + }) + } + + _distributeTags(tagsList) { + // Calculate the number of lists as the ceiling of the square root of the length of tagsList + const numLists = Math.ceil(Math.sqrt(tagsList.length)) + + // Initialize result array with numLists empty arrays + const result = Array.from({ length: numLists }, () => []) + // Initialize lengths array with numLists zeros + const lengths = Array(numLists).fill(0) + + // Sort tagsList in descending order based on the length of the tags + tagsList.sort((a, b) => b.length - a.length) + + // Distribute tags + tagsList.forEach(tag => { + // Find the index of the child list with the minimum total character length + const minIndex = lengths.indexOf(Math.min(...lengths)) + // Add the tag to this child list + result[minIndex].push(tag); + // Update the total character length of this child list + lengths[minIndex] += tag.length + }) + + return result; + } + + /** + * @function tagsTable + * @description Create a table with tags + * @param {Object} tags - tags object + * @returns {Object} a docx Table object + * @example + * const tags = { + * "tag1": "value1", + * "tag2": "value2" + * } + * const myTable = tagsTable(tags) + * + * // Add the table to the document + * doc.addSection({ + * properties: {}, + * children: [myTable] + * }) + */ + tagsTable(tags) { + // Get the length of the tags + const tagsList = Object.keys(tags) + const distributedTags = this._distributeTags(tagsList) + let myRows = [] + distributedTags.forEach(tags => { + let cells = [] + tags.forEach(tag => { + cells.push(this._tagCell(tag)) + }) + myRows.push(new docx.TableRow({ + children: cells + })) + }) + // define the table with the summary theme information + const myTable = new docx.Table({ + columnWidths: Array(distributedTags.length).fill(100/distributedTags.length), + rows: myRows, + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) + + return myTable + } + +/** + * Begin functions for completely defining tables with a particular structure +*/ + + /** + * + * @function descriptiveStatisticsTable + * @param {*} statistics + * @returns + * @todo turn into a loop instead of having the code repeated + * @todo if the length of any number is greater than 3 digits shrink the font size by 15% and round down + * @todo add a check for the length of the title and shrink the font size by 15% and round down + * @todo add a check for the length of the value and shrink the font size by 15% and round down + */ + descriptiveStatisticsTable(statistics) { + let myRows = [] + for(const stat in statistics) { + myRows.push( + new docx.TableRow({ + children: [ + new docx.TableCell({ + children: [ + this.util.makeParagraph( + statistics[stat].value, + { + fontSize: this.generalStyle.metricFontSize, + fontColor: this.themeStyle.titleFontColor, + font: this.generalStyle.heavyFont, + bold: true, + center: true + } + ) + ], + borders: this.bottomBorder, + margins: { + top: this.generalStyle.tableMargin + } + }), + ] + }), + new docx.TableRow({ + children: [ + new docx.TableCell({ + children: [ + this.util.makeParagraph( + statistics[stat].title, + { + fontSize: this.generalStyle.metricFontSize/2, + fontColor: this.themeStyle.titleFontColor, + bold: false, + center: true + } + ) + ], + borders: this.noBorders, + margins: { + bottom: this.generalStyle.tableMargin, + top: this.generalStyle.tableMargin + } + }), + ] + }) + ) + } + return new docx.Table({ + columnWidths: [95], + borders: this.noBorders, + rows: myRows, + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) + } + + twoCellRow (name, data) { + // return the row + return new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 20, + type: docx.WidthType.PERCENTAGE + }, + children: [this.util.makeParagraph(name, {fontSize: this.generalStyle.dashFontSize, bold: true})], + borders: this.bottomBorder, + margins: { + top: this.generalStyle.tableMargin, + right: this.generalStyle.tableMargin + }, + }), + new docx.TableCell({ + width: { + size: 80, + type: docx.WidthType.PERCENTAGE + }, + children: [this.util.makeParagraph(data, {fontSize: this.generalStyle.dashFontSize})], + borders: this.bottomAndRightBorders, + margins: { + top: this.generalStyle.tableMargin, + right: this.generalStyle.tableMargin + }, + }) + ] + }) + } +} + +export default TableWidget \ No newline at end of file diff --git a/src/report/widgets/Text.js b/src/report/widgets/Text.js new file mode 100644 index 0000000..eb350de --- /dev/null +++ b/src/report/widgets/Text.js @@ -0,0 +1,200 @@ +// report/widgets/TableWidget.js +import Widgets from './Widgets.js' +import docx from 'docx' + +class TextWidgets extends Widgets { + constructor(env) { + super(env) + // Initialization code for TextWidget + } + + /** + * @function makeBullet + * @description Create a bullet for a bit of prose + * @param {String} text - text/prose for the bullet + * @param {Integer} level - the level of nesting for the bullet + * @returns {Object} new docx paragraph object as a bullet + */ + makeBullet(text, level=0) { + return new docx.Paragraph({ + text: text, + numbering: { + reference: 'bullet-styles', + level: level + } + }) + } + + /** + * @function makeHeader + * @description Generate a header with an item's name and the document type fields + * @param {String} itemName + * @param {String} documentType + * @param {Object} options + */ + makeHeader(itemName, documentType, options={}) { + const { + landscape = false, + fontColor = this.themeSettings.textFontColor, + } = options + let separator = "\t".repeat(3) + if (landscape) { separator = "\t".repeat(4)} + return new docx.Header({ + children: [ + new docx.Paragraph({ + alignment: docx.AlignmentType.CENTER, + children: [ + new docx.TextRun({ + children: [documentType], + font: this.generalSettings.font, + size: this.generalSettings.headerFontSize, + color: fontColor ? fontColor : this.themeSettings.textFontColor + }), + new docx.TextRun({ + children: [itemName], + font: this.generalSettings.font, + size: this.generalSettings.headerFontSize, + color: fontColor ? fontColor : this.themeSettings.textFontColor + }) + ], + }), + ], + }) + } + + makeFooter(documentAuthor, datePrepared, options={}) { + const { + landscape = false, + fontColor = this.themeSettings.textFontColor, + } = options + let separator = "\t" + if (landscape) { separator = "\t".repeat(2)} + return new docx.Paragraph({ + alignment: docx.AlignmentType.CENTER, + children: [ + new docx.TextRun({ + children: ['Page ', docx.PageNumber.CURRENT, ' of ', docx.PageNumber.TOTAL_PAGES, separator], + font: this.generalSettings.font, + size: this.generalSettings.footerFontSize, + color: fontColor ? fontColor : this.themeSettings.textFontColor + }), + new docx.TextRun({ + children: ['|', separator, documentAuthor, separator], + font: this.generalSettings.font, + size: this.generalSettings.footerFontSize, + color: fontColor ? fontColor : this.textFontColor + }), + new docx.TextRun({ + children: ['|', separator, datePrepared], + font: this.generalSettings.font, + size: this.generalSettings.footerFontSize, + color: fontColor ? fontColor : this.themeSettings.textFontColor + }) + ] + }) + } + + /** + * @function makeParagraph + * @description For a section of prose create a paragraph + * @param {String} paragraph - text/prose for the paragraph + * @param {Object} objects - an object that contains the font size, color, and other styling options + * @returns {Object} a docx paragraph object + */ + makeParagraph (paragraph,options={}) { + const { + fontSize, + bold=false, + fontColor, + font='Avenir Next', + center=false, + italics=false, + underline=false, + spaceAfter=0, + } = options + // const fontSize = 2 * this.fullFontSize // Font size is measured in half points, multiply by to is needed + return new docx.Paragraph({ + alignment: center ? docx.AlignmentType.CENTER : docx.AlignmentType.LEFT, + children: [ + new docx.TextRun({ + text: paragraph, + font: font ? font : this.generalSettings.font, + size: fontSize ? fontSize : this.fullFontSize, // Default font size size 10pt or 2 * 10 = 20 + bold: bold ? bold : false, // Bold is off by default + italics: italics ? italics : false, // Italics off by default + underline: underline ? underline : false, // Underline off by default + break: spaceAfter ? spaceAfter : 0, // Defaults to no trailing space + color: fontColor ? fontColor : this.themeSettings.textFontColor + }) + ] + }) + } + + + /** + * @function pageBreak + * @description Create a page break + * @returns {Object} a docx paragraph object with a PageBreak + */ + pageBreak() { + return new docx.Paragraph({ + children: [ + new docx.PageBreak() + ] + }) + } + + /** + * @function makeHeading1 + * @description Create a text of heading style 1 + * @param {String} text - text/prose for the function + * @returns {Object} a new paragraph as a heading + */ + makeHeading1(text) { + return new docx.Paragraph({ + text: text, + heading: docx.HeadingLevel.HEADING_1 + }) + } + + /** + * @function makeHeading2 + * @description Create a text of heading style 2 + * @param {String} text - text/prose for the function + * @returns {Object} a new paragraph as a heading + */ + makeHeading2(text) { + return new docx.Paragraph({ + text: text, + heading: docx.HeadingLevel.HEADING_2 + }) + } + + /** + * @function makeHeading3 + * @description Create a text of heading style 3 + * @param {String} text - text/prose for the function + * @returns {Object} a new paragraph as a heading + */ + makeHeading3(text) { + return new docx.Paragraph({ + text: text, + heading: docx.HeadingLevel.HEADING_3 + }) + } + + /** + * @function makeIntro + * @description Creates an introduction paragraph with a heading of level 1 + * @param {String} introText - text/prose for the introduction + * @returns {Object} a complete introduction with heading level 1 and a paragraph + */ + makeIntro (introText) { + return [ + this.makeHeading1('Introduction'), + this.makeParagraph(introText) + ] + } +} + +export default TextWidgets \ No newline at end of file diff --git a/src/report/widgets/Widgets.js b/src/report/widgets/Widgets.js new file mode 100644 index 0000000..8d626f5 --- /dev/null +++ b/src/report/widgets/Widgets.js @@ -0,0 +1,225 @@ +/** + * The parent class that contains all the widgets for building reports in docx + * @author Michael Hay + * @file Widgets.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + */ + +import docxSettings from './settings.js' +import docx from 'docx' + +class Widgets { + constructor(env) { + this.env = env + this.generalSettings = docxSettings.general + this.themeSettings = docxSettings[this.env.theme] + this.styles = this.initStyles() + } + + initStyles () { + const hangingSpace = 0.18 + return { + default: { + heading1: { + run: { + size: this.generalSettings.fullFontSize, + bold: true, + font: this.generalSettings.font, + color: this.themeSettings.textFontColor + }, + paragraph: { + spacing: { + before: 160, + after: 80, + }, + }, + }, + heading2: { + run: { + size: 0.75 * this.generalSettings.fullFontSize, + bold: true, + font: this.generalSettings.font, + color: this.themeSettings.textFontColor + }, + paragraph: { + spacing: { + before: 240, + after: 120, + }, + }, + }, + heading3: { + run: { + size: 0.8 * this.generalSettings.fullFontSize, + bold: true, + font: this.generalSettings.font, + color: this.themeSettings.textFontColor + }, + paragraph: { + spacing: { + before: 240, + after: 120, + }, + }, + }, + listParagraph: { + run: { + font: this.generalSettings.font, + size: 1.5 * this.generalSettings.halfFontSize, + color: this.themeSettings.textFontColor + }, + }, + paragraph: { + font: this.generalSettings.font, + size: this.generalSettings.halfFontSize, + color: this.themeSettings.textFontColor + } + }, + paragraphStyles: [ + { + id: "mrNormal", + name: "MediumRoast Normal", + basedOn: "Normal", + next: "Normal", + quickFormat: true, + run: { + font: this.generalSettings.font, + size: this.generalSettings.halfFontSize, + }, + }, + ], + numbering: { + config: [ + { + reference: 'number-styles', + levels: [ + { + level: 0, + format: docx.LevelFormat.DECIMAL, + text: "%1.", + alignment: docx.AlignmentType.START, + style: { + paragraph: { + indent: { + left: docx.convertInchesToTwip(0.25), + hanging: docx.convertInchesToTwip(hangingSpace) + }, + spacing: { + before: 75 + } + }, + }, + }, + { + level: 1, + format: docx.LevelFormat.LOWER_LETTER, + text: "%2.", + alignment: docx.AlignmentType.START, + style: { + paragraph: { + indent: { left: docx.convertInchesToTwip(0.50), hanging: docx.convertInchesToTwip(hangingSpace) }, + }, + }, + }, + { + level: 2, + format: docx.LevelFormat.LOWER_ROMAN, + text: "%3.", + alignment: docx.AlignmentType.START, + style: { + paragraph: { + indent: { left: docx.convertInchesToTwip(0.75), hanging: docx.convertInchesToTwip(hangingSpace) }, + }, + }, + }, + { + level: 3, + format: docx.LevelFormat.UPPER_LETTER, + text: "%4.", + alignment: docx.AlignmentType.START, + style: { + paragraph: { + indent: { left: docx.convertInchesToTwip(1.0), hanging: docx.convertInchesToTwip(hangingSpace) }, + }, + }, + }, + { + level: 4, + format: docx.LevelFormat.UPPER_ROMAN, + text: "%5.", + alignment: docx.AlignmentType.START, + style: { + paragraph: { + indent: { left: docx.convertInchesToTwip(1.25), hanging: docx.convertInchesToTwip(hangingSpace) }, + }, + }, + }, + ] + }, + { + reference: "bullet-styles", + levels: [ + { + level: 0, + format: docx.LevelFormat.BULLET, + text: "-", + alignment: docx.AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: docx.convertInchesToTwip(0.5), hanging: docx.convertInchesToTwip(0.25) }, + }, + }, + }, + { + level: 1, + format: docx.LevelFormat.BULLET, + text: "\u00A5", + alignment: docx.AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: docx.convertInchesToTwip(1), hanging: docx.convertInchesToTwip(0.25) }, + }, + }, + }, + { + level: 2, + format: docx.LevelFormat.BULLET, + text: "\u273F", + alignment: docx.AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 2160, hanging: docx.convertInchesToTwip(0.25) }, + }, + }, + }, + { + level: 3, + format: docx.LevelFormat.BULLET, + text: "\u267A", + alignment: docx.AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 2880, hanging: docx.convertInchesToTwip(0.25) }, + }, + }, + }, + { + level: 4, + format: docx.LevelFormat.BULLET, + text: "\u2603", + alignment: docx.AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 3600, hanging: docx.convertInchesToTwip(0.25) }, + }, + }, + }, + ] + } + ]} + } + } +} + +export default Widgets \ No newline at end of file diff --git a/src/report/widgets/settings.js b/src/report/widgets/settings.js new file mode 100644 index 0000000..59dadc4 --- /dev/null +++ b/src/report/widgets/settings.js @@ -0,0 +1,78 @@ +import docx from 'docx' + +const docxSettings = { + general: { + halfFontSize: 11, + fullFontSize: 22, + headerFontSize: 18, + footerFontSize: 18, + fontFactor: 1, + dashFontSize: 22, + tableFontSize: 8, + titleFontSize: 18, + companyNameFontSize: 11.5, + metricFontTitleSize: 11, + metricFontSize: 48, + chartTitleFontSize: 18, + chartFontSize: 10, + chartAxesFontSize: 12, + chartTickFontSize: 10, + chartSymbolSize: 30, + tableBorderSize: 8, + tableBorderStyle: docx.BorderStyle.SINGLE, + noBorderStyle: docx.BorderStyle.NIL, + tableMargin: docx.convertInchesToTwip(0.1), + tagMargin: docx.convertInchesToTwip(0.08), + font: "Avenir Next", + heavyFont: "Avenir Next Heavy", + lightFont: "Avenir Next Light" + + }, + coffee: { + tableBorderColor: "4A7E92", // Light Blue + tagColor: "4A7E92", // Light Blue + tagFontColor: "0F0D0E", // Coffee black + documentColor: "0F0D0E", // Coffee black + titleFontColor: "41A6CE", // Saturated Light Blue + textFontColor: "41A6CE", // Ultra light Blue + chartAxisLineColor: "#374246", + chartAxisFontColor: "rgba(71,121,140, 0.7)", + chartAxisTickFontColor: "rgba(149,181,192, 0.6)", + chartItemFontColor: "rgba(149,181,192, 0.9)", + chartSeriesColor: "rgb(71,113,128)", + chartSeriesBorderColor: "rgba(149,181,192, 0.9)", + highlightFontColor: "" + }, + espresso: { + tableBorderColor: "C7701E", // Orange + tagColor: "C7701E", // Light Blue + tagFontColor: "0F0D0E", // Coffee black + documentColor: "0F0D0E", // Coffee black + titleFontColor: "C7701E", // Saturated Light Blue + textFontColor: "C7701E", // Ultra light Blue + chartAxisLineColor: "#374246", + chartAxisFontColor: "rgba(71,121,140, 0.7)", + chartAxisTickFontColor: "rgba(149,181,192, 0.6)", + chartItemFontColor: "rgba(149,181,192, 0.9)", + chartSeriesColor: "rgb(71,113,128)", + chartSeriesBorderColor: "rgba(149,181,192, 0.9)", + highlightFontColor: "" + }, + latte: { + tableBorderColor: "25110f", // Orange + tagColor: "25110f", // Light Blue + tagFontColor: "F1F0EE", // Coffee black + documentColor: "F1F0EE", // Coffee black + titleFontColor: "25110f", // Saturated Light Blue + textFontColor: "25110f", // Ultra light Blue + chartAxisLineColor: "#374246", + chartAxisFontColor: "rgba(71,121,140, 0.7)", + chartAxisTickFontColor: "rgba(149,181,192, 0.6)", + chartItemFontColor: "rgba(149,181,192, 0.9)", + chartSeriesColor: "rgb(71,113,128)", + chartSeriesBorderColor: "rgba(149,181,192, 0.9)", + highlightFontColor: "" + } +} + +export default docxSettings \ No newline at end of file From 226cee55cfcb9dba9286c43ad2eb47b4e265dd87 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sat, 10 Aug 2024 10:11:18 -0700 Subject: [PATCH 03/23] Charts refactored --- cli/mrcli-company.js | 28 +- package-lock.json | 20 +- package.json | 1 + src/report/charts.js | 552 +++++++++++++++++------------------ src/report/common.js | 2 +- src/report/companies.js | 81 +++-- src/report/dashboard.js | 125 +------- src/report/settings.js | 12 +- src/report/tools.js | 16 +- src/report/widgets/Tables.js | 114 ++++---- 10 files changed, 425 insertions(+), 526 deletions(-) diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 5c50262..71c2d3b 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -69,12 +69,15 @@ function initializeSource() { return { company: [], interactions: [], + allInteractions: [], competitors: { leastSimilar: {}, mostSimilar: {}, + all: [] }, totalInteractions: 0, totalCompanies: 0, + averageInteractionsPerCompany: 0, } } @@ -99,7 +102,7 @@ function getCompetitors(sourceCompany, allCompanies) { const competitorNames = Object.keys(sourceCompany[0].similarity); const allCompetitors = competitorNames.map(competitorName => allCompanies.mrJson.find(company => company.name === competitorName) - ).filter(company => company !== undefined); + ).filter(company => company !== undefined) const mostSimilar = competitorNames.reduce((mostSimilar, competitorName) => { const competitor = allCompanies.mrJson.find(company => company.name === competitorName); @@ -107,10 +110,10 @@ function getCompetitors(sourceCompany, allCompanies) { const similarityScore = sourceCompany[0].similarity[competitorName].similarity; if (!mostSimilar || similarityScore > mostSimilar.similarity) { - return { ...competitor, similarity: similarityScore }; + return { ...competitor, similarity: similarityScore } } - return mostSimilar; - }, null); + return mostSimilar + }, null) const leastSimilar = competitorNames.reduce((leastSimilar, competitorName) => { const competitor = allCompanies.mrJson.find(company => company.name === competitorName); @@ -118,12 +121,12 @@ function getCompetitors(sourceCompany, allCompanies) { const similarityScore = sourceCompany[0].similarity[competitorName].similarity; if (!leastSimilar || similarityScore < leastSimilar.similarity) { - return { ...competitor, similarity: similarityScore }; + return { ...competitor, similarity: similarityScore } } - return leastSimilar; + return leastSimilar }, null); - return { allCompetitors, mostSimilar, leastSimilar }; + return { allCompetitors, mostSimilar, leastSimilar } } async function _prepareData(companyName) { @@ -134,17 +137,16 @@ async function _prepareData(companyName) { source.company = getSourceCompany(allCompanies, companyName) source.totalCompanies = allCompanies.mrJson.length - source.interactions = getInteractions(source.company, allInteractions); + source.interactions = getInteractions(source.company, allInteractions) + source.allInteractions = allInteractions.mrJson source.totalInteractions = source.interactions.length + source.averageInteractionsPerCompany = Math.round(source.totalInteractions / source.totalCompanies) const { allCompetitors, mostSimilar, leastSimilar } = getCompetitors(source.company, allCompanies) source.competitors.all = allCompetitors source.competitors.mostSimilar = mostSimilar source.competitors.leastSimilar = leastSimilar - console.log(JSON.stringify(source, null, 2)) - process.exit(0) - return source } @@ -165,9 +167,7 @@ if (myArgs.report) { // Set up the document controller const docController = new CompanyStandalone( reportData, - myEnv, - 'mediumroast.io barrista robot', // The author - 'Mediumroast, Inc.' // The authoring company/org + myEnv ) if (myArgs.package) { diff --git a/package-lock.json b/package-lock.json index 0ac00af..b3d27d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediumroast_js", - "version": "0.4.46", + "version": "0.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mediumroast_js", - "version": "0.4.46", + "version": "0.7.1", "license": "Apache-2.0", "dependencies": { "@json2csv/plainjs": "^7.0.4", @@ -19,6 +19,7 @@ "commander": "^8.3.0", "configparser": "^0.3.9", "docx": "^7.4.1", + "flatted": "^3.3.1", "inquirer": "^9.1.2", "mediumroast_js": "^0.3.6", "node-fetch": "^3.2.10", @@ -31,10 +32,12 @@ }, "bin": { "mrcli": "cli/mrcli.js", + "mrcli-billing": "cli/mrcli-billing.js", "mrcli-company": "cli/mrcli-company.js", "mrcli-interaction": "cli/mrcli-interaction.js", "mrcli-setup": "cli/mrcli-setup.js", - "mrcli-study": "cli/mrcli-study.js" + "mrcli-study": "cli/mrcli-study.js", + "mrcli-user": "cli/mrcli-user.js" }, "devDependencies": { "@babel/core": "^7.16.12", @@ -3784,6 +3787,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -10063,6 +10072,11 @@ "path-exists": "^4.0.0" } }, + "flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", diff --git a/package.json b/package.json index fb6662f..e1f383c 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "commander": "^8.3.0", "configparser": "^0.3.9", "docx": "^7.4.1", + "flatted": "^3.3.1", "inquirer": "^9.1.2", "mediumroast_js": "^0.3.6", "node-fetch": "^3.2.10", diff --git a/src/report/charts.js b/src/report/charts.js index 6b3870d..aeaa140 100755 --- a/src/report/charts.js +++ b/src/report/charts.js @@ -1,154 +1,182 @@ -#!/usr/bin/env node +/** + * @fileOverview This file contains the Charting class which is used to generate charts for the report + * + * @license Apache-2.0 + * @version 2.0.0 + * + * @author Michael Hay + * @file github.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * + * @module Charting + * @requires axios + * @requires path + * @requires fs + * @requires settings + * @example + * import Charting from './charts.js' + * const myChart = new Charting(env) + * const myData = { + * company: { + * name: "Mediumroast", + * similarities: { + * 'company_1': { + * 'most_similar: 0.5, + * 'least_similar: 0.1 + * }, + * 'company_2': { + * 'most_similar: 0.7, + * 'least_similar: 0.2 + * } + * }, + * } + * const myChartPath = await myChart.bubbleChart(myData) + * console.log(myChartPath) + * +*/ + + import axios from 'axios' import docxSettings from './settings.js' -import { Utilities as CLIUtilities } from '../cli/common.js' +import path from 'path' +import * as fs from 'fs' -// Pickup the general settings -const generalStyle = docxSettings.general -function _transformForBubble(objData) { - let chartData = [] - for(const obj in objData){ - const objName = objData[obj].name - const mostSimilar = (objData[obj].most_similar.score * 100).toFixed(2) - const leastSimilar = (objData[obj].least_similar.score * 100).toFixed(2) - chartData.push([mostSimilar, leastSimilar, objName]) +class Charting { + constructor(env) { + this.env = env + this.generalStyle = docxSettings.general + this.themeStyle = docxSettings[env.theme] + this.baseDir = env.workDir + this.workingImageDir = path.resolve(this.baseDir + '/images') + this.chartDefaults = { + title: { + text: 'Mediumroast for GitHub Chart', + textStyle: { + color: `#${this.themeStyle.titleFontColor}`, + fontFamily: this.generalStyle.heavyFont, + fontSize: this.generalStyle.chartTitleFontSize + }, + left: '5%', + top: '2%', + }, + textStyle: { + fontFamily: this.generalStyle.font, + color: `#${this.themeStyle.titleFontColor}`, + fontSize: this.themeStyle.chartFontSize + }, + imageWidth: 800, + imageHeight: 600, + backgroundColor: `#${this.themeStyle.documentColor}`, + animation: false + } } - return chartData -} -// TODO this isn't really correct or used yet -function _transformForPie(objData) { - let chartData = [] - for(const obj in objData){ - const objName = objData[obj].name - const objScore = (objData[obj].score * 100).toFixed(2) - chartData.push([objName, objScore]) - } - return chartData -} + // -------------------------------------------------------- + // Internal methods _postToChartServer() + // -------------------------------------------------------- -async function _postToChartServer(jsonObj, server) { - const myURL = server - const myHeaders = { - headers: { - 'Content-Type': 'application/json', - } - } - try { - const resp = await axios.post(myURL, jsonObj, myHeaders) - return [true, {status_code: resp.status, status_msg: resp.statusText}, resp.data] - } catch (err) { - return [false, err, err.response] - } -} + async _postToChartServer(jsonObj, server) { + const myURL = `${server}` + const myHeaders = { + headers: { + 'Content-Type': 'application/json', + } + } + try { + const resp = await axios.post(myURL, jsonObj, {...myHeaders, responseType: 'arraybuffer'}) + return [true, {status_code: resp.status, status_msg: resp.headers}, resp.data] + } catch (err) { + return [false, err, err.response] + } -function _transformForRadar(company, competitors) { - // Build a lookup table - const lookup = { - 'General Notes': 'General', - 'Frequently Asked Questions': 'General', - 'White Paper': 'Article', - 'Case Study': 'Article', - 'Public Company Filing': 'General', - 'Patent': 'General', - 'Press Release': 'Article', - 'Blog Post': 'Social', - 'Social Media Post(s)': 'Social', - 'Product Document': 'Product/Service', - 'Service Document': 'Product/Service', - 'Transcript': 'General', - 'Article': 'Article', - 'About the company': 'About', - 'Research Paper': 'General', - } - // Define the model for recording the scores for the radar chart - let counts = { - 'General': 0, - 'Article': 0, - 'Social': 0, - 'Product/Service': 0, - 'About': 0 } - let competitorQualities = competitors.map( - (competitor) => { - return competitor.company.quality - } - ) - competitorQualities.push(company.quality) - for (const quality in competitorQualities) { - const qualities = Object.keys(competitorQualities[quality]) - for(const qualityType in qualities) { - counts[lookup[qualities[qualityType]]] += competitorQualities[quality][qualities[qualityType]] + // -------------------------------------------------------- + // External method: bubbleChart() + // -------------------------------------------------------- + + _transformForBubble(objData) { + let chartData = [] + const similarityData = objData.similarities + for(const obj in similarityData){ + const objName = similarityData[obj].name + const mostSimilar = (similarityData[obj].most_similar.score * 100).toFixed(2) + const leastSimilar = (similarityData[obj].least_similar.score * 100).toFixed(2) + chartData.push([mostSimilar, leastSimilar, objName]) } + // Add the company to the end of the list + chartData.push([100, 100, objData.company.name]) + return chartData } - return counts -} - -// -------------------------------------------------------- -// External methods: bubbleChart(), radardChart() -// -------------------------------------------------------- -/** - * @async - * @function bubbleChart - * @description Generates a bubble chart from the supplied data following the configured theme - * @param {Object} objData - data sent to the chart that will be plotted - * @param {Object} env - an object containing key environmental variables for the CLI - * @param {String} baseDir - directory used to store the working files, in this case the chart images - * @param {String} chartTitle - title for the chart, default Similarity Landscape - * @param {String} xAxisTitle - x-axis title for the chart, default Most similar score - * @param {Sting} yAxisTitle - y-axis title for the chart, default Least similar score - * @param {String} chartFile - file name for the chart, default similarity_bubble_chart.png - */ -export async function bubbleChart ( - objData, - env, - baseDir, - chartTitle='Similarity Landscape', - xAxisTitle='Most similar score', - yAxisTitle='Least similar score', - chartFile='similarity_bubble_chart.png' -) { - // Construct the CLIUtilities object - const cliUtil = new CLIUtilities() + /** + * @async + * @function bubbleChart + * @description Generates a bubble chart from the supplied data following the configured theme + * @param {Object} objData - data sent to the chart that will be plotted + * @param {Object} options - optional parameters for the chart + * @returns {String} - the full path to the generated chart image + * @example + * const myChart = new Charting(env) + * const myData = { + * company: { + * name: "Mediumroast", + * similarities: { + * 'company_1': { + * 'most_similar: 0.5, + * 'least_similar: 0.1 + * }, + * 'company_2': { + * 'most_similar: 0.7, + * 'least_similar: 0.2 + * } + * }, + * } + * const myChartPath = await myChart.bubbleChart(myData) + * console.log(myChartPath) + */ + async bubbleChart (objData, options={}) { + // Destructure the options + const { + chartTitle='Similarity Landscape', + xAxisTitle='Most Similar Score', + yAxisTitle='Least Similar Score', + chartFile='similarity_bubble_chart.png' + } = options - // Pick up the settings including those from the theme - const themeStyle = docxSettings[env.theme] + // Change the originating data into data aligned to the bubble chart + const bubbleSeries = this._transformForBubble(objData) - // Change the originating data into data aligned to the bubble chart - const myData = _transformForBubble(objData) + const myData = [ + { + name: "bubble", + data: bubbleSeries, + type: "scatter", + symbolSize: [this.generalStyle.chartSymbolSize,this.generalStyle.chartSymbolSize], + itemStyle: { + borderColor: this.themeStyle.chartSeriesBorderColor, + borderWidth: 1, + color: this.themeStyle.chartSeriesColor + }, + label: { + show: true, + formatter: "{@[2]}", + position: "left", + color: this.themeStyle.chartItemFontColor, + backgroundColor: `#${this.themeStyle.documentColor}`, + padding: [5,4], + } + } + ] - // Construct the chart object - let myChart = { - title: { - text: chartTitle, // For some reason the chart title isn't displaying - textStyle: { - color: "#" + themeStyle.titleFontColor, - fontFamily: generalStyle.font, - fontSize: generalStyle.chartTitleFontSize - }, - left: '5%', - top: '2%' - }, - textStyle: { - fontFamily: generalStyle.font, - color: "#" + themeStyle.titleFontColor, - fontSize: themeStyle.chartFontSize - }, - imageWidth: 600, - imageHeight: 500, - backgroundColor: "#" + themeStyle.documentColor, - // color: "#47798c", // TODO check to see what this does, if not needed deprecate - animation: false, - xAxis: { + const xAxis = { axisLine: { lineStyle: { - color: themeStyle.chartAxisLineColor, + color: this.themeStyle.chartAxisLineColor, width: 1, opacity: 0.95, type: "solid" @@ -159,29 +187,30 @@ export async function bubbleChart ( nameLocation: "center", nameGap: 35, nameTextStyle: { - color: themeStyle.chartAxisFontColor, - fontFamily: generalStyle.font, - fontSize: generalStyle.chartAxesFontSize + color: this.themeStyle.chartAxisFontColor, + fontFamily: this.generalStyle.font, + fontSize: this.generalStyle.chartAxesFontSize, }, axisLabel: { - color: themeStyle.chartAxisTickFontColor, - fontFamily: generalStyle.font, - fontSize: generalStyle.chartTickFontSize + color: this.themeStyle.chartAxisTickFontColor, + fontFamily: this.generalStyle.font, + fontSize: this.generalStyle.chartTickFontSize }, show: true, splitLine: { show: true, lineStyle: { type: "dashed", - color: "#" + themeStyle.chartAxisLineColor, + color: "#" + this.themeStyle.chartAxisLineColor, width: 0.5 } }, - }, - yAxis: { + } + + const yAxis = { axisLine: { lineStyle: { - color: themeStyle.chartAxisLineColor, + color: this.themeStyle.chartAxisLineColor, width: 1, opacity: 0.95, type: "solid" @@ -192,176 +221,119 @@ export async function bubbleChart ( name: yAxisTitle, nameLocation: "center", nameTextStyle: { - color: themeStyle.chartAxisFontColor, - fontFamily: generalStyle.font, - fontSize: generalStyle.chartAxesFontSize + color: this.themeStyle.chartAxisFontColor, + fontFamily: this.generalStyle.font, + fontSize: this.generalStyle.chartAxesFontSize }, axisLabel: { - color: themeStyle.chartAxisTickFontColor, - fontFamily: generalStyle.font, - fontSize: generalStyle.chartTickFontSize + color: this.themeStyle.chartAxisTickFontColor, + fontFamily: this.generalStyle.font, + fontSize: this.generalStyle.chartTickFontSize }, show: true, splitLine: { show: true, lineStyle: { type: "dashed", - color: themeStyle.chartAxisLineColor, + color: this.themeStyle.chartAxisLineColor, width: 0.75 }, scale: true }, - }, - "series": [ - { - name: "bubble", - data: myData, - type: "scatter", - symbolSize: [generalStyle.chartSymbolSize,generalStyle.chartSymbolSize], - itemStyle: { - borderColor: themeStyle.chartSeriesBorderColor, - borderWidth: 1, - color: themeStyle.chartSeriesColor - }, - label: { - show: true, - formatter: "{@[2]}", - position: "left", - color: themeStyle.chartItemFontColor, - } - } - ] + } + + let myChart = this.chartDefaults + myChart.series = myData + myChart.title.text = chartTitle + myChart.xAxis = xAxis + myChart.yAxis = yAxis + + // Send to the chart server + const putResult = await this._postToChartServer(myChart, this.env.echartsServer) + const myFullPath = path.resolve(this.workingImageDir, chartFile) + fs.writeFileSync(myFullPath, putResult[2]) + return myFullPath } - // Send to the chart server - const putResult = await _postToChartServer(myChart, env.echartsServer) - // Destructure the response into the URL for the created chart - const imageURL = env.echartsServer + '/' + putResult[2].filename - // Download the chart to the proper location - return await cliUtil.downloadImage(imageURL, baseDir + '/images', chartFile) -} -export async function radarChart ( - objData, - env, - baseDir, - chartFile='interaction_radar_chart.png', - seriesName="Quality by average", - chartTitle="Overall interaction quality by category", - dataName="Comparison population", - standards={total: 15} -) { - // Construct the CLIUtilities object - const cliUtil = new CLIUtilities() - // Transform data for the chart - let myQualityCounts = _transformForRadar(objData.company, objData.competitors) - // Compute and normalize the total number of interactions - // Total - const standardTotal = Math.round(objData.stats.averageStats / standards.total) * 100 - // Normalize by total - const myTotal = objData.stats.totalStats - const myCategories = Object.keys(myQualityCounts) - for (const category in myCategories) { - myQualityCounts[myCategories[category]] = Math.round(myQualityCounts[myCategories[category]] / myTotal * 100) + // -------------------------------------------------------- + // External method: pieChart() + // Internal method: _transformForPie() + // -------------------------------------------------------- + _transformForPie(objData) { + let chartData = [] + for(const obj in objData){ + chartData.push({name: obj, value: objData[obj]}) + } + return chartData } - // Pick up the settings including those from the theme - const generalStyle = docxSettings.general - const themeStyle = docxSettings[env.theme] + /** + * @async + * @function pieChart + * @description Generates a pie chart from the supplied data following the configured theme + * @param {Object} objData - data sent to the chart that will be plotted + * @returns {String} - the full path to the generated chart image + * @example + * const myChart = new Charting(env) + * const myData = { + * company: { + * quality: { + * 'good': 10, + * 'bad': 5, + * 'ugly': 2 + * } + * } + * const myChartPath = await myChart.pieChart(myData) + * console.log(myChartPath) + */ - + async pieChart (objData, options={}) { + // Desctructure the options + const { + chartTitle='Interaction Characterization', + seriesName='Interaction Types', + chartFile='interaction_pie_chart.png' + } = options - const myData = { - radar: { - shape: 'circle', - indicator: [ - { name: 'Total', max: 100, min: 0 }, // 15 * N would be the total max or 100% - { name: 'Product/Service', max: 20, min: 0 }, // 20% for each category - { name: 'Article', max: 20, min: 0 }, // 20% for each category - { name: 'Social', max: 20, min: 0 }, // 20% for each category - { name: 'About', max: 20, min: 0 }, // 20% for each category - { name: 'General', max: 20, min: 0 } // 20% for each category - ], - axisLine: { - lineStyle: { - color: themeStyle.chartAxisLineColor, - width: 1, - opacity: 0.95, - type: "solid" - } - }, - splitArea: { - show: true, - areaStyle: { - color: ['rgba(156,184,200, 0.1)','rgba(156,184,200, 0.09)'] - } - }, - radius: "75%", - center: ["50%","54%"], - splitLine: { - show: true, - lineStyle: { - type: "dashed", - color: [themeStyle.chartAxisLineColor], - width: 0.75 + // Transform data for the chart + const qualitySeries = this._transformForPie(objData.company.quality) + + const myData = [ + { + name: seriesName, + type: 'pie', + radius: '55%', + center: ['50%', '50%'], + data: qualitySeries.sort(function (a, b) { return a.value - b.value; }), + roseType: 'radius', + itemStyle: { + color: this.themeStyle.chartSeriesColor, + borderColor: this.themeStyle.chartSeriesBorderColor, + }, + areaStyle: { + opacity: 0.45 + }, + label: { + show: true, + formatter: '{b}: {c} ({d}%)', + color: this.themeStyle.chartItemFontColor, + backgroundColor: `#${this.themeStyle.documentColor}`, + padding: [5,4], + }, } - }, - }, - series: [ - { - name: seriesName, - type: 'radar', - data:[{ - value: [ - standardTotal, - myQualityCounts['Product/Service'], - myQualityCounts['Article'], - myQualityCounts['Social'], - myQualityCounts['About'], - myQualityCounts['General'] - ], // these should be averages - name: dataName - }], - itemStyle: { - color: themeStyle.chartSeriesColor, - borderColor: themeStyle.chartSeriesBorderColor, - }, - areaStyle: { - opacity: 0.45 - }, - symbol: 'none' - } - ], - } - let myChart = { - title: { - text: chartTitle, - textStyle: { - color: "#" + themeStyle.titleFontColor, - fontFamily: generalStyle.heavyFont, - fontSize: generalStyle.chartTitleFontSize - }, - left: '5%', - top: '2%', - }, - textStyle: { - fontFamily: generalStyle.font, - color: "#" + themeStyle.titleFontColor, - fontSize: themeStyle.chartFontSize - }, - imageWidth: 800, - imageHeight: 500, - backgroundColor: "#" + themeStyle.documentColor, - // color: "#47798c", - animation: false, - radar: myData.radar, - series: myData.series + ] + + let myChart = this.chartDefaults + myChart.series = myData + myChart.title.text = chartTitle + + const putResult = await this._postToChartServer(myChart, this.env.echartsServer) + const myFullPath = path.resolve(this.workingImageDir, chartFile) + fs.writeFileSync(myFullPath, putResult[2]) + return myFullPath } - const putResult = await _postToChartServer(myChart, env.echartsServer) - let imageURL = env.echartsServer + '/' + putResult[2].filename - return await cliUtil.downloadImage(imageURL, baseDir + '/images', chartFile) + } -export async function pieChart () { - // TODO -} \ No newline at end of file +export default Charting \ No newline at end of file diff --git a/src/report/common.js b/src/report/common.js index ec35b85..85229f3 100644 --- a/src/report/common.js +++ b/src/report/common.js @@ -40,7 +40,7 @@ class DOCXUtilities { this.fullFontSize = docxSettings.general.fullFontSize this.fontFactor = docxSettings.general.fontFactor this.theme = this.env.theme - this.documentColor = docxSettings[this.theme].documentColor + this.documentColor = this.themeSettings.documentColor this.textFontColor = `#${docxSettings[this.theme].textFontColor.toLowerCase()}` this.titleFontColor = `#${docxSettings[this.theme].titleFontColor.toLowerCase()}` this.tableBorderColor = `#${docxSettings[this.theme].tableBorderColor.toLowerCase()}` diff --git a/src/report/companies.js b/src/report/companies.js index af48aa9..29eeb74 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -15,6 +15,7 @@ import { InteractionSection } from './interactions.js' import { CompanyDashbord } from './dashboard.js' import { Utilities as CLIUtilities } from '../cli/common.js' import { getMostSimilarCompany } from './tools.js' +import TextWidgets from './widgets/Text.js' class BaseCompanyReport { constructor(company, env) { @@ -27,6 +28,7 @@ class BaseCompanyReport { this.baseDir = this.env.outputDir this.workDir = this.env.workDir this.baseName = company.name.replace(/ /g,"_") + this.textWidgets = new TextWidgets(env) } } @@ -45,6 +47,7 @@ class CompanySection extends BaseCompanyReport { */ constructor(company, env) { super(company, env) + console.log('CompanySection constructor') } // Create a URL on Google maps to search for the address @@ -334,14 +337,14 @@ class CompanyStandalone extends BaseCompanyReport { * @todo Adapt to settings.js for consistent application of settings, follow dashboard.js */ constructor(sourceData, env, author='Mediumroast for GitHub') { - super(sourceData.company, env) + super(sourceData.company[0], env) this.objectType = 'Company' this.creator = author this.author = author this.authoredBy = author this.title = `${this.company.name} Company Report` - this.interactions = this.company.interactions - this.competitors = competitors + this.interactions = sourceData.allInteractions + this.competitors = sourceData.competitors.all this.description = `A Company report for ${this.company.name} and including relevant company data.` this.introduction = `The mediumroast.io system automatically generated this document. It includes key metadata for this Company object and relevant summaries and metadata from the associated interactions. If this report document is produced as a package, instead of standalone, then the @@ -349,8 +352,9 @@ class CompanyStandalone extends BaseCompanyReport { package is opened.` this.similarity = this.company.similarity, this.noInteractions = String(Object.keys(this.company.linked_interactions).length) - this.totalInteractions = this.company.totalInteractions - this.totalCompanies = this.company.totalCompanies + this.totalInteractions = sourceData.totalInteractions + this.totalCompanies = sourceData.totalCompanies + this.averageInteractions = sourceData.averageInteractionsPerCompany } /** @@ -383,43 +387,38 @@ class CompanyStandalone extends BaseCompanyReport { const preparedFor = `${this.authoredBy} report for: ` // Construct the company section - const companySection = new CompanySection(this.company, this.baseDir, env) - const interactionSection = new InteractionSection( - this.interactions, - this.company.name, - this.objectType, - this.env - ) + // const companySection = new CompanySection(this.company, this.env) + // const interactionSection = new InteractionSection( + // this.interactions, + // this.company.name, + // this.objectType, + // this.env + // ) const myDash = new CompanyDashbord(this.env) - // Construct the interactions section - // const interactionsSection = new InteractionSection(this.interactions) - // Set up the default options for the document - const myDocument = [].concat( - this.util.makeIntro(this.introduction), - [ - this.util.makeHeading1('Company Detail'), - companySection.makeFirmographicsDOCX(), - this.util.makeHeading1('Comparison') - ], - companySection.makeComparisonDOCX(this.similarity, this.competitors), - [ this.util.makeHeading1('Topics'), - this.util.makeParagraph( - 'The following topics were automatically generated from all ' + - this.noInteractions + ' interactions associated to this company.' - ), - // this.util.makeHeading2('Topics Table'), - // this.util.topicTable(this.topics), - this.util.makeHeadingBookmark1('Interaction Summaries', 'interaction_summaries') - ], - ...interactionSection.makeDescriptionsDOCX(), - await companySection.makeCompetitorsDOCX(this.competitors, isPackage), - [ this.util.pageBreak(), - this.util.makeHeading1('References') - ], - ...interactionSection.makeReferencesDOCX(isPackage) - ) + // const myDocument = [].concat( + // this.util.makeIntro(this.introduction), + // [ + // this.util.makeHeading1('Company Detail'), + // companySection.makeFirmographicsDOCX(), + // this.util.makeHeading1('Comparison') + // ], + // companySection.makeComparisonDOCX(this.similarity, this.competitors), + // [ this.util.makeHeading1('Topics'), + // this.util.makeParagraph( + // 'The following topics were automatically generated from all ' + + // this.noInteractions + ' interactions associated to this company.' + // ), + // this.util.makeHeadingBookmark1('Interaction Summaries', 'interaction_summaries') + // ], + // ...interactionSection.makeDescriptionsDOCX(), + // await companySection.makeCompetitorsDOCX(this.competitors, isPackage), + // [ this.util.pageBreak(), + // this.util.makeHeading1('References') + // ], + // ...interactionSection.makeReferencesDOCX(isPackage) + // ) // Construct the document const myDoc = new docx.Document ({ @@ -453,7 +452,7 @@ class CompanyStandalone extends BaseCompanyReport { await myDash.makeDashboard( this.company, this.competitors, - this.baseDir + this.workDir ) ], }, @@ -467,7 +466,7 @@ class CompanyStandalone extends BaseCompanyReport { children: [this.util.makeFooter(authoredBy, preparedOn)] }) }, - children: myDocument, + children: [this.textWidgets.makeParagraph('Table of Contents')], } ], }) diff --git a/src/report/dashboard.js b/src/report/dashboard.js index 99a9f73..8299d5d 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -12,7 +12,7 @@ import docx from 'docx' import * as fs from 'fs' import DOCXUtilities from './common.js' import docxSettings from './settings.js' -import { bubbleChart, radarChart } from './charts.js' +import Charting from './charts.js' class Dashboards { /** @@ -79,6 +79,8 @@ class Dashboards { top: this.borderStyle, bottom: this.borderStyle } + + // TODO need baseDir to be passed in or defined in the constructor } @@ -422,64 +424,6 @@ class CompanyDashbord extends Dashboards { constructor(env) { super(env) } - // constructor(env) { - // this.env = env - // this.util = new DOCXUtilities(env) - // this.themeStyle = docxSettings[env.theme] // Set the theme for the report - // this.generalStyle = docxSettings.general // Pull in all of the general settings - - // // Define specifics for table borders - // this.noneStyle = { - // style: this.generalStyle.noBorderStyle - // } - // this.borderStyle = { - // style: this.generalStyle.tableBorderStyle, - // size: this.generalStyle.tableBorderSize, - // color: this.themeStyle.tableBorderColor - // } - // // No borders - // this.noBorders = { - // left: this.noneStyle, - // right: this.noneStyle, - // top: this.noneStyle, - // bottom: this.noneStyle - // } - // // Right border only - // this.rightBorder = { - // left: this.noneStyle, - // right: this.borderStyle, - // top: this.noneStyle, - // bottom: this.noneStyle - // } - // // Bottom border only - // this.bottomBorder = { - // left: this.noneStyle, - // right: this.noneStyle, - // top: this.noneStyle, - // bottom: this.borderStyle - // } - // // Bottom and right borders - // this.bottomAndRightBorders = { - // left: this.noneStyle, - // right: this.borderStyle, - // top: this.noneStyle, - // bottom: this.borderStyle - // } - // // Top and right borders - // this.topAndRightBorders = { - // left: this.noneStyle, - // right: this.borderStyle, - // top: this.borderStyle, - // bottom: this.noneStyle - // } - // // All borders, helpful for debugging - // this.allBorders = { - // left: this.borderStyle, - // right: this.borderStyle, - // top: this.borderStyle, - // bottom: this.borderStyle - // } - // } /** * @@ -954,36 +898,6 @@ class CompanyDashbord extends Dashboards { return mostSimilarCompany[0] } - // Compute interaction descriptive statistics - _computeInteractionStats(company, competitors) { - // Pull out company interactions - const companyInteractions = Object.keys(company.linked_interactions).length - - // Get the total number of interactions - // Add the current company interactions to the total - let totalInteractions = companyInteractions - // Sum all other companies' interactions - for(const competitor in competitors) { - totalInteractions += Object.keys(competitors[competitor].company.linked_interactions).length - } - - // Compute the average interactions per company - const totalCompanies = 1 + competitors.length - const averageInteractions = Math.round(totalInteractions/totalCompanies) - - // Return the result - return { - totalStatsTitle: "Total Interactions", - totalStats: totalInteractions, - averageStatsTitle: "Average Interactions/Company", - averageStats: averageInteractions, - companyStatsTitle: "Interactions", - companyStats: companyInteractions, - totalCompaniesTitle: "Total Companies", - totalCompanies: totalCompanies - } - } - /** * * @param {*} paragraph @@ -1036,30 +950,15 @@ class CompanyDashbord extends Dashboards { * @returns */ async makeDashboard(company, competitors, baseDir) { + // Construct the Charting class + const charting = new Charting(this.env) // Create the bubble chart from the company comparisons - const bubbleChartFile = await bubbleChart( - company.comparison, - this.env, - baseDir - ) - // Find the most similar company - const mostSimilarCompany = this._getMostSimilarCompany( - company.comparison, - competitors - ) - // Pull in the relevant interactions from the most similar company - const mostLeastSimilarInteractions = { - most_similar: mostSimilarCompany.mostSimilar.interaction, - least_similar: mostSimilarCompany.leastSimilar.interaction - } - // Compute the descriptive statistics for interactions - const myStats = this._computeInteractionStats(company,competitors) - // TODO change to pie chart - const radarChartFile = await radarChart( - {company: company, competitors: competitors, stats: myStats}, - this.env, - baseDir - ) + const bubbleChartFile = await charting.bubbleChart({similarities: company.similarity, company: company}) + + // Create the pie chart for interaction characterization + const pieChartFile = await charting.pieChart({company: company}) + + process.exit(0) /** * NOTICE * I believe that there is a potential bug in node.js filesystem module. @@ -1090,7 +989,7 @@ class CompanyDashbord extends Dashboards { 'scratch_chart.png' ) let myRows = [ - this.firstRow(bubbleChartFile, radarChartFile, myStats), + this.firstRow(bubbleChartFile, pieChartFile, myStats), this.shellRow("companyDesc", mostSimilarCompany), this.shellRow("docDesc", null, mostLeastSimilarInteractions), ] diff --git a/src/report/settings.js b/src/report/settings.js index 59dadc4..12e3064 100644 --- a/src/report/settings.js +++ b/src/report/settings.js @@ -65,12 +65,12 @@ const docxSettings = { documentColor: "F1F0EE", // Coffee black titleFontColor: "25110f", // Saturated Light Blue textFontColor: "25110f", // Ultra light Blue - chartAxisLineColor: "#374246", - chartAxisFontColor: "rgba(71,121,140, 0.7)", - chartAxisTickFontColor: "rgba(149,181,192, 0.6)", - chartItemFontColor: "rgba(149,181,192, 0.9)", - chartSeriesColor: "rgb(71,113,128)", - chartSeriesBorderColor: "rgba(149,181,192, 0.9)", + chartAxisLineColor: "#25110f", + chartAxisFontColor: "rgba(37,17,15, 0.7)", + chartAxisTickFontColor: "rgba(37,17,15, 0.6)", + chartItemFontColor: "rgba(37,17,15, 0.9)", + chartSeriesColor: "rgb(27,12,10, 0.7)", + chartSeriesBorderColor: "rgba(27,12,10, 0.9)", highlightFontColor: "" } } diff --git a/src/report/tools.js b/src/report/tools.js index 3b293ca..826835b 100644 --- a/src/report/tools.js +++ b/src/report/tools.js @@ -12,29 +12,29 @@ import FilesystemOperators from '../cli/filesystem.js' /** * @function getMostSimilarCompany * @description Find the closest competitor using the Euclidean distance - * @param {Object} comparisons - the comparisons from a company object + * @param {Object} similarities - the similarities from a company object * @returns {Object} An array containing a section description and a table of interaction descriptions */ -export function getMostSimilarCompany(comparisons, companies) { +export function getMostSimilarCompany(similarities, companies) { // x1 = 1 and y1 = 1 because this the equivalent of comparing a company to itself const x1 = 1 const y1 = 1 let distanceToCompany = {} let companyToDistance = {} - for(const companyId in comparisons) { + for(const companyName in similarities) { // Compute the distance using d = sqrt((x2 - x1)^2 + (y2 - y1)^2) const myDistance = Math.sqrt( - (comparisons[companyId].most_similar.score - x1) ** 2 + - (comparisons[companyId].least_similar.score - y1) ** 2 + (similarities[companyName].most_similar.score - x1) ** 2 + + (similarities[companyName].least_similar.score - y1) ** 2 ) - distanceToCompany[myDistance] = companyId - companyToDistance[companyId] = myDistance + distanceToCompany[myDistance] = companyName + companyToDistance[companyName] = myDistance } // Obtain the closest company using max, note min returns the least similar const mostSimilarId = distanceToCompany[Math.max(...Object.keys(distanceToCompany))] // Get the id for the most similar company const mostSimilarCompany = companies.filter(company => { - if (parseInt(company.company.id) === parseInt(mostSimilarId)) { + if (parseInt(company.name) === parseInt(mostSimilarId)) { return company } }) diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index f3c60c2..1527d69 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -3,7 +3,7 @@ import Widgets from './Widgets.js' import TextWidgets from './Text.js' import docx from 'docx' -class TableWidget extends Widgets { +class TableWidgets extends Widgets { constructor(env) { super(env) // Define specifics for table borders @@ -76,6 +76,7 @@ class TableWidget extends Widgets { allColumnsBold = false, allBorders = false, bottomBorders = true, + lastCellBottomRightBorders = false } = options // Set the first column to bold if all columns are bold @@ -84,11 +85,17 @@ class TableWidget extends Widgets { } // Set the border style - let borderStyle = this.noBorders + let leftBorderStyle = this.noBorders + let rightBorderStyle = this.noBorders if (allBorders) { - borderStyle = this.allBorders + leftBorderStyle = this.allBorders + rightBorderStyle = this.allBorders } else if (bottomBorders) { - borderStyle = this.bottomBorder + leftBorderStyle = this.bottomBorder + rightBorderStyle = this.bottomBorder + } else if (lastCellBottomRightBorders) { + leftBorderStyle = this.bottomBorder + rightBorderStyle = this.bottomAndRightBorders } // Destructure the cols array @@ -103,7 +110,7 @@ class TableWidget extends Widgets { type: docx.WidthType.PERCENTAGE }, children: [TextWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], - borders: borderStyle + borders: leftBorderStyle }), new docx.TableCell({ width: { @@ -111,7 +118,7 @@ class TableWidget extends Widgets { type: docx.WidthType.PERCENTAGE }, children: [TextWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], - borders: borderStyle + borders: rightBorderStyle }) ] }) @@ -130,6 +137,7 @@ class TableWidget extends Widgets { allColumnsBold = false, allBorders = false, bottomBorders = true, + lastCellBottomRightBorders = false } = options // Set the first column to bold if all columns are bold @@ -138,11 +146,17 @@ class TableWidget extends Widgets { } // Set the border style - let borderStyle = this.noBorders + let leftBorderStyle = this.noBorders + let rightBorderStyle = this.noBorders if (allBorders) { - borderStyle = this.allBorders + leftBorderStyle = this.allBorders + rightBorderStyle = this.allBorders } else if (bottomBorders) { - borderStyle = this.bottomBorder + leftBorderStyle = this.bottomBorder + rightBorderStyle = this.bottomBorder + } else if (lastCellBottomRightBorders) { + leftBorderStyle = this.bottomBorder + rightBorderStyle = this.bottomAndRightBorders } // Destructure the cols array @@ -156,6 +170,7 @@ class TableWidget extends Widgets { style: 'Hyperlink', font: this.generalSettings.font, size: this.generalSettings.fullFontSize, + bold: allColumnsBold }) ], link: col2Hyperlink @@ -169,14 +184,16 @@ class TableWidget extends Widgets { size: 20, type: docx.WidthType.PERCENTAGE }, - children: [this.makeParagraph(col1, this.fontFactor * this.fontSize, true)] + children: [TextWidgets.makeParagraph(col1, {fontSize: this.generalSettings.fullFontSize, bold: firstColumnBold})], + borders: leftBorderStyle }), new docx.TableCell({ width: { size: 80, type: docx.WidthType.PERCENTAGE }, - children: [new docx.Paragraph({children:[myUrl]})] + children: [new docx.Paragraph({children:[myUrl]})], + borders: rightBorderStyle }) ] }) @@ -243,6 +260,9 @@ class TableWidget extends Widgets { }) } + /** + * Begin functions for completely defining tables with a particular structure + */ _tagCell(tag) { return new docx.TableCell({ margins: { @@ -332,17 +352,43 @@ class TableWidget extends Widgets { return myTable } - -/** - * Begin functions for completely defining tables with a particular structure -*/ + /** + * @function simpleDescriptiveTable + * @param {String} title - the title of the table + * @param {String} text - the text for the table + * @returns {Object} a docx Table object + * @example + * const myTable = simpleDescriptiveTable("Title", "Text") + * + * Will return a table with the title in the first column and the text in the second column, like this: + * + * ------------------------------------- + * | Title | Text | + * ------------------------------------- + */ + simpleDescriptiveTable(title, text) { + return new docx.Table({ + columnWidths: [95], + margins: { + left: this.generalStyle.tableMargin, + right: this.generalStyle.tableMargin, + bottom: this.generalStyle.tableMargin, + top: this.generalStyle.tableMargin + }, + rows: [this.twoColumnRowBasic([title, text], {firstColumnBold: true, allColumnsBold: false, lastCellBottomRightBorders: true})], + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) + } + /** * * @function descriptiveStatisticsTable - * @param {*} statistics + * @param {Object} statistics * @returns - * @todo turn into a loop instead of having the code repeated * @todo if the length of any number is greater than 3 digits shrink the font size by 15% and round down * @todo add a check for the length of the title and shrink the font size by 15% and round down * @todo add a check for the length of the value and shrink the font size by 15% and round down @@ -407,38 +453,6 @@ class TableWidget extends Widgets { } }) } - - twoCellRow (name, data) { - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE - }, - children: [this.util.makeParagraph(name, {fontSize: this.generalStyle.dashFontSize, bold: true})], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin - }, - }), - new docx.TableCell({ - width: { - size: 80, - type: docx.WidthType.PERCENTAGE - }, - children: [this.util.makeParagraph(data, {fontSize: this.generalStyle.dashFontSize})], - borders: this.bottomAndRightBorders, - margins: { - top: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin - }, - }) - ] - }) - } } -export default TableWidget \ No newline at end of file +export default TableWidgets \ No newline at end of file From 9837915684ef797b0698030f7bd2c4f6fbbc6525 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sat, 10 Aug 2024 15:55:17 -0700 Subject: [PATCH 04/23] Page 1 of company report --- cli/mrcli-company.js | 2 +- src/report/companies.js | 35 +- src/report/dashboard.js | 613 +++++---------------------------- src/report/widgets/Tables.js | 209 +++++++++-- src/report/widgets/Widgets.js | 2 +- src/report/widgets/settings.js | 78 ----- 6 files changed, 283 insertions(+), 656 deletions(-) delete mode 100644 src/report/widgets/settings.js diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 71c2d3b..869b9ac 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -139,7 +139,7 @@ async function _prepareData(companyName) { source.interactions = getInteractions(source.company, allInteractions) source.allInteractions = allInteractions.mrJson - source.totalInteractions = source.interactions.length + source.totalInteractions = allInteractions.mrJson.length source.averageInteractionsPerCompany = Math.round(source.totalInteractions / source.totalCompanies) const { allCompetitors, mostSimilar, leastSimilar } = getCompetitors(source.company, allCompanies) diff --git a/src/report/companies.js b/src/report/companies.js index 29eeb74..4bd47b3 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -338,6 +338,7 @@ class CompanyStandalone extends BaseCompanyReport { */ constructor(sourceData, env, author='Mediumroast for GitHub') { super(sourceData.company[0], env) + this.sourceData = sourceData this.objectType = 'Company' this.creator = author this.author = author @@ -419,7 +420,7 @@ class CompanyStandalone extends BaseCompanyReport { // ], // ...interactionSection.makeReferencesDOCX(isPackage) // ) - + // Construct the document const myDoc = new docx.Document ({ creator: this.creator, @@ -451,23 +452,27 @@ class CompanyStandalone extends BaseCompanyReport { children: [ await myDash.makeDashboard( this.company, - this.competitors, - this.workDir + {mostSimilar: this.sourceData.competitors.mostSimilar, leastSimilar: this.sourceData.competitors.leastSimilar}, + this.interactions, + this.noInteractions, + this.totalInteractions, + this.totalCompanies, + this.averageInteractions ) ], }, - { - properties: {}, - headers: { - default: this.util.makeHeader(this.company.name, preparedFor) - }, - footers: { - default: new docx.Footer({ - children: [this.util.makeFooter(authoredBy, preparedOn)] - }) - }, - children: [this.textWidgets.makeParagraph('Table of Contents')], - } + // { + // properties: {}, + // headers: { + // default: this.util.makeHeader(this.company.name, preparedFor) + // }, + // footers: { + // default: new docx.Footer({ + // children: [this.util.makeFooter(authoredBy, preparedOn)] + // }) + // }, + // children: [this.textWidgets.makeParagraph('Table of Contents')], + // } ], }) diff --git a/src/report/dashboard.js b/src/report/dashboard.js index 8299d5d..c718f1a 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -13,6 +13,7 @@ import * as fs from 'fs' import DOCXUtilities from './common.js' import docxSettings from './settings.js' import Charting from './charts.js' +import TableWidgets from './widgets/Tables.js' class Dashboards { /** @@ -25,6 +26,8 @@ class Dashboards { constructor(env) { this.env = env this.util = new DOCXUtilities(env) + this.charting = new Charting(env) + this.tableWidgets = new TableWidgets(env) this.themeStyle = docxSettings[env.theme] // Set the theme for the report this.generalStyle = docxSettings.general // Pull in all of the general settings @@ -221,6 +224,11 @@ class Dashboards { return shortText } + // Create a function called truncate text that takes a string and an integer called numChars, and returns the first numChars of the string + truncateText(text, numChars=107) { + return `${text.substring(0, numChars)}...` + } + /** * * @param {*} imageFile @@ -419,528 +427,38 @@ class CompanyDashbord extends Dashboards { * @constructor * @classdesc To operate this class the constructor should be passed a the environmental setting for the object. * @param {Object} env - Environmental variable settings for the CLI environment - * @param {String} theme - Governs the color of the dashboard, be either coffee or latte */ constructor(env) { super(env) } - /** - * - * @param {*} statistics - * @returns - * @todo turn into a loop instead of having the code repeated - * @todo if the length of any number is greater than 3 digits shrink the font size by 15% and round down - */ - _statisticsTable(statistics) { - const myRows = [ - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.companyStats, - this.generalStyle.metricFontSize, - this.themeStyle.titleFontColor, - 0, - true, - true, - this.generalStyle.heavyFont - ) - ], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.companyStatsTitle, - this.generalStyle.metricFontTitleSize, - this.themeStyle.titleFontColor, - 0, - false, - true - ) - ], - borders: this.noBorders, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.averageStats, - this.generalStyle.metricFontSize, - this.themeStyle.titleFontColor, - 0, - true, - true, - this.generalStyle.heavyFont - ) - ], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.averageStatsTitle, - this.generalStyle.metricFontTitleSize, - this.themeStyle.titleFontColor, - 0, - false, - true - ) - ], - borders: this.noBorders, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.totalStats, - this.generalStyle.metricFontSize, - this.themeStyle.titleFontColor, - 0, - true, - true, - this.generalStyle.heavyFont - ) - ], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.totalStatsTitle, - this.generalStyle.metricFontTitleSize, - this.themeStyle.titleFontColor, - 0, - false, - true - ) - ], - borders: this.noBorders, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.totalCompanies, - this.generalStyle.metricFontSize, - this.themeStyle.titleFontColor, - 0, - true, - true, - this.generalStyle.heavyFont - ) - ], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - statistics.totalCompaniesTitle, - this.generalStyle.metricFontTitleSize, - this.themeStyle.titleFontColor, - 0, - false, - true - ) - ], - borders: this.noBorders, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - }), - ] - }), - ] + // Create a table with the two images in a single row + // 50% | 50% + // bubble chart | radar chart + _createChartsTable (bubbleImage, pieImage) { return new docx.Table({ - columnWidths: [95], - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - } - - // Create the first row with two images and a nested table - // 40% | 40% | 20% - // bubble chart | radar chart | nested table for stats - firstRow (bubbleImage, radarImage, stats) { - return new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [this.insertImage(bubbleImage, 226, 259.2)], - borders: this.bottomAndRightBorders - }), - new docx.TableCell({ - children: [this.insertImage(radarImage, 226, 345.6)], - borders: this.bottomAndRightBorders - }), - new docx.TableCell({ - children: [this._statisticsTable(stats)], - borders: this.noBorders, - rowSpan: 5, - margins: { - left: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin, - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - }, - verticalAlign: docx.VerticalAlign.CENTER, - }), - ] - }) - } - - // A custom table that contains a company logo/name and description - _companyDescRow(company, descriptionLen=550) { - // Trim the length of the description to the required size - let companyDesc = company.company.description - companyDesc = companyDesc.replace(/\.\.\.|\.$/, '') - if (companyDesc.length > descriptionLen) { - companyDesc = companyDesc.substring(0,descriptionLen) - } - companyDesc = companyDesc + '...' - const myRows = [ + columnWidths: [50, 50], + rows: [ new docx.TableRow({ children: [ new docx.TableCell({ - children: [ - this.makeParagraph( - company.company.name, // Text for the paragraph - this.generalStyle.companyNameFontSize, // Font size - this.themeStyle.titleFontColor, // Specify the font color - 0, // Set the space after attribute to 0 - false, // Set bold to false - true, // Set alignment to center - this.generalStyle.heavyFont // Define the font used - ) - ], - borders: this.noBorders, - columnSpan: 2, - margins: { - top: this.generalStyle.tableMargin - } + children: [this.insertImage(bubbleImage, 240, 345)], + borders: this.bottomAndRightBorders }), - ], - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - companyDesc, - this.generalStyle.dashFontSize, - this.themeStyle.fontColor, - false - ) - ], - borders: this.noBorders, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - }), - ] - }) - ] - return new docx.Table({ - columnWidths: [100], - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - } - - _docDescRow ( - docs, - fileNameLen=25, - docLen=460, - headerText="Most/Least Similar Interactions", - mostSimilarRowName="Most similar", - leastSimilarRowName="Least similar" - ) { - // TODO add the header row with colspan=3, centered, and with margins all around to myrows - let myRows = [ - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - headerText, - this.generalStyle.dashFontSize, - this.themeStyle.titleFontColor, - 0, // Set the space after attribute to 0 - true, // Set bold to true - true, // Set alignment to center - this.generalStyle.heavyFont // Define the font used - ) - ], - borders: this.noBorders, - columnSpan: 3, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - - }), - ], - })] - for(const doc in docs) { - let docCategoryName = "" - doc === "most_similar" ? docCategoryName = mostSimilarRowName : docCategoryName = leastSimilarRowName - // Trim the length of the document name - let docName = docs[doc].name - if (docName.length > fileNameLen) { - docName = docName.substring(0,fileNameLen) + '...' - } - // Trim the length of the document description - let docDesc = docs[doc].description - docDesc = docDesc.replace(/\.\.\.|\.$/, '') - if (docDesc.length > docLen) { - docDesc = docDesc.substring(0,docLen) - } - docDesc = docDesc + '...' - myRows.push( - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.makeParagraph( - docCategoryName, - this.generalStyle.dashFontSize, - this.themeStyle.fontColor, - ) - ], - borders: this.noBorders, - margins: { - top: this.generalStyle.tableMargin, - left: this.generalStyle.tableMargin, - }, - width: { - size: 15, - type: docx.WidthType.PERCENTAGE - } - }), - new docx.TableCell({ - children: [ - // TODO this needs to be a URL which points to the relevant document - this.makeParagraph( - docName, - this.generalStyle.dashFontSize, - this.themeStyle.fontColor, - ) - ], - borders: this.noBorders, - margins: { - top: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin, - }, - width: { - size: 15, - type: docx.WidthType.PERCENTAGE - } - }), - new docx.TableCell({ - children: [ - this.makeParagraph( - docDesc, - this.generalStyle.dashFontSize, - this.themeStyle.fontColor, - ) - ], - borders: this.noBorders, - margins: { - // left: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin, - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - }, - width: { - size: 70, - type: docx.WidthType.PERCENTAGE - } - }), - ], + new docx.TableCell({ + children: [this.insertImage(pieImage, 240, 345)], + borders: this.bottomAndRightBorders + }), + ] }) - ) - } - return new docx.Table({ - columnWidths: [20, 30, 50], - rows: myRows, + ], width: { size: 100, type: docx.WidthType.PERCENTAGE - }, - }) - - } - - - // A shell row to contain company description, document descriptions, etc. - shellRow (type, company, docs) { - let myTable = {} - if (type === "companyDesc") { - myTable = this._companyDescRow(company) - } else if (type === "docDesc") { - myTable = this._docDescRow(docs) - } else { - myTable = this._blankRow() - } - return new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [myTable], - borders: this.topAndRightBorders, - columnSpan: 2, - }) - ] - }) - } - - /** - * - * @param {*} imageFile - * @param {*} height - * @param {*} width - * @returns - * @todo move to common - */ - insertImage (imageFile, height, width) { - const myFile = fs.readFileSync(imageFile) - return new docx.Paragraph({ - alignment: docx.AlignmentType.CENTER, - children: [ - new docx.ImageRun({ - data: myFile, - transformation: { - height: height, - width: width - } - }) - ] - }) - } - - // Find the closest competitor - // This uses the Euclidean distance, given points (x1, y1) and (x2, y2) - // d = sqrt((x2 - x1)^2 + (y2 - y1)^2) - _getMostSimilarCompany(comparisons, companies) { - const x1 = 1 // In this case x1 = 1 and y1 = 1 - const y1 = 1 - let distances = {} - for(const companyId in comparisons) { - const myDistance = Math.sqrt( - (comparisons[companyId].most_similar.score - x1) ** 2 + - (comparisons[companyId].least_similar.score - y1) ** 2 - ) - distances[myDistance] = companyId - } - const mostSimilarId = distances[Math.min(...Object.keys(distances))] - const mostSimilarCompany = companies.filter(company => { - if (parseInt(company.company.id) === parseInt(mostSimilarId)) { - return company } }) - return mostSimilarCompany[0] } - /** - * - * @param {*} paragraph - * @param {*} size - * @param {*} color - * @param {*} spaceAfter - * @param {*} bold - * @param {*} center - * @param {*} font - * @returns - * @todo Replace the report/common.js makeParagraph method with this one during refactoring - * @todo Add an options object in a future release when refactoring - * @todo Review the NOTICE section and at a later date work on all TODOs there - */ - makeParagraph ( - paragraph, - size=20, - color="000", - spaceAfter=0, - bold=false, - center=false, - font="Avenir Next", - italics=false, - underline=false - ) { - size = 2 * size // Font size is measured in half points, multiply by to is needed - return new docx.Paragraph({ - alignment: center ? docx.AlignmentType.CENTER : docx.AlignmentType.LEFT, - children: [ - new docx.TextRun({ - text: paragraph, - font: font ? font : "Avenir Next", // Default font: Avenir next - size: size ? size : 20, // Default font size size 10pt or 2 * 10 = 20 - bold: bold ? bold : false, // Bold is off by default - italics: italics ? italics : false, // Italics off by default - underline: underline ? underline : false, // Underline off by default - break: spaceAfter ? spaceAfter : 0, // Defaults to no trailing space after the paragraph - color: color ? color : "000", // Default color is black - }) - ], - - }) - } /** * @async @@ -948,17 +466,11 @@ class CompanyDashbord extends Dashboards { * @param {Object} competitors - the competitors to the company * @param {String} baseDir - the complete directory needed to store images for the dashboard * @returns + * + * */ - async makeDashboard(company, competitors, baseDir) { - // Construct the Charting class - const charting = new Charting(this.env) - // Create the bubble chart from the company comparisons - const bubbleChartFile = await charting.bubbleChart({similarities: company.similarity, company: company}) - - // Create the pie chart for interaction characterization - const pieChartFile = await charting.pieChart({company: company}) + async makeDashboard(company, competitors, interactions, noInteractions, totalInteractions, totalCompanies, averageInteractions) { - process.exit(0) /** * NOTICE * I believe that there is a potential bug in node.js filesystem module. @@ -982,31 +494,60 @@ class CompanyDashbord extends Dashboards { * 3. Clearly document the steps and problem encountered * 4. In the separate standalone program try using with and without axios use default http without */ - const scratchChartFile = await radarChart( - {company: company, competitors: competitors, stats: myStats}, - this.env, - baseDir, - 'scratch_chart.png' - ) - let myRows = [ - this.firstRow(bubbleChartFile, pieChartFile, myStats), - this.shellRow("companyDesc", mostSimilarCompany), - this.shellRow("docDesc", null, mostLeastSimilarInteractions), - ] - const myTable = new docx.Table({ - columnWidths: [40, 40, 20], - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - }, - height: { - size: 100, - type: docx.WidthType.PERCENTAGE + // const scratchChartFile = await radarChart( + // {company: company, competitors: competitors, stats: myStats}, + // this.env, + // baseDir, + // 'scratch_chart.png' + // ) + + // Create bubble and pie charts and the associated wrapping table + const bubbleChartFile = await this.charting.bubbleChart({similarities: company.similarity, company: company}) + const pieChartFile = await this.charting.pieChart({company: company}) + const chartsTable = this._createChartsTable(bubbleChartFile, pieChartFile) + + // Create the most similar company description table + const mostSimilarCompanyDescTable = this.tableWidgets.oneColumnTwoRowsTable([ + `Most similar company to ${company.name}: ${competitors.mostSimilar.name}`, + this.shortenText(competitors.mostSimilar.description, 3) + ]) + + // From the most similar company find the most similar interaction and the least similar interaction + const mostSimilarInterationName = company.similarity[competitors.mostSimilar.name].most_similar.name + // Find the most similar interaction using mostSimilarInterationName from the interactions object + const mostSimilarInteraction = interactions.filter(interaction => { + if (interaction.name === mostSimilarInterationName) { + return interaction } - }) + })[0] + const mostSimilarInteractionDescTable = this.tableWidgets.oneColumnTwoRowsTable([ + this.truncateText(`Most similar interaction from ${competitors.mostSimilar.name}: ${mostSimilarInteraction.name}`), + this.shortenText(mostSimilarInteraction.description, 1) + ]) + + const leastSimilarInterationName = company.similarity[competitors.mostSimilar.name].least_similar.name + // Find the least similar interaction using leastSimilarInterationName from the interactions object + const leastSimilarInteraction = interactions.filter(interaction => { + if (interaction.name === leastSimilarInterationName) { + return interaction + } + })[0] + const leastSimilarInteractionDescTable = this.tableWidgets.oneColumnTwoRowsTable([ + this.truncateText(`Least similar interaction from ${competitors.mostSimilar.name}: ${leastSimilarInteraction.name}`), + this.shortenText(leastSimilarInteraction.description, 1) + ]) + + // Create the left and right contents + const leftContents = this.tableWidgets.packContents([chartsTable, mostSimilarCompanyDescTable, mostSimilarInteractionDescTable, leastSimilarInteractionDescTable]) + const rightContents = this.tableWidgets.descriptiveStatisticsTable([ + {title: 'Number of Interactions', value: noInteractions}, + {title: 'Average Interactions per Company', value: averageInteractions}, + {title: 'Total Interactions', value: totalInteractions}, + {title: 'Total Companies', value: totalCompanies}, + ]) + - return myTable + return this.tableWidgets.createDashboardShell(leftContents, rightContents, {leftWidth: 85, rightWidth: 15}) } } diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index 1527d69..b25db47 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -8,12 +8,12 @@ class TableWidgets extends Widgets { super(env) // Define specifics for table borders this.noneStyle = { - style: this.generalStyle.noBorderStyle + style: this.generalSettings.noBorderStyle } this.borderStyle = { - style: this.generalStyle.tableBorderStyle, - size: this.generalStyle.tableBorderSize, - color: this.themeStyle.tableBorderColor + style: this.generalSettings.tableBorderStyle, + size: this.generalSettings.tableBorderSize, + color: this.themeSettings.tableBorderColor } // No borders this.noBorders = { @@ -57,6 +57,7 @@ class TableWidgets extends Widgets { top: this.borderStyle, bottom: this.borderStyle } + this.textWidgets = new TextWidgets(env) } /** @@ -109,7 +110,7 @@ class TableWidgets extends Widgets { size: 20, type: docx.WidthType.PERCENTAGE }, - children: [TextWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], borders: leftBorderStyle }), new docx.TableCell({ @@ -117,7 +118,7 @@ class TableWidgets extends Widgets { size: 80, type: docx.WidthType.PERCENTAGE }, - children: [TextWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + children: [this.textWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], borders: rightBorderStyle }) ] @@ -184,7 +185,7 @@ class TableWidgets extends Widgets { size: 20, type: docx.WidthType.PERCENTAGE }, - children: [TextWidgets.makeParagraph(col1, {fontSize: this.generalSettings.fullFontSize, bold: firstColumnBold})], + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.generalSettings.fullFontSize, bold: firstColumnBold})], borders: leftBorderStyle }), new docx.TableCell({ @@ -237,7 +238,7 @@ class TableWidgets extends Widgets { size: 40, type: docx.WidthType.PERCENTAGE, }, - children: [TextWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], borders: borderStyle }), new docx.TableCell({ @@ -245,7 +246,7 @@ class TableWidgets extends Widgets { size: 30, type: docx.WidthType.PERCENTAGE, }, - children: [TextWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + children: [this.textWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], borders: borderStyle }), new docx.TableCell({ @@ -253,13 +254,80 @@ class TableWidgets extends Widgets { size: 30, type: docx.WidthType.PERCENTAGE, }, - children: [TextWidgets.makeParagraph(col3, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + children: [this.textWidgets.makeParagraph(col3, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], borders: borderStyle }), ] }) } + oneColumnTwoRowsBasic (rows, options={}) { + // Desctructure the options object + let { + firstRowBold = true, + allRowsBold = false, + allBorders = false, + bottomBorders = true, + centerFirstRow = false + } = options + + // Desctructure the rows array + const [row1, row2] = rows + + // Set the first row to bold if all rows are bold + if (allRowsBold) { + firstRowBold = true + } + + // Set the border style + let borderStyle = this.noBorders + if (allBorders) { + borderStyle = this.allBorders + } else if (bottomBorders) { + borderStyle = this.bottomBorders + } else { + borderStyle = this.bottomAndRightBorders + } + + // return the row + return [ + new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 100, + type: docx.WidthType.PERCENTAGE, + }, + children: [this.textWidgets.makeParagraph(row1, {fontSize: this.fontFactor * this.fontSize, bold: firstRowBold, align: centerFirstRow ? docx.AlignmentType.CENTER : docx.AlignmentType.START})], + margins: { + bottom: this.generalSettings.tableMargin + }, + borders: this.rightBorder + + }), + ], + borders: borderStyle + }), + new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 100, + type: docx.WidthType.PERCENTAGE, + }, + children: [this.textWidgets.makeParagraph(row2, {fontSize: this.fontFactor * this.fontSize, bold: allRowsBold})], + borders: this.bottomAndRightBorders, + margins: { + bottom: this.generalSettings.tableMargin + } + }), + ], + }) + ] + } + + + /** * Begin functions for completely defining tables with a particular structure */ @@ -278,7 +346,7 @@ class TableWidgets extends Widgets { right: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, }, shading: {fill: this.themeSettings.tagColor}, - children: [this.makeParagraph(tag, {fontSize: this.fontSize, fontColor: this.themeSettings.tagFontColor, center: true})], + children: [this.textWidgets.makeParagraph(tag, {fontSize: this.fontSize, fontColor: this.themeSettings.tagFontColor, center: true})], verticalAlign: docx.AlignmentType.CENTER, }) } @@ -371,10 +439,10 @@ class TableWidgets extends Widgets { return new docx.Table({ columnWidths: [95], margins: { - left: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin, - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin + left: this.generalSettings.tableMargin, + right: this.generalSettings.tableMargin, + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin }, rows: [this.twoColumnRowBasic([title, text], {firstColumnBold: true, allColumnsBold: false, lastCellBottomRightBorders: true})], width: { @@ -401,12 +469,12 @@ class TableWidgets extends Widgets { children: [ new docx.TableCell({ children: [ - this.util.makeParagraph( + this.textWidgets.makeParagraph( statistics[stat].value, { - fontSize: this.generalStyle.metricFontSize, - fontColor: this.themeStyle.titleFontColor, - font: this.generalStyle.heavyFont, + fontSize: this.generalSettings.metricFontSize, + fontColor: this.themeSettings.titleFontColor, + font: this.generalSettings.heavyFont, bold: true, center: true } @@ -414,7 +482,7 @@ class TableWidgets extends Widgets { ], borders: this.bottomBorder, margins: { - top: this.generalStyle.tableMargin + top: this.generalSettings.tableMargin } }), ] @@ -423,11 +491,11 @@ class TableWidgets extends Widgets { children: [ new docx.TableCell({ children: [ - this.util.makeParagraph( + this.textWidgets.makeParagraph( statistics[stat].title, { - fontSize: this.generalStyle.metricFontSize/2, - fontColor: this.themeStyle.titleFontColor, + fontSize: this.generalSettings.metricFontSize/2, + fontColor: this.themeSettings.titleFontColor, bold: false, center: true } @@ -435,8 +503,8 @@ class TableWidgets extends Widgets { ], borders: this.noBorders, margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin } }), ] @@ -450,6 +518,97 @@ class TableWidgets extends Widgets { width: { size: 100, type: docx.WidthType.PERCENTAGE + }, + + }) + } + + oneColumnTwoRowsTable(rows) { + const tableRows = this.oneColumnTwoRowsBasic(rows) + return new docx.Table({ + columnWidths: [95], + rows: tableRows, + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) + } + + packContents (contents) { + let myRows = [] + for (const content in contents) { + myRows.push( + new docx.TableRow({ + children: [ + new docx.TableCell({ + children: [contents[content]], + borders: this.noBorders, + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + }, + }), + ] + }) + ) + } + return new docx.Table({ + columnWidths: [95], + borders: this.noBorders, + margins: { + left: this.generalSettings.tableMargin, + right: this.generalSettings.tableMargin, + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + }, + rows: myRows, + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) + } + + // Create the dashboard shell which will contain all of the outputs + createDashboardShell (leftContents, rightContents, options={}) { + const { + allBorders = false, + leftWidth = 70, + rightWidth = 30 + } = options + return new docx.Table({ + columnWidths: [leftWidth, rightWidth], + rows: [ + new docx.TableRow({ + children: [ + new docx.TableCell({ + children: [leftContents], + borders: allBorders ? this.allBorders : this.noBorders, + width: { + size: leftWidth, + type: docx.WidthType.PERCENTAGE + } + }), + new docx.TableCell({ + children: [rightContents], + borders: allBorders ? this.allBorders : this.noBorders, + width: { + size: rightWidth, + type: docx.WidthType.PERCENTAGE + }, + verticalAlign: docx.VerticalAlign.CENTER + }), + ] + }) + ], + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + }, + height: { + size: 100, + type: docx.WidthType.PERCENTAGE } }) } diff --git a/src/report/widgets/Widgets.js b/src/report/widgets/Widgets.js index 8d626f5..1dc952b 100644 --- a/src/report/widgets/Widgets.js +++ b/src/report/widgets/Widgets.js @@ -6,7 +6,7 @@ * @license Apache-2.0 */ -import docxSettings from './settings.js' +import docxSettings from '../settings.js' import docx from 'docx' class Widgets { diff --git a/src/report/widgets/settings.js b/src/report/widgets/settings.js deleted file mode 100644 index 59dadc4..0000000 --- a/src/report/widgets/settings.js +++ /dev/null @@ -1,78 +0,0 @@ -import docx from 'docx' - -const docxSettings = { - general: { - halfFontSize: 11, - fullFontSize: 22, - headerFontSize: 18, - footerFontSize: 18, - fontFactor: 1, - dashFontSize: 22, - tableFontSize: 8, - titleFontSize: 18, - companyNameFontSize: 11.5, - metricFontTitleSize: 11, - metricFontSize: 48, - chartTitleFontSize: 18, - chartFontSize: 10, - chartAxesFontSize: 12, - chartTickFontSize: 10, - chartSymbolSize: 30, - tableBorderSize: 8, - tableBorderStyle: docx.BorderStyle.SINGLE, - noBorderStyle: docx.BorderStyle.NIL, - tableMargin: docx.convertInchesToTwip(0.1), - tagMargin: docx.convertInchesToTwip(0.08), - font: "Avenir Next", - heavyFont: "Avenir Next Heavy", - lightFont: "Avenir Next Light" - - }, - coffee: { - tableBorderColor: "4A7E92", // Light Blue - tagColor: "4A7E92", // Light Blue - tagFontColor: "0F0D0E", // Coffee black - documentColor: "0F0D0E", // Coffee black - titleFontColor: "41A6CE", // Saturated Light Blue - textFontColor: "41A6CE", // Ultra light Blue - chartAxisLineColor: "#374246", - chartAxisFontColor: "rgba(71,121,140, 0.7)", - chartAxisTickFontColor: "rgba(149,181,192, 0.6)", - chartItemFontColor: "rgba(149,181,192, 0.9)", - chartSeriesColor: "rgb(71,113,128)", - chartSeriesBorderColor: "rgba(149,181,192, 0.9)", - highlightFontColor: "" - }, - espresso: { - tableBorderColor: "C7701E", // Orange - tagColor: "C7701E", // Light Blue - tagFontColor: "0F0D0E", // Coffee black - documentColor: "0F0D0E", // Coffee black - titleFontColor: "C7701E", // Saturated Light Blue - textFontColor: "C7701E", // Ultra light Blue - chartAxisLineColor: "#374246", - chartAxisFontColor: "rgba(71,121,140, 0.7)", - chartAxisTickFontColor: "rgba(149,181,192, 0.6)", - chartItemFontColor: "rgba(149,181,192, 0.9)", - chartSeriesColor: "rgb(71,113,128)", - chartSeriesBorderColor: "rgba(149,181,192, 0.9)", - highlightFontColor: "" - }, - latte: { - tableBorderColor: "25110f", // Orange - tagColor: "25110f", // Light Blue - tagFontColor: "F1F0EE", // Coffee black - documentColor: "F1F0EE", // Coffee black - titleFontColor: "25110f", // Saturated Light Blue - textFontColor: "25110f", // Ultra light Blue - chartAxisLineColor: "#374246", - chartAxisFontColor: "rgba(71,121,140, 0.7)", - chartAxisTickFontColor: "rgba(149,181,192, 0.6)", - chartItemFontColor: "rgba(149,181,192, 0.9)", - chartSeriesColor: "rgb(71,113,128)", - chartSeriesBorderColor: "rgba(149,181,192, 0.9)", - highlightFontColor: "" - } -} - -export default docxSettings \ No newline at end of file From 04ed8e09364751d486fa17608aaf4f54f67eb74a Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sat, 10 Aug 2024 21:53:41 -0700 Subject: [PATCH 05/23] Firmographics, tags, similarity --- src/report/companies.js | 191 +++++++++++++++------------------- src/report/dashboard.js | 72 ++++--------- src/report/settings.js | 6 +- src/report/widgets/Tables.js | 66 +++++++++--- src/report/widgets/Widgets.js | 6 +- 5 files changed, 162 insertions(+), 179 deletions(-) diff --git a/src/report/companies.js b/src/report/companies.js index 4bd47b3..182dd2f 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -16,6 +16,7 @@ import { CompanyDashbord } from './dashboard.js' import { Utilities as CLIUtilities } from '../cli/common.js' import { getMostSimilarCompany } from './tools.js' import TextWidgets from './widgets/Text.js' +import TableWidgets from './widgets/Tables.js' class BaseCompanyReport { constructor(company, env) { @@ -29,6 +30,7 @@ class BaseCompanyReport { this.workDir = this.env.workDir this.baseName = company.name.replace(/ /g,"_") this.textWidgets = new TextWidgets(env) + this.tableWidgets = new TableWidgets(env) } } @@ -47,11 +49,10 @@ class CompanySection extends BaseCompanyReport { */ constructor(company, env) { super(company, env) - console.log('CompanySection constructor') } // Create a URL on Google maps to search for the address - addressRow() { + _addressRow() { // Define the address string const addressBits = [ this.company.street_address, @@ -79,38 +80,14 @@ class CompanySection extends BaseCompanyReport { addressBits[1] + ', ' + addressBits[2] + ' ' + addressBits[3] + ', ' + addressBits[4] // Return the created elements - return this.util.urlRow('Location', addressString, addressUrl) + return this.tableWidgets.twoColumnRowWithHyperlink(['Location', addressString, addressUrl]) } - // Create a URL to search for patents on Google patents - patentRow() { - const patentString = `${this.company.name} Patent Search` - return this.util.urlRow('Patents', patentString, this.company.google_patents_url) - } - - // Create a URL to search for news on Google news - newsRow() { - const newsString = `${this.company.name} Company News` - return this.util.urlRow('News', newsString, this.company.google_news_url) - } - - // Define the CIK and link it to an EDGAR search if available - cikRow() { - if (this.company.cik === 'Unknown') { - return this.util.basicRow('CIK', this.company.cik) - } else { - const baseURL = 'https://www.sec.gov/edgar/search/#/ciks=' - return this.util.urlRow('CIK', this.company.cik, baseURL + this.company.cik) - } - } - - // Define the CIK and link it to an EDGAR search if available - stockSymbolRow() { - if (this.company.stock_symbol === 'Unknown') { - return this.util.basicRow('Stock Symbol', this.company.stock_symbol) + _genericUrlRow(label, urlText, url) { + if (urlText === 'Unknown') { + return this.tableWidgets.twoColumnRowBasic([label, urlText]) } else { - const baseURL = 'https://www.bing.com/search?q=' - return this.util.urlRow('Stock Symbol', this.company.stock_symbol, baseURL + this.company.stock_symbol) + return this.tableWidgets.twoColumnRowWithHyperlink([label, urlText, url]) } } @@ -120,38 +97,39 @@ class CompanySection extends BaseCompanyReport { * @returns {Object} A docx table is return to the caller */ makeFirmographicsDOCX() { - const noInteractions = String(Object.keys(this.company.linked_interactions).length) - const noStudies = String(Object.keys(this.company.linked_studies).length) - const myTable = new docx.Table({ + return new docx.Table({ columnWidths: [20, 80], rows: [ - this.util.basicRow('Name', this.company.name), - this.util.basicRow('Description', this.company.description), - this.util.urlRow('Website', this.company.url, this.company.url), - this.util.basicRow('Role', this.company.role), - this.util.basicRow('Industry', this.company.industry), - this.patentRow(), - this.newsRow(), - this.addressRow(), - this.util.basicRow('Region', this.company.region), - this.util.basicRow('Phone', this.company.phone), - this.util.basicRow('Type', this.companyType), - this.stockSymbolRow(), - this.cikRow(), - this.util.basicRow('No. Interactions', noInteractions), - this.util.basicRow('No. Studies', noStudies), + this.tableWidgets.twoColumnRowBasic(['Name', this.company.name]), + this.tableWidgets.twoColumnRowBasic(['Description', this.company.description]), + this._genericUrlRow('Website', this.company.url, this.company.url), + this.tableWidgets.twoColumnRowBasic(['Role', this.company.role]), + this._genericUrlRow('Patents', 'Patent Search', this.company.google_patents_url), + this._genericUrlRow('News', 'Company News', this.company.google_news_url), + this._addressRow(), + this.tableWidgets.twoColumnRowBasic(['Region', this.company.region]), + this.tableWidgets.twoColumnRowBasic(['Phone', this.company.phone]), + this.tableWidgets.twoColumnRowBasic(['Type', this.companyType]), + this._genericUrlRow( + 'Stock Symbol', + this.company.stock_symbol, + `https://www.bing.com/search?q=${this.company.stock_symbol}` + ), + this._genericUrlRow( + 'EDGAR CIK', + this.company.cik, + `https://www.sec.gov/edgar/search/#/ciks=${this.company.cik}` + ) ], width: { size: 100, type: docx.WidthType.PERCENTAGE } }) - - return myTable } // Rank supplied topics and return an object that can be rendered - rankComparisons (comparisons, competitors) { + _rankComparisons (comparisons, competitors) { // Set up a blank object to help determine the top score let rankPicker = {} @@ -204,7 +182,7 @@ class CompanySection extends BaseCompanyReport { */ makeComparisonDOCX(comparisons, competitors) { // Transform the comparisons into something that is usable for display - const [myComparison, picks] = this.rankComparisons(comparisons, competitors) + const [myComparison, picks] = this._rankComparisons(comparisons, competitors) // Choose the company object with the top score const topChoice = picks[Object.keys(picks).sort()[0]] @@ -212,13 +190,16 @@ class CompanySection extends BaseCompanyReport { const topCompanyName = topCompany.name const topCompanyRole = topCompany.role - let myRows = [this.util.basicComparisonRow('Company', 'Role', 'Similarity Distance', true)] + let myRows = [this.tableWidgets.threeColumnRowBasic(['Company', 'Role', 'Similarity Distance'], {allColumnsBold: true})] for (const comparison in myComparison) { myRows.push( - this.util.basicComparisonRow( - myComparison[comparison].name, - myComparison[comparison].role, - `${String.fromCharCode(0x2588).repeat(myComparison[comparison].score)} (${myComparison[comparison].rank})`, + this.tableWidgets.threeColumnRowBasic( + [ + myComparison[comparison].name, + myComparison[comparison].role, + `${String.fromCharCode(0x2588).repeat(myComparison[comparison].score)} (${myComparison[comparison].rank})`, + ], + {firstColumnBold: false} ) ) } @@ -233,13 +214,12 @@ class CompanySection extends BaseCompanyReport { }) return [ - this.util.makeParagraph( - `According to findings from mediumroast.io, the closest company to ${this.company.name} in terms of ` + + this.textWidgets.makeParagraph( + `According to findings from Mediumroast for GitHub, the closest company to ${this.company.name} in terms of ` + `content similarity is ${topCompanyName} who appears to be a ${topCompanyRole} of ${this.company.name}. ` + `Additional information on ` + `${this.company.name}'s comparison with other companies is available in the accompanying table.` ), - this.util.makeHeading2('Comparison Table'), myTable ] } @@ -347,10 +327,7 @@ class CompanyStandalone extends BaseCompanyReport { this.interactions = sourceData.allInteractions this.competitors = sourceData.competitors.all this.description = `A Company report for ${this.company.name} and including relevant company data.` - this.introduction = `The mediumroast.io system automatically generated this document. - It includes key metadata for this Company object and relevant summaries and metadata from the associated interactions. If this report document is produced as a package, instead of standalone, then the - hyperlinks are active and will link to documents on the local folder after the - package is opened.` + this.introduction = `Mediumroast for GitHub automatically generated this document. It includes company firmographics, key information on competitors, and Interactions data for ${this.company.name}. If this report is produced as a package, then the hyperlinks are active and will link to documents on the local folder after the package is opened.` this.similarity = this.company.similarity, this.noInteractions = String(Object.keys(this.company.linked_interactions).length) this.totalInteractions = sourceData.totalInteractions @@ -388,38 +365,40 @@ class CompanyStandalone extends BaseCompanyReport { const preparedFor = `${this.authoredBy} report for: ` // Construct the company section - // const companySection = new CompanySection(this.company, this.env) - // const interactionSection = new InteractionSection( - // this.interactions, - // this.company.name, - // this.objectType, - // this.env - // ) + const companySection = new CompanySection(this.company, this.env) + const interactionSection = new InteractionSection( + this.interactions, + this.company.name, + this.objectType, + this.env + ) const myDash = new CompanyDashbord(this.env) // Set up the default options for the document - // const myDocument = [].concat( - // this.util.makeIntro(this.introduction), - // [ - // this.util.makeHeading1('Company Detail'), - // companySection.makeFirmographicsDOCX(), - // this.util.makeHeading1('Comparison') - // ], - // companySection.makeComparisonDOCX(this.similarity, this.competitors), - // [ this.util.makeHeading1('Topics'), - // this.util.makeParagraph( - // 'The following topics were automatically generated from all ' + - // this.noInteractions + ' interactions associated to this company.' - // ), - // this.util.makeHeadingBookmark1('Interaction Summaries', 'interaction_summaries') - // ], - // ...interactionSection.makeDescriptionsDOCX(), - // await companySection.makeCompetitorsDOCX(this.competitors, isPackage), - // [ this.util.pageBreak(), - // this.util.makeHeading1('References') - // ], - // ...interactionSection.makeReferencesDOCX(isPackage) - // ) + const myDocument = [].concat( + this.util.makeIntro(this.introduction), + [ + this.textWidgets.makeHeading1('Firmographics'), + companySection.makeFirmographicsDOCX(), + this.textWidgets.makeHeading1('Tags'), + this.tableWidgets.tagsTable(this.company.tags), + this.textWidgets.makeHeading1('Competitive Similarity'), + ], + companySection.makeComparisonDOCX(this.similarity, this.competitors), + // [ this.util.makeHeading1('Topics'), + // this.util.makeParagraph( + // 'The following topics were automatically generated from all ' + + // this.noInteractions + ' interactions associated to this company.' + // ), + // this.util.makeHeadingBookmark1('Interaction Summaries', 'interaction_summaries') + // ], + // ...interactionSection.makeDescriptionsDOCX(), + // await companySection.makeCompetitorsDOCX(this.competitors, isPackage), + // [ this.util.pageBreak(), + // this.util.makeHeading1('References') + // ], + // ...interactionSection.makeReferencesDOCX(isPackage) + ) // Construct the document const myDoc = new docx.Document ({ @@ -461,18 +440,18 @@ class CompanyStandalone extends BaseCompanyReport { ) ], }, - // { - // properties: {}, - // headers: { - // default: this.util.makeHeader(this.company.name, preparedFor) - // }, - // footers: { - // default: new docx.Footer({ - // children: [this.util.makeFooter(authoredBy, preparedOn)] - // }) - // }, - // children: [this.textWidgets.makeParagraph('Table of Contents')], - // } + { + properties: {}, + headers: { + default: this.util.makeHeader(this.company.name, preparedFor) + }, + footers: { + default: new docx.Footer({ + children: [this.util.makeFooter(authoredBy, preparedOn)] + }) + }, + children: myDocument, + } ], }) diff --git a/src/report/dashboard.js b/src/report/dashboard.js index c718f1a..706e8b8 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -402,28 +402,13 @@ class InteractionDashboard extends Dashboards { const leftContents = this._mergeLeftContents([interactionNameTable, interactionDescriptionTable, associatedCompanyTable, protorequirementsTable]) - - // Create and return the dashboard shell with left and right contents - /* - ---------------------------------------------- - | 80% | 20% | - | | | - | | | - | | | - | | | - | | | - | | | - | | | - ---------------------------------------------- - */ - return this._createDashboardShell(leftContents, rightContents) } } class CompanyDashbord extends Dashboards { /** - * A high class meant to create an initial dashboard page for an MS Word document company report + * A class meant to create an initial dashboard page for an MS Word document company report * @constructor * @classdesc To operate this class the constructor should be passed a the environmental setting for the object. * @param {Object} env - Environmental variable settings for the CLI environment @@ -462,44 +447,29 @@ class CompanyDashbord extends Dashboards { /** * @async - * @param {Object} company - the company the dashboard is for - * @param {Object} competitors - the competitors to the company - * @param {String} baseDir - the complete directory needed to store images for the dashboard - * @returns + * @function makeDashboard - Create a dashboard for a company report + * @param {Object} company - A company object + * @param {Object} competitors - A competitors object + * @param {Object} interactions - An interactions object + * @param {Number} noInteractions - The number of interactions + * @param {Number} totalInteractions - The total number of interactions + * @param {Number} totalCompanies - The total number of companies + * @param {Number} averageInteractions - The average number of interactions per company + * @returns {Object} - A docx.Table object that contains the dashboard * + * @example + * const companyDashboard = new CompanyDashboard(env) + * const dashboard = await companyDashboard.makeDashboard(company, competitors, interactions, noInteractions, totalInteractions, totalCompanies, averageInteractions) * */ - async makeDashboard(company, competitors, interactions, noInteractions, totalInteractions, totalCompanies, averageInteractions) { - - /** - * NOTICE - * I believe that there is a potential bug in node.js filesystem module. - * This file is needed because otherwise the actual final of the two images - * that needs to be inserted into the docx file won't load. If we create a - * scratch file then it will. Essentially something is off with the last - * file created in a series of files, but the second to last file appears ok. - * - * Obviously more testing is needed before we approach the node team with something - * half baked. Until then here are some observations: - * 1. Unless the file of a given name is present, even if zero bytes, the image data - * will not be put into the file. If the file name exists then everything works. - * 2. Again the last file in a series of files appears to be corrupted and cannot, for - * some odd reason, be read by the docx module and be inserted into a docx file. - * Yet when we look at the file system object within the file system it can be opened - * without any problems. - * - * TODOs - * 1. Create a separate program that emulates what is done in the mrcli dashboard - * 2. Try on multiple OSes - * 3. Clearly document the steps and problem encountered - * 4. In the separate standalone program try using with and without axios use default http without - */ - // const scratchChartFile = await radarChart( - // {company: company, competitors: competitors, stats: myStats}, - // this.env, - // baseDir, - // 'scratch_chart.png' - // ) + async makeDashboard( + company, + competitors, + interactions, + noInteractions, + totalInteractions, + totalCompanies, + averageInteractions) { // Create bubble and pie charts and the associated wrapping table const bubbleChartFile = await this.charting.bubbleChart({similarities: company.similarity, company: company}) diff --git a/src/report/settings.js b/src/report/settings.js index 12e3064..3efe274 100644 --- a/src/report/settings.js +++ b/src/report/settings.js @@ -3,13 +3,13 @@ import docx from 'docx' const docxSettings = { general: { halfFontSize: 11, - fullFontSize: 22, + fullFontSize: 11 * 2, // Note these are in half points, so 22 * 2 = 44 headerFontSize: 18, footerFontSize: 18, fontFactor: 1, dashFontSize: 22, - tableFontSize: 8, - titleFontSize: 18, + tableFontSize: 9 * 2, // Note these are in half points, so 9 * 2 = 18 + titleFontSize: 30 * 2, companyNameFontSize: 11.5, metricFontTitleSize: 11, metricFontSize: 48, diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index b25db47..845eb2c 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -2,6 +2,7 @@ import Widgets from './Widgets.js' import TextWidgets from './Text.js' import docx from 'docx' +import { right } from 'inquirer/lib/utils/readline.js' class TableWidgets extends Widgets { constructor(env) { @@ -110,7 +111,7 @@ class TableWidgets extends Widgets { size: 20, type: docx.WidthType.PERCENTAGE }, - children: [this.textWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.generalSettings.tableFontSize, bold: firstColumnBold})], borders: leftBorderStyle }), new docx.TableCell({ @@ -118,7 +119,7 @@ class TableWidgets extends Widgets { size: 80, type: docx.WidthType.PERCENTAGE }, - children: [this.textWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], + children: [this.textWidgets.makeParagraph(col2, {fontSize: this.generalSettings.tableFontSize, bold: allColumnsBold})], borders: rightBorderStyle }) ] @@ -170,7 +171,7 @@ class TableWidgets extends Widgets { text: col2, style: 'Hyperlink', font: this.generalSettings.font, - size: this.generalSettings.fullFontSize, + size: this.generalSettings.tableFontSize, bold: allColumnsBold }) ], @@ -185,7 +186,7 @@ class TableWidgets extends Widgets { size: 20, type: docx.WidthType.PERCENTAGE }, - children: [this.textWidgets.makeParagraph(col1, {fontSize: this.generalSettings.fullFontSize, bold: firstColumnBold})], + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.generalSettings.tableFontSize, bold: firstColumnBold})], borders: leftBorderStyle }), new docx.TableCell({ @@ -238,24 +239,36 @@ class TableWidgets extends Widgets { size: 40, type: docx.WidthType.PERCENTAGE, }, - children: [this.textWidgets.makeParagraph(col1, {fontSize: this.fontFactor * this.fontSize, bold: firstColumnBold})], - borders: borderStyle + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.generalSettings.tableFontSize, bold: firstColumnBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } }), new docx.TableCell({ width: { size: 30, type: docx.WidthType.PERCENTAGE, }, - children: [this.textWidgets.makeParagraph(col2, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], - borders: borderStyle + children: [this.textWidgets.makeParagraph(col2, {fontSize: this.generalSettings.tableFontSize, bold: allColumnsBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } }), new docx.TableCell({ width: { size: 30, type: docx.WidthType.PERCENTAGE, }, - children: [this.textWidgets.makeParagraph(col3, {fontSize: this.fontFactor * this.fontSize, bold: allColumnsBold})], - borders: borderStyle + children: [this.textWidgets.makeParagraph(col3, {fontSize: this.generalSettings.tableFontSize, bold: allColumnsBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } }), ] }) @@ -298,9 +311,10 @@ class TableWidgets extends Widgets { size: 100, type: docx.WidthType.PERCENTAGE, }, - children: [this.textWidgets.makeParagraph(row1, {fontSize: this.fontFactor * this.fontSize, bold: firstRowBold, align: centerFirstRow ? docx.AlignmentType.CENTER : docx.AlignmentType.START})], + children: [this.textWidgets.makeParagraph(row1, {fontSize: this.generalSettings.tableFontSize, bold: firstRowBold, align: centerFirstRow ? docx.AlignmentType.CENTER : docx.AlignmentType.START})], margins: { - bottom: this.generalSettings.tableMargin + bottom: this.generalSettings.tableMargin, + right: this.generalSettings.tableMargin }, borders: this.rightBorder @@ -315,10 +329,11 @@ class TableWidgets extends Widgets { size: 100, type: docx.WidthType.PERCENTAGE, }, - children: [this.textWidgets.makeParagraph(row2, {fontSize: this.fontFactor * this.fontSize, bold: allRowsBold})], + children: [this.textWidgets.makeParagraph(row2, {fontSize: this.generalSettings.tableFontSize, bold: allRowsBold})], borders: this.bottomAndRightBorders, margins: { - bottom: this.generalSettings.tableMargin + bottom: this.generalSettings.tableMargin, + right: this.generalSettings.tableMargin } }), ], @@ -346,7 +361,7 @@ class TableWidgets extends Widgets { right: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, }, shading: {fill: this.themeSettings.tagColor}, - children: [this.textWidgets.makeParagraph(tag, {fontSize: this.fontSize, fontColor: this.themeSettings.tagFontColor, center: true})], + children: [this.textWidgets.makeParagraph(tag, {fontSize: this.generalSettings.tableFontSize, fontColor: this.themeSettings.tagFontColor, center: true})], verticalAlign: docx.AlignmentType.CENTER, }) } @@ -570,7 +585,26 @@ class TableWidgets extends Widgets { }) } - // Create the dashboard shell which will contain all of the outputs + /** + * @function createDashboardShell - Create a dashboard shell with left and right contents + * @param {Array} leftContents - the contents for the left side of the dashboard + * @param {Array} rightContents - the contents for the right side of the dashboard + * @param {Object} options + * @returns {Object} a docx Table object + */ + // Create and return the dashboard shell with left and right contents + /* + ---------------------------------------------- + | Left% | Right% | + | | | + | | | + | | | + | | | + | | | + | | | + | | | + ---------------------------------------------- + */ createDashboardShell (leftContents, rightContents, options={}) { const { allBorders = false, diff --git a/src/report/widgets/Widgets.js b/src/report/widgets/Widgets.js index 1dc952b..c566613 100644 --- a/src/report/widgets/Widgets.js +++ b/src/report/widgets/Widgets.js @@ -23,7 +23,7 @@ class Widgets { default: { heading1: { run: { - size: this.generalSettings.fullFontSize, + size: this.generalSettings.titleFontSize, bold: true, font: this.generalSettings.font, color: this.themeSettings.textFontColor @@ -37,7 +37,7 @@ class Widgets { }, heading2: { run: { - size: 0.75 * this.generalSettings.fullFontSize, + size: 0.9 * this.generalSettings.titleFontSize, bold: true, font: this.generalSettings.font, color: this.themeSettings.textFontColor @@ -51,7 +51,7 @@ class Widgets { }, heading3: { run: { - size: 0.8 * this.generalSettings.fullFontSize, + size: 0.8 * this.generalSettings.titleFontSize, bold: true, font: this.generalSettings.font, color: this.themeSettings.textFontColor From 9421b0f742fee7772e686f5ff28a814fc9a95526 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 11 Aug 2024 16:02:30 -0700 Subject: [PATCH 06/23] Most least similar companies pt 1 --- src/report/companies.js | 165 ++++++++++++++++++++----------------- src/report/widgets/Text.js | 43 ++++++++++ 2 files changed, 131 insertions(+), 77 deletions(-) diff --git a/src/report/companies.js b/src/report/companies.js index 182dd2f..bdaa66c 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -17,6 +17,7 @@ import { Utilities as CLIUtilities } from '../cli/common.js' import { getMostSimilarCompany } from './tools.js' import TextWidgets from './widgets/Text.js' import TableWidgets from './widgets/Tables.js' +import { read } from 'xlsx' class BaseCompanyReport { constructor(company, env) { @@ -224,79 +225,62 @@ class CompanySection extends BaseCompanyReport { ] } - async makeCompetitorsDOCX(competitors, isPackage){ - const myUtils = new CLIUtilities() - let competitivePages = [] - let totalReadingTime = null - for (const myComp in competitors) { - // Filter in the competitor - const competitor = competitors[myComp] - - // Construct the object to create company related document sections - const comp = new CompanySection(competitor.company, this.env) - - // Create a section for the most/least similar interactions - const interact = new InteractionSection( - [ - competitor.mostSimilar.interaction, - competitor.leastSimilar.interaction - ], - competitor.company.name, - 'Company' - ) + async makeCompetitorDOCX(similarCompany, interactions, similarity, isPackage){ + let competitivePage = [] - // Compute reading time - totalReadingTime += - parseInt(competitor.mostSimilar.interaction.reading_time) + - parseInt(competitor.leastSimilar.interaction.reading_time) - - // Create the company firmographics table - const firmographicsTable = comp.makeFirmographicsDOCX() - - // Assemble the rows and table - const myRows = [ - this.util.basicTopicRow('Name', 'Percent Similar', 'Category', true), - this.util.basicTopicRow( - competitor.mostSimilar.name, - competitor.mostSimilar.score, - 'Most Similar'), - this.util.basicTopicRow( - competitor.leastSimilar.name, - competitor.leastSimilar.score, - 'Least Similar'), - ] - const summaryTable = new docx.Table({ - columnWidths: [60, 20, 20], - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - - // Construct this competitive pages - competitivePages.push( - this.util.makeHeadingBookmark2(`Firmographics for: ${competitor.company.name}`), - firmographicsTable, - this.util.makeHeadingBookmark2('Table for most/least similar interactions'), - summaryTable, - this.util.makeHeadingBookmark2('Interaction descriptions'), - ...interact.makeDescriptionsDOCX(), - this.util.makeHeadingBookmark2('Interaction summaries'), - ...interact.makeReferencesDOCX(isPackage) - ) + // Construct the object to create company related document sections + const comp = new CompanySection(similarCompany, this.env) + // Create the company firmographics table + const firmographicsTable = comp.makeFirmographicsDOCX() + const tagsTable = this.tableWidgets.tagsTable(similarCompany.tags) + + // Create a section for the most/least similar interactions + const mostSimIntName = similarity[similarCompany.name].most_similar.name + const mostSimIntScore = Math.round(parseFloat(similarity[similarCompany.name].most_similar.score) * 100) + const mostSimInt = interactions.filter(interaction => interaction.name === mostSimIntName)[0] + const leastSimIntName = similarity[similarCompany.name].least_similar.name + const leastSimIntScore = Math.round(parseFloat(similarity[similarCompany.name].least_similar.score) * 100) + const leastSimInt = interactions.filter(interaction => interaction.name === leastSimIntName)[0] + + // Compute reading time + const totalReadingTime = parseInt(mostSimInt.reading_time) + parseInt(leastSimInt.reading_time) + + // Build the most/least similar interactions tables + const simTableRows = [ + this.tableWidgets.threeColumnRowBasic(['Interaction Name', 'Similarity Score', 'Type'], {allColumnsBold: true}), + this.tableWidgets.threeColumnRowBasic([mostSimIntName, mostSimIntScore, 'Most Similar'], {firstColumnBold: false}), + this.tableWidgets.threeColumnRowBasic([leastSimIntName, leastSimIntScore, 'Least Similar'], {firstColumnBold: false}) + ] + const simTable = new docx.Table({ + columnWidths: [60, 20, 20], + rows: simTableRows, + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) - } + // Create a section for the most/least similar interactions + const interact = new InteractionSection( + [mostSimInt, leastSimInt], + similarCompany.name, + 'Company' + ) + + // Construct this competitive pages + competitivePage.push( + this.util.makeHeadingBookmark2(`Details for: ${similarCompany.name}`), + firmographicsTable, + this.util.makeHeadingBookmark2('Tags'), + tagsTable, + this.util.makeHeadingBookmark2('Table for most/least similar interactions'), + simTable, + this.util.makeHeadingBookmark2('Interaction abstracts'), + // ...interact.makeReferencesDOCX(isPackage) + ) // Return the document fragment - return [ - this.util.pageBreak(), - this.util.makeHeadingBookmark1('Competitive Content'), - this.util.makeParagraph( - `For compared companies additional data is provided including firmographics, most/least similar interaction table, most/least similar interaction descriptions, and most/least similar interaction summaries.\r\rTotal estimated reading time for all source most/least similar interactions is ${totalReadingTime} minutes.` - ), - ...competitivePages - ] + return {reading_time: totalReadingTime, doc: competitivePage} } } @@ -326,7 +310,9 @@ class CompanyStandalone extends BaseCompanyReport { this.title = `${this.company.name} Company Report` this.interactions = sourceData.allInteractions this.competitors = sourceData.competitors.all - this.description = `A Company report for ${this.company.name} and including relevant company data.` + this.mostSimilar = sourceData.competitors.mostSimilar + this.leastSimilar = sourceData.competitors.leastSimilar + this.description = `A Company report for ${this.company.name} that includes firmographics, key information on competitors, and Interactions data.` this.introduction = `Mediumroast for GitHub automatically generated this document. It includes company firmographics, key information on competitors, and Interactions data for ${this.company.name}. If this report is produced as a package, then the hyperlinks are active and will link to documents on the local folder after the package is opened.` this.similarity = this.company.similarity, this.noInteractions = String(Object.keys(this.company.linked_interactions).length) @@ -364,19 +350,34 @@ class CompanyStandalone extends BaseCompanyReport { const authoredBy = `Authored by: ${this.authoredBy}` const preparedFor = `${this.authoredBy} report for: ` - // Construct the company section + // Construct the companySection and interactionSection objects const companySection = new CompanySection(this.company, this.env) - const interactionSection = new InteractionSection( - this.interactions, - this.company.name, - this.objectType, - this.env - ) + // const interactionSection = new InteractionSection( + // this.interactions, + // this.company.name, + // this.objectType, + // this.env + // ) + + // Construct the dashboard object const myDash = new CompanyDashbord(this.env) + // Build sections for most and least similar companies + const mostSimilarReport = await companySection.makeCompetitorDOCX(this.mostSimilar, this.interactions, this.similarity, isPackage) + + const leastSimilarReport = await companySection.makeCompetitorDOCX(this.leastSimilar, this.interactions, this.similarity, isPackage) + + // Compute the total reading time for all source most/least similar interactions + const totalReadingTime = mostSimilarReport.reading_time + leastSimilarReport.reading_time + + const mostLeastSimilarIntro = this.textWidgets.makeParagraph( + `For the most and least similar companies, additional data is provided including firmographics, most/least similar interaction table, and most/least similar interaction abstracts.\n\nTotal estimated reading time for all interactions from most/least similar companies is ${totalReadingTime} minutes, but reading the abstracts will take less time.` + ) + // Set up the default options for the document const myDocument = [].concat( this.util.makeIntro(this.introduction), + // Generate the firmographics and tags sections for the Company being reported on [ this.textWidgets.makeHeading1('Firmographics'), companySection.makeFirmographicsDOCX(), @@ -384,7 +385,17 @@ class CompanyStandalone extends BaseCompanyReport { this.tableWidgets.tagsTable(this.company.tags), this.textWidgets.makeHeading1('Competitive Similarity'), ], + // Generate the comparisons section for the Company being reported on to characterize competitive similarity companySection.makeComparisonDOCX(this.similarity, this.competitors), + [ + this.textWidgets.pageBreak(), + this.textWidgets.makeHeadingBookmark1('Detail For Most/Least Similar Companies'), + mostLeastSimilarIntro, + ...mostSimilarReport.doc, + ...leastSimilarReport.doc + ], + + // [ this.util.makeHeading1('Topics'), // this.util.makeParagraph( // 'The following topics were automatically generated from all ' + diff --git a/src/report/widgets/Text.js b/src/report/widgets/Text.js index eb350de..103880d 100644 --- a/src/report/widgets/Text.js +++ b/src/report/widgets/Text.js @@ -183,6 +183,49 @@ class TextWidgets extends Widgets { }) } + /** + * @function makeHeadingBookmark1 + * @description Create a target within a document to link to with an internal hyperlink of heading 1 + * @param {String} text - text/prose for the function + * @param {String} ident - the unique name of the bookmark + * @returns {Object} a new docx paragraph object with a bookmark at the heading level 1 + * @todo could we generalize this function and make the heading level a parameter in the future? + */ + makeHeadingBookmark1(text, ident) { + return new docx.Paragraph({ + heading: docx.HeadingLevel.HEADING_1, + children: [ + new docx.Bookmark({ + id: String(ident), + children: [ + new docx.TextRun({text: text}) + ] + }) + ] + }) + } + + /** + * @function makeHeadingBookmark2 + * @description Create a target within a document to link to with an internal hyperlink of heading 2 + * @param {String} text - text/prose for the function + * @param {String} ident - the unique name of the bookmark + * @returns {Object} a new docx paragraph object with a bookmark at the heading level 2 + */ + makeHeadingBookmark2(text, ident) { + return new docx.Paragraph({ + heading: docx.HeadingLevel.HEADING_2, + children: [ + new docx.Bookmark({ + id: String(ident), + children: [ + new docx.TextRun({text: text}) + ] + }) + ] + }) + } + /** * @function makeIntro * @description Creates an introduction paragraph with a heading of level 1 From e28798d0fe258d9f8b23f196c4ecb4a7d15f77ec Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 11 Aug 2024 18:10:42 -0700 Subject: [PATCH 07/23] For most/least similar added interactions w/ md --- src/report/companies.js | 22 +++++-- src/report/interactions.js | 123 +++++++++++++++++------------------ src/report/widgets/Tables.js | 85 ++++++++++++++++++++++++ src/report/widgets/Text.js | 42 ++++++++++++ 4 files changed, 204 insertions(+), 68 deletions(-) diff --git a/src/report/companies.js b/src/report/companies.js index bdaa66c..6e1137d 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -264,19 +264,29 @@ class CompanySection extends BaseCompanyReport { const interact = new InteractionSection( [mostSimInt, leastSimInt], similarCompany.name, - 'Company' + 'Company', + this.env ) + // Replaced any spaces in the company name with underscores + const company_bookmark_base = String(similarCompany.name.replace(/ /g, '_')).substring(0, 20) + // Construct this competitive pages competitivePage.push( - this.util.makeHeadingBookmark2(`Details for: ${similarCompany.name}`), + this.textWidgets.makeHeadingBookmark2(`Details for: ${similarCompany.name}`, `company_${company_bookmark_base}`), firmographicsTable, - this.util.makeHeadingBookmark2('Tags'), + this.textWidgets.makeHeadingBookmark2('Tags', `tags_${company_bookmark_base}`), tagsTable, - this.util.makeHeadingBookmark2('Table for most/least similar interactions'), + this.textWidgets.makeHeadingBookmark2('Table for most/least similar interactions', `similarities_${company_bookmark_base}`), simTable, - this.util.makeHeadingBookmark2('Interaction abstracts'), - // ...interact.makeReferencesDOCX(isPackage) + this.textWidgets.makeHeadingBookmark2('Interaction abstracts', `abstracts_${company_bookmark_base}`), + ...interact.makeReferencesDOCX(isPackage, + { + bookmarkName: `Back to ${similarCompany.name}`, + bookmarkLink: `company_${company_bookmark_base}` + } + ), + this.textWidgets.pageBreak() ) // Return the document fragment diff --git a/src/report/interactions.js b/src/report/interactions.js index e30d353..b2d12b3 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -13,6 +13,8 @@ import DOCXUtilities from './common.js' import { CompanySection } from './companies.js' import { InteractionDashboard } from './dashboard.js' import docxSettings from './settings.js' +import TextWidgets from './widgets/Text.js' +import TableWidgets from './widgets/Tables.js' class BaseInteractionsReport { constructor(interactions, objectType, objectName, env) { @@ -27,6 +29,8 @@ class BaseInteractionsReport { this.objectType = objectType this.env = env this.util = new DOCXUtilities(env) + this.textWidgets = new TextWidgets(env) + this.tableWidgets = new TableWidgets(env) this.themeStyle = docxSettings[env.theme] // Set the theme for the report this.generalStyle = docxSettings.general // Pull in all of the general settings @@ -211,12 +215,13 @@ class InteractionSection extends BaseInteractionsReport { * @param {Boolean} isPackage - When set to true links are set up for connecting to interaction documents * @returns {Array} An array containing a section description and a table of interaction references */ - makeReferencesDOCX(isPackage) { - // Link this back to the descriptions section - const descriptionsLink = this.util.makeInternalHyperLink( - 'Back to Interaction Summaries', - 'interaction_summaries' - ) + makeReferencesDOCX(isPackage, bookmark=null) { + const { + bookmarkName = 'Back to Interaction Descriptions', + bookmarkLink ='interaction_descriptions' + } = bookmark + + const descriptionsLink = this.textWidgets.makeInternalHyperLink(bookmarkName, bookmarkLink) // Create the array for the references starting with the introduction let references = [] @@ -228,63 +233,59 @@ class InteractionSection extends BaseInteractionsReport { totalReadingTime += parseInt(this.interactions[interaction].reading_time) // Create the link to the underlying interaction document - const objWithPath = this.interactions[interaction].url.split('://').pop() - const myObj = objWithPath.split('/').pop() + // TODO consider making this a hyperlink to the interaction document in GitHub + const myObj = this.interactions[interaction].url.split('/').pop() let interactionLink = this.util.makeExternalHyperLink( 'Document', - './interactions/' + myObj + `./interactions/${myObj}` ) // Depending upon if this is a package or not create the metadata strip with/without document link - let metadataStrip = null + let metadataRow + let metadataStrip if(isPackage) { // isPackage version of the strip - metadataStrip = new docx.Paragraph({ - spacing: { - before: 100, - }, - children: [ - this.util.makeTextrun('[ '), + metadataRow = this.tableWidgets.fourColumnRowBasic( + [ interactionLink, - this.util.makeTextrun( - ' | Created on: ' + - this.interactions[interaction].creation_date + - ' | ' - ), - this.util.makeTextrun( - ' Est. Reading Time: ' + - this.interactions[interaction].reading_time + ' min' + - ' | ' - ), - descriptionsLink, - this.util.makeTextrun(' ]'), - ] + `Created on: ${this.interactions[interaction].creation_date}`, + `Est. reading time: ${this.interactions[interaction].reading_time} min`, + descriptionsLink + ], + {firstColumnBold: false} + ) + metadataStrip = new docx.Table({ + columnWidths: [25, 25, 25, 25], + rows: [metadataRow], + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } }) } else { // Non isPackage version of the strip - metadataStrip = new docx.Paragraph({ - spacing: { - before: 100, - }, - children: [ - this.util.makeTextrun('[ '), - this.util.makeTextrun( - 'Creation Date: ' + - this.interactions[interaction].creation_date + - ' | ' - ), - this.util.makeTextrun( - ' Est. Reading Time: ' + - this.interactions[interaction].reading_time + ' min' + - ' | ' - ), - descriptionsLink, - this.util.makeTextrun(' ]'), - ] + metadataRow = this.tableWidgets.fourColumnRowBasic( + [ + `Created on: ${this.interactions[interaction].creation_date}`, + `Est. reading time: ${this.interactions[interaction].reading_time} min`, + descriptionsLink + ], + {firstColumnBold: false} + ) + metadataStrip = new docx.Table({ + columnWidths: [50, 25, 25], + rows: [metadataRow], + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } }) } + + + // Create the tags table for the interaction + const tagsTable = this.tableWidgets.tagsTable(this.interactions[interaction].tags) - // NOTE: Early reviews by users show topcis are confusing // Generate the topic table // const topics = this.util.rankTags(this.interactions[interaction].topics) // const topicTable = this.util.topicTable(topics) @@ -296,30 +297,28 @@ class InteractionSection extends BaseInteractionsReport { this.interactions[interaction].name, String( 'interaction_' + - String(this.interactions[interaction].id) + String(this.interactions[interaction].file_hash) ).substring(0, 40) ), // Create the abstract for the interaction - this.util.makeParagraph( - this.interactions[interaction].abstract, - {fontSize: this.util.halfFontSize * 1.5} - ), + this.util.makeParagraph(this.interactions[interaction].abstract), + metadataStrip, + this.textWidgets.makeHeading2('Tags'), + tagsTable, // NOTE: Early reviews by users show topcis are confusing // this.util.makeHeading2('Topics'), // topicTable, - metadataStrip ) } references.splice(0,0, // Section intro - this.util.makeParagraph( - 'The Mediumroast for GitHub automatically generated this section.' + - ' It includes key metadata from each interaction associated to the object ' + this.objectName + - '. If this report document is produced as a package, instead of standalone, then the' + - ' hyperlinks are active and will link to documents on the local folder after the' + - ' package is opened. ' + - `Note that the total estimated reading time for all interactions is ${totalReadingTime} minutes.` + this.textWidgets.makeParagraph( + 'The Mediumroast for GitHub automatically generated this section. ' + + `It includes key metadata from each interaction associated to the company ${this.objectName}. ` + + 'If this report is produced as a package, then hyperlinks are present and will link to documents ' + + 'on the local system. ' + + `Note the total estimated reading time for all interactions is ${totalReadingTime} minutes.` ) ) diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index 845eb2c..6262740 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -274,6 +274,91 @@ class TableWidgets extends Widgets { }) } + /** + * @function fourColumnRowBasic + * @description Basic table row with 4 columns + * @param {Array} cols - an array of 4 strings to be used as the text/prose for the cells + * * @param {Object} options - options for the cell to control bolding in the first column and all columns and border styles + * @returns {Object} a new 3 column docx TableRow object + */ + fourColumnRowBasic (cols, options={}) { + let { + firstColumnBold = true, + allColumnsBold = false, + allBorders = false, + bottomBorders = true, + } = options + + // Set the first column to bold if all columns are bold + if (allColumnsBold) { + firstColumnBold = true + } + + // Set the border style + let borderStyle = this.noBorders + if (allBorders) { + borderStyle = this.allBorders + } else if (bottomBorders) { + borderStyle = this.bottomBorder + } + // Destructure the cols array + const [col1, col2, col3, col4] = cols + + // return the row + return new docx.TableRow({ + children: [ + new docx.TableCell({ + width: { + size: 40, + type: docx.WidthType.PERCENTAGE, + }, + children: [this.textWidgets.makeParagraph(col1, {fontSize: this.generalSettings.tableFontSize, bold: firstColumnBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } + }), + new docx.TableCell({ + width: { + size: 30, + type: docx.WidthType.PERCENTAGE, + }, + children: [this.textWidgets.makeParagraph(col2, {fontSize: this.generalSettings.tableFontSize, bold: allColumnsBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } + }), + new docx.TableCell({ + width: { + size: 30, + type: docx.WidthType.PERCENTAGE, + }, + children: [this.textWidgets.makeParagraph(col3, {fontSize: this.generalSettings.tableFontSize, bold: allColumnsBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } + }), + new docx.TableCell({ + width: { + size: 30, + type: docx.WidthType.PERCENTAGE, + }, + children: [this.textWidgets.makeParagraph(col4, {fontSize: this.generalSettings.tableFontSize, bold: allColumnsBold})], + borders: borderStyle, + margins: { + bottom: this.generalSettings.tableMargin, + top: this.generalSettings.tableMargin + } + }), + ] + }) + } + oneColumnTwoRowsBasic (rows, options={}) { // Desctructure the options object let { diff --git a/src/report/widgets/Text.js b/src/report/widgets/Text.js index 103880d..ddd88fc 100644 --- a/src/report/widgets/Text.js +++ b/src/report/widgets/Text.js @@ -226,6 +226,48 @@ class TextWidgets extends Widgets { }) } + /** + * @function makeInternalHyperLink + * @description Create an external hyperlink + * @param {String} text - text/prose for the function + * @param {String} link - the URL for the hyperlink within the document + * @returns {Object} a new docx InternalHyperlink object + */ + makeInternalHyperLink(text, link) { + return new docx.InternalHyperlink({ + children: [ + new docx.TextRun({ + text: text, + style: 'Hyperlink', + font: this.font, + size: 16, + }), + ], + anchor: link, + }) + } + + /** + * @function makeBookmark + * @description Create a target within a document to link to with an internal hyperlink + * @param {String} text - text/prose for the function + * @param {String} ident - the unique name of the bookmark + * @returns {Object} a new docx paragraph object with a bookmark + * @todo test and revise this function as it may need to be a textrun which can be embedded in something else + */ + makeBookmark(text, ident) { + return new docx.Paragraph({ + children: [ + new docx.Bookmark({ + id: String(ident), + children: [ + new docx.TextRun({text: text}) + ] + }) + ] + }) + } + /** * @function makeIntro * @description Creates an introduction paragraph with a heading of level 1 From 61a5a436042ef7202d0a2859dfd38268ec169390 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 11 Aug 2024 18:37:01 -0700 Subject: [PATCH 08/23] Added references for primary company --- src/report/companies.js | 30 +++++++++++++---------------- src/report/interactions.js | 39 ++++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/report/companies.js b/src/report/companies.js index 6e1137d..3d14323 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -13,11 +13,9 @@ import boxPlot from 'box-plot' import DOCXUtilities from './common.js' import { InteractionSection } from './interactions.js' import { CompanyDashbord } from './dashboard.js' -import { Utilities as CLIUtilities } from '../cli/common.js' import { getMostSimilarCompany } from './tools.js' import TextWidgets from './widgets/Text.js' import TableWidgets from './widgets/Tables.js' -import { read } from 'xlsx' class BaseCompanyReport { constructor(company, env) { @@ -318,6 +316,7 @@ class CompanyStandalone extends BaseCompanyReport { this.author = author this.authoredBy = author this.title = `${this.company.name} Company Report` + this.companyInteractions = sourceData.interactions this.interactions = sourceData.allInteractions this.competitors = sourceData.competitors.all this.mostSimilar = sourceData.competitors.mostSimilar @@ -362,12 +361,12 @@ class CompanyStandalone extends BaseCompanyReport { // Construct the companySection and interactionSection objects const companySection = new CompanySection(this.company, this.env) - // const interactionSection = new InteractionSection( - // this.interactions, - // this.company.name, - // this.objectType, - // this.env - // ) + const interactionSection = new InteractionSection( + this.companyInteractions, + this.company.name, + this.objectType, + this.env + ) // Construct the dashboard object const myDash = new CompanyDashbord(this.env) @@ -406,19 +405,16 @@ class CompanyStandalone extends BaseCompanyReport { ], - // [ this.util.makeHeading1('Topics'), - // this.util.makeParagraph( - // 'The following topics were automatically generated from all ' + - // this.noInteractions + ' interactions associated to this company.' - // ), - // this.util.makeHeadingBookmark1('Interaction Summaries', 'interaction_summaries') - // ], // ...interactionSection.makeDescriptionsDOCX(), - // await companySection.makeCompetitorsDOCX(this.competitors, isPackage), + // [ this.util.pageBreak(), // this.util.makeHeading1('References') // ], - // ...interactionSection.makeReferencesDOCX(isPackage) + [ + this.textWidgets.pageBreak(), + this.textWidgets.makeHeading1('References') + ], + ...interactionSection.makeReferencesDOCX(isPackage) ) // Construct the document diff --git a/src/report/interactions.js b/src/report/interactions.js index b2d12b3..382140b 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -56,7 +56,7 @@ class BaseInteractionsReport { new docx.TableRow({ children: [ new docx.TableCell({ - children: [this.util.makeParagraph('Id', {bold: true, fontSize: this.generalStyle.dashFontSize})], + children: [this.textWidgets.makeParagraph('Id', {bold: true, fontSize: this.generalStyle.tableFontSize})], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -67,7 +67,7 @@ class BaseInteractionsReport { }, }), new docx.TableCell({ - children: [this.util.makeParagraph('Frequency', {bold: true, fontSize: this.generalStyle.dashFontSize})], + children: [this.textWidgets.makeParagraph('Frequency', {bold: true, fontSize: this.generalStyle.tableFontSize})], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -78,7 +78,7 @@ class BaseInteractionsReport { } }), new docx.TableCell({ - children: [this.util.makeParagraph('Proto-requirement', {bold: true, fontSize: this.generalStyle.dashFontSize})], + children: [this.textWidgets.makeParagraph('Proto-requirement', {bold: true, fontSize: this.generalStyle.tableFontSize})], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -96,7 +96,7 @@ class BaseInteractionsReport { new docx.TableRow({ children: [ new docx.TableCell({ - children: [this.util.makeParagraph(topic, {fontSize: this.generalStyle.dashFontSize})], + children: [this.textWidgets.makeParagraph(topic, {fontSize: this.generalStyle.tableFontSize})], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -107,7 +107,7 @@ class BaseInteractionsReport { }, }), new docx.TableCell({ - children: [this.util.makeParagraph(topics[topic].frequency, {fontSize: this.generalStyle.dashFontSize})], + children: [this.textWidgets.makeParagraph(topics[topic].frequency, {fontSize: this.generalStyle.tableFontSize})], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -118,7 +118,7 @@ class BaseInteractionsReport { }, }), new docx.TableCell({ - children: [this.util.makeParagraph(topics[topic].label, {fontSize: this.generalStyle.dashFontSize})], + children: [this.textWidgets.makeParagraph(topics[topic].label, {fontSize: this.generalStyle.tableFontSize})], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -215,7 +215,7 @@ class InteractionSection extends BaseInteractionsReport { * @param {Boolean} isPackage - When set to true links are set up for connecting to interaction documents * @returns {Array} An array containing a section description and a table of interaction references */ - makeReferencesDOCX(isPackage, bookmark=null) { + makeReferencesDOCX(isPackage, bookmark={}) { const { bookmarkName = 'Back to Interaction Descriptions', bookmarkLink ='interaction_descriptions' @@ -232,19 +232,18 @@ class InteractionSection extends BaseInteractionsReport { // Calculate the aggregate reading time totalReadingTime += parseInt(this.interactions[interaction].reading_time) - // Create the link to the underlying interaction document - // TODO consider making this a hyperlink to the interaction document in GitHub - const myObj = this.interactions[interaction].url.split('/').pop() - let interactionLink = this.util.makeExternalHyperLink( - 'Document', - `./interactions/${myObj}` - ) - // Depending upon if this is a package or not create the metadata strip with/without document link let metadataRow let metadataStrip if(isPackage) { // isPackage version of the strip + // Create the link to the underlying interaction document + // TODO consider making this a hyperlink to the interaction document in GitHub + const myObj = this.interactions[interaction].url.split('/').pop() + let interactionLink = this.util.makeExternalHyperLink( + 'Document', + `./interactions/${myObj}` + ) metadataRow = this.tableWidgets.fourColumnRowBasic( [ interactionLink, @@ -286,9 +285,8 @@ class InteractionSection extends BaseInteractionsReport { // Create the tags table for the interaction const tagsTable = this.tableWidgets.tagsTable(this.interactions[interaction].tags) - // Generate the topic table - // const topics = this.util.rankTags(this.interactions[interaction].topics) - // const topicTable = this.util.topicTable(topics) + // Generate the proto-requirements table + const protoRequirementsTable = this.protorequirementsTable(this.interactions[interaction].topics) // Push all of the content into the references array references.push( @@ -305,9 +303,8 @@ class InteractionSection extends BaseInteractionsReport { metadataStrip, this.textWidgets.makeHeading2('Tags'), tagsTable, - // NOTE: Early reviews by users show topcis are confusing - // this.util.makeHeading2('Topics'), - // topicTable, + this.textWidgets.makeHeading2('Proto-Requirements'), + protoRequirementsTable ) } From 719dbac76e1e4728095cba67771d53a408ca1399 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 11 Aug 2024 19:54:27 -0700 Subject: [PATCH 09/23] Company reports complete --- src/report/companies.js | 11 +++-------- src/report/interactions.js | 28 ++++++++++++++-------------- src/report/settings.js | 10 +++++----- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/report/companies.js b/src/report/companies.js index 3d14323..1ef74d6 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -401,15 +401,10 @@ class CompanyStandalone extends BaseCompanyReport { this.textWidgets.makeHeadingBookmark1('Detail For Most/Least Similar Companies'), mostLeastSimilarIntro, ...mostSimilarReport.doc, - ...leastSimilarReport.doc + ...leastSimilarReport.doc, + this.textWidgets.makeHeading1('Interaction Descriptions') ], - - - // ...interactionSection.makeDescriptionsDOCX(), - - // [ this.util.pageBreak(), - // this.util.makeHeading1('References') - // ], + ...interactionSection.makeDescriptionsDOCX(), [ this.textWidgets.pageBreak(), this.textWidgets.makeHeading1('References') diff --git a/src/report/interactions.js b/src/report/interactions.js index 382140b..8218816 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -173,24 +173,26 @@ class InteractionSection extends BaseInteractionsReport { const noInteractions = this.interactions.length // Create the header row for the descriptions - let myRows = [this.util.descriptionRow('Id', 'Description', true)] + let myRows = [this.tableWidgets.twoColumnRowBasic(['Name', 'Description'], {allColumnsBold: true})] // Loop over the interactions and pull out the interaction ids and descriptions for (const interaction in this.interactions) { - myRows.push(this.util.descriptionRow( - // Create the internal hyperlink for the interaction reference - this.util.makeInternalHyperLink( - this.interactions[interaction].id, 'interaction_' + String(this.interactions[interaction].id) - ), - // Pull in the description - this.interactions[interaction].description + const interactionBaseLink = String(`interaction_${this.interactions[interaction].file_hash}`).substring(0, 20) + myRows.push(this.tableWidgets.twoColumnRowBasic( + [ + this.textWidgets.makeInternalHyperLink( + this.interactions[interaction].name, interactionBaseLink + ), + this.interactions[interaction].description + ], + {firstColumnBold: false} ) ) } // define the table with the summary theme information const myTable = new docx.Table({ - columnWidths: [10, 90], + columnWidths: [30, 70], rows: myRows, width: { size: 100, @@ -200,10 +202,8 @@ class InteractionSection extends BaseInteractionsReport { // Return the results as an array return [ - this.util.makeParagraph( - 'This section contains descriptions for the ' + noInteractions + ' interactions associated to the ' + - this.objectName + ' ' + this.objectType + ' object. Additional detail is in ' + - 'the References section of this document.' + this.textWidgets.makeParagraph( + `This section contains descriptions for the ${noInteractions} interactions associated to the ${this.objectName} ${this.objectType}. Additional detail is in the References section of this document.` ), myTable ] @@ -296,7 +296,7 @@ class InteractionSection extends BaseInteractionsReport { String( 'interaction_' + String(this.interactions[interaction].file_hash) - ).substring(0, 40) + ).substring(0, 20) ), // Create the abstract for the interaction this.util.makeParagraph(this.interactions[interaction].abstract), diff --git a/src/report/settings.js b/src/report/settings.js index 3efe274..8dd6fed 100644 --- a/src/report/settings.js +++ b/src/report/settings.js @@ -51,11 +51,11 @@ const docxSettings = { titleFontColor: "C7701E", // Saturated Light Blue textFontColor: "C7701E", // Ultra light Blue chartAxisLineColor: "#374246", - chartAxisFontColor: "rgba(71,121,140, 0.7)", - chartAxisTickFontColor: "rgba(149,181,192, 0.6)", - chartItemFontColor: "rgba(149,181,192, 0.9)", - chartSeriesColor: "rgb(71,113,128)", - chartSeriesBorderColor: "rgba(149,181,192, 0.9)", + chartAxisFontColor: "rgba(199,112,30, 0.7)", + chartAxisTickFontColor: "rgba(199,112,30, 0.6)", + chartItemFontColor: "rgba(199,112,30, 0.9)", + chartSeriesColor: "rgb(199,112,30, 0.7)", + chartSeriesBorderColor: "rgba(199,112,30,, 0.9)", highlightFontColor: "" }, latte: { From 2e45ddf2792e08f13ed1c4be7e04e9e1263c5cb4 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Mon, 12 Aug 2024 20:36:47 -0700 Subject: [PATCH 10/23] Coerced values in descriptiveTable to strings, fixed coloring --- cli/mrcli.js | 2 +- package.json | 2 +- src/report/settings.js | 4 ++-- src/report/widgets/Tables.js | 5 +++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/mrcli.js b/cli/mrcli.js index 096e1ba..f53ed54 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.7.1') + .version('0.7.2') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index e1f383c..741fe51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.7.1", + "version": "0.7.2", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/report/settings.js b/src/report/settings.js index 8dd6fed..c48d985 100644 --- a/src/report/settings.js +++ b/src/report/settings.js @@ -55,7 +55,7 @@ const docxSettings = { chartAxisTickFontColor: "rgba(199,112,30, 0.6)", chartItemFontColor: "rgba(199,112,30, 0.9)", chartSeriesColor: "rgb(199,112,30, 0.7)", - chartSeriesBorderColor: "rgba(199,112,30,, 0.9)", + chartSeriesBorderColor: "rgba(199,112,30, 0.9)", highlightFontColor: "" }, latte: { @@ -65,7 +65,7 @@ const docxSettings = { documentColor: "F1F0EE", // Coffee black titleFontColor: "25110f", // Saturated Light Blue textFontColor: "25110f", // Ultra light Blue - chartAxisLineColor: "#25110f", + chartAxisLineColor: "#374246", chartAxisFontColor: "rgba(37,17,15, 0.7)", chartAxisTickFontColor: "rgba(37,17,15, 0.6)", chartItemFontColor: "rgba(37,17,15, 0.9)", diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index 6262740..e22b79c 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -2,11 +2,12 @@ import Widgets from './Widgets.js' import TextWidgets from './Text.js' import docx from 'docx' -import { right } from 'inquirer/lib/utils/readline.js' +import FilesystemOperators from '../../cli/filesystem.js' class TableWidgets extends Widgets { constructor(env) { super(env) + this.filesystem = new FilesystemOperators() // Define specifics for table borders this.noneStyle = { style: this.generalSettings.noBorderStyle @@ -570,7 +571,7 @@ class TableWidgets extends Widgets { new docx.TableCell({ children: [ this.textWidgets.makeParagraph( - statistics[stat].value, + String(statistics[stat].value), { fontSize: this.generalSettings.metricFontSize, fontColor: this.themeSettings.titleFontColor, From 88e1c9263ef18811376330e24c0e156bd2c7ddc9 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Tue, 13 Aug 2024 05:03:46 -0700 Subject: [PATCH 11/23] Update actions and workflows from mr --- .../actions/basic-reporting/companies.js | 67 +++++ .../actions/basic-reporting/company.js | 262 ++++++++++++++++++ .../actions/basic-reporting/index copy.js | 53 ++++ cli/actions/actions/basic-reporting/index.js | 6 +- .../actions/basic-reporting/interactions.js | 75 +++++ cli/actions/actions/basic-reporting/main.js | 86 ++++++ .../actions/basic-reporting/package.json | 2 +- .../actions/basic-reporting/reports.js | 22 +- cli/actions/workflows/prune-branches.yml | 1 - 9 files changed, 562 insertions(+), 12 deletions(-) create mode 100644 cli/actions/actions/basic-reporting/companies.js create mode 100644 cli/actions/actions/basic-reporting/company.js create mode 100644 cli/actions/actions/basic-reporting/index copy.js create mode 100644 cli/actions/actions/basic-reporting/interactions.js create mode 100644 cli/actions/actions/basic-reporting/main.js diff --git a/cli/actions/actions/basic-reporting/companies.js b/cli/actions/actions/basic-reporting/companies.js new file mode 100644 index 0000000..b7724e4 --- /dev/null +++ b/cli/actions/actions/basic-reporting/companies.js @@ -0,0 +1,67 @@ +const mrMarkdownBuilder = require('mr_markdown_builder') + +// Globals +const MAPS_WARNING = `**Notice:** If you are using Safari and had previously disabled \`Prevent cross-site tracking\` feature in the \`Privacy tab\` in Safari's preferences, you can now reenable it since this bug has been fixed by GitHub.${mrMarkdownBuilder.cr()}${mrMarkdownBuilder.cr()}` + +function createCompaniesMap (companies) { + // Filter out companies with unknown latitude or longitude + companies = companies.filter((company) => company.latitude !== 'Unknown' && company.longitude !== 'Unknown') + // Create the map + let map = mrMarkdownBuilder.h1('Company Locations') + map += MAPS_WARNING + map += mrMarkdownBuilder.geojson({ + type: 'FeatureCollection', + features: companies.map((company) => { + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [company.longitude, company.latitude] + }, + properties: { + name: company.name, + description: company.description, + role: company.role, + url: company.url + } + } + }) + }) + // return the map + return mrMarkdownBuilder.cr() + map +} + +function createCompaniesReport (companies) { + let readme = `[${mrMarkdownBuilder.link('Back to main README', '../README.md')}]\n` + readme += mrMarkdownBuilder.hr() + readme += mrMarkdownBuilder.h1('Introduction') + readme += `There are currently \`${companies.length}\` companies in the repository. The table below lists all available companies and some of their firmographics. Click on the company name to view the company's profile. Below the table is a map of all companies in the repository. Click on a company's marker to view additional company information in context.` + readme += mrMarkdownBuilder.h1('Table of Companies') + // Create the table header + const tableHeader = mrMarkdownBuilder.tableHeader(['Company Name', 'Company Type', 'Company Role', 'Company Region']) + // Create the table rows + const tableRows = companies.map((company) => { + const companyRow = [ + mrMarkdownBuilder.link(company.name, `./${encodeURI(company.name.replace(/[\s,.\?!]/g, ''))}.md`), + company.company_type, + company.role, + company.region + ] + return companyRow + }) + // Create the table + const companyTable = tableHeader + "\n" + mrMarkdownBuilder.tableRows(tableRows) + // Create the README.md file + readme += companyTable + // Add a line break + readme += mrMarkdownBuilder.cr() + mrMarkdownBuilder.hr() + // Call the createMap function + readme += mrMarkdownBuilder.cr() + createCompaniesMap(companies) + // Return the file content + return readme + +} + +module.exports = { + createCompaniesReport +} \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/company.js b/cli/actions/actions/basic-reporting/company.js new file mode 100644 index 0000000..6e64569 --- /dev/null +++ b/cli/actions/actions/basic-reporting/company.js @@ -0,0 +1,262 @@ +const mrMarkdownBuilder = require('mr_markdown_builder') +const interactionsBuilder = require('./interactions.js') + + +// Globals +const MAPS_WARNING = `**Notice:** If you are using Safari and had previously disabled \`Prevent cross-site tracking\` feature in the \`Privacy tab\` in Safari's preferences, you can now reenable it since this bug has been fixed by GitHub.` + +async function createBadges(company) { + // Create a badge for the company role + const badgesRow = [ + await mrMarkdownBuilder.badge(encodeURIComponent('Role'), company.role), + await mrMarkdownBuilder.badge(encodeURIComponent('Type'), encodeURIComponent(company.company_type)), + await mrMarkdownBuilder.badge(encodeURIComponent('Region'), company.region), + await mrMarkdownBuilder.badge(encodeURIComponent('Creator'), encodeURIComponent(company.creator_name)) + ] + return "\n" + badgesRow.join(mrMarkdownBuilder.space()) + "\n" +} + +function createIndustryList(company) { + const industryDataList = [ + `${mrMarkdownBuilder.b('Major Group')} ${mrMarkdownBuilder.rightArrow()} ${company.major_group_description} (Code: ${company.major_group_code})`, + `${mrMarkdownBuilder.b('Industry Group')} ${mrMarkdownBuilder.rightArrow()} ${company.industry_group_description} (Code: ${company.industry_group_code})`, + `${mrMarkdownBuilder.b('Industry')} ${mrMarkdownBuilder.rightArrow()} ${company.industry} (Code: ${company.industry_code})` + ] + // Create a list of industries + let industryList = `${mrMarkdownBuilder.b('Industry:')} ${company.industry} (Code: ${company.industry_code}) ${mrMarkdownBuilder.cr()}` + industryList += mrMarkdownBuilder.collapsible( + `Industry Details, click to expand`, mrMarkdownBuilder.ul(industryDataList) + ) + return industryList +} + +function createCompanyWebLinkList(company) { + // Create the table rows + let wikipediaURL + company.wikipedia_url === 'Unknown' ? + wikipediaURL = `The Wikipedia URL is ${company.wikipedia_url}` : + wikipediaURL = mrMarkdownBuilder.link(`Wikipedia for ${company.name}`, company.wikipedia_url) + let listItems = [ + + [wikipediaURL], + [mrMarkdownBuilder.link(`${company.name} on Google News`, encodeURIComponent(company.google_news_url))], + [mrMarkdownBuilder.link(`Map for ${company.name}`, encodeURIComponent(company.google_maps_url))], + [mrMarkdownBuilder.link(`${company.name} Patents`, encodeURIComponent(company.google_patents_url))] + ] + // If the company is public then add the public properties + if (company.company_type === 'Public') { + const propertyToName = { + google_finance_url: 'Google Finance', + recent10k_url: 'Most Recent 10-K Filing', + recent10q_url: 'Most Recent 10-Q Filing', + firmographics_url: 'SEC EDGAR Firmographics', + filings_url: `All Filings for ${company.name}`, + owner_transactions_url: 'Shareholder Transactions' + } + for (const property in [ + 'google_finance_url', 'recent10k_url', 'recent10q_url', 'firmographics_url', 'filings_url', 'owner_transactions_url'] + ) { + if (company[property] !== 'Unknown') { continue } + listItems.push([mrMarkdownBuilder.link(propertyToName[property], company[property])]) + } + } + // Create the table + return mrMarkdownBuilder.h2('Key Web Links') + "\n" + mrMarkdownBuilder.ul(listItems) +} + +function createInteractionList(company, interactions) { + // Create a list of interactions + const interactionNames = Object.keys(company.linked_interactions) + const interactionList = interactionNames.map((interactionName) => { + // Find the interaction object that matches the interaction name + const interaction = interactions.find((interaction) => interaction.name === interactionName) + // Create link internal link to the interaction file + const interactionLink = mrMarkdownBuilder.link(interaction.name, `/${encodeURI(interaction.url)}`) + return interactionLink + }) + + return `${mrMarkdownBuilder.h2('Interactions')} \n ${mrMarkdownBuilder.ul(interactionList)}` +} + +function createCompanyMap(company) { + // Check to see if either the latitude or longitude is "Unknown" and if so return false + if (company.latitude === 'Unknown' || company.longitude === 'Unknown') { + return '' + } + let geoJsonMarkdown = mrMarkdownBuilder.h2('Location') + geoJsonMarkdown += MAPS_WARNING + mrMarkdownBuilder.cr() + mrMarkdownBuilder.cr() + // Create the location JSON + const geoJson = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [company.longitude, company.latitude] + }, + properties: { + name: company.name, + description: company.description, + role: company.role, + url: company.url + } + } + // Add the geojson object to the company file + geoJsonMarkdown += mrMarkdownBuilder.geojson(geoJson) + return geoJsonMarkdown +} + +async function createTags (company) { + // Create a list of interaction tags + const companyProperties = Object.keys(company) + const tags = companyProperties.map((property) => { + return mrMarkdownBuilder.tag(property) + }) + return mrMarkdownBuilder.cr() + tags.join(mrMarkdownBuilder.space()) + mrMarkdownBuilder.cr() +} + +async function createSimilarCompany(company) { + // Add the company name with the logo image + const companyLogo = mrMarkdownBuilder.imageWithSize(`${company.name} Logo`, company.logo_url, 20, company.name) + // Create the link to the markdown file + const companyLink = mrMarkdownBuilder.link(company.name, `./${encodeURI(company.name.replace(/[\s,.\?!]/g, ''))}.md`) + // With the logo create an h3 header with the company name that also links to the company's markdown file + let similarCompany = mrMarkdownBuilder.h3(`${companyLogo} ${companyLink}`) + // Add a line break + similarCompany += mrMarkdownBuilder.cr() + // Add the company badges + similarCompany += await createBadges(company) + // Add a line break + similarCompany += mrMarkdownBuilder.cr() + // Add the company description + similarCompany += `${mrMarkdownBuilder.b('Description:')} ${company.description} ${mrMarkdownBuilder.cr()}` + // Add a line break + similarCompany += mrMarkdownBuilder.cr() + // Create the Industry List + similarCompany += createIndustryList(company) + // return the similar company markdown + return similarCompany +} + + + +async function createInteractionList(interaction, isMostSimilar) { + // Destructure the interactions object into most and least + // Skip interaction if any section is "Unknown" + if (interaction.description === "Unknown" || interaction.abstract === "Unknown" || interaction.reading_time === "Unknown" || interaction.tags === "Unknown") { + return null + } + + // Create link internal link to the interaction file + const interactionLink = mrMarkdownBuilder.link(interaction.name, `/${encodeURIComponent(interaction.url)}`) + + const mostLeastSimilar = isMostSimilar ? 'Most Similar Interaction: ' : 'Least Similar Interaction: ' + + // Create the interaction section + let interactionSection = `${mrMarkdownBuilder.h3(mostLeastSimilar + interactionLink)}${mrMarkdownBuilder.cr()}` + interactionSection += await interactionsBuilder.createMetadataBadges(interaction) + mrMarkdownBuilder.cr() + interactionSection += `${interaction.description}${mrMarkdownBuilder.cr()}` + + return interactionSection +} + +async function createSimilarCompanies(similarCompanies, companies, interactions) { + // Create a list of similar companies to process + const mostSimilarCompany = Object.keys(similarCompanies).reduce((a, b) => similarCompanies[a] > similarCompanies[b] ? a : b) + // Append the most similar company to the list from the companies array + const companyObject = companies.find((company) => company.name === mostSimilarCompany) + // Create the most similar company markdown + const mostSimilarCompanySection = await createSimilarCompany(companyObject) + // Get the most and least similar interactions from the similarCompanies object using companyObject name + const mostSimilarInteraction = interactions.find((interaction) => interaction.name === similarCompanies[mostSimilarCompany].most_similar.name) + const leastSimilarInteraction = interactions.find((interaction) => interaction.name === similarCompanies[mostSimilarCompany].least_similar.name) + console.log(mostSimilarInteraction) + // Create the most and least similar interactions markdown + const mostSimilar = await createInteractionList(mostSimilarInteraction, true) + const leastSimilar = await createInteractionList(leastSimilarInteraction, false) + // Combine the most and least similar interactions into a single string + const similarInteractions = `${mostSimilar}${leastSimilar}` + // Process the similar interactions list by calling the createSimilarInteractions function, this should return the markdown for the similar interactions + // Return the markdown for the similar companies and interactions + return `${mrMarkdownBuilder.h2('Most Similar Company')}${mrMarkdownBuilder.cr()}${mostSimilarCompanySection}${mrMarkdownBuilder.cr()}${similarInteractions}${mrMarkdownBuilder.hr()}` +} + +async function createCompanyReport(companies, interactions, reports) { + let companyReports = [] + const suffix = '.md' + const prefix = reports.company + for (let company of companies) { + // ---- BEGIN Company Introduction ---- + // Get the name of the company and remove spaces, commas, periods, question marks, and exclamation points + const companyFileName = company.name.replace(/[\s,.\?!]/g, '') + // Add company name with the logo image + const companyLogo = mrMarkdownBuilder.imageWithSize(`${company.name} Logo`, company.logo_url, 25, company.name) + // Call the h1 method from the headers module + let companyFile = `[${mrMarkdownBuilder.link('Back to Company Directory', './README.md')}]\n` + companyFile += mrMarkdownBuilder.hr() + companyFile += mrMarkdownBuilder.h1(`${companyLogo} ${mrMarkdownBuilder.link(company.name, company.url)}`) + // Add a line break + companyFile += mrMarkdownBuilder.cr() + // Add the company badges + companyFile += await createBadges(company) + // Add a line break + companyFile += mrMarkdownBuilder.cr() + // Add the company description + companyFile += `${mrMarkdownBuilder.b('Description:')} ${company.description} ${mrMarkdownBuilder.cr()}` + // Add a line break + companyFile += mrMarkdownBuilder.cr() + // Create the Industry List + companyFile += createIndustryList(company) + // If there are tags add them to the company file + if (company.tags && Object.keys(company.tags).length > 0) { + companyFile += `${mrMarkdownBuilder.h3('Tags')}${mrMarkdownBuilder.cr()}${await createTags(company.tags)}${mrMarkdownBuilder.cr()}` + } + // Add a horizontal rule + companyFile += mrMarkdownBuilder.hr() + // Add a line break + companyFile += mrMarkdownBuilder.cr() + // ---- End Company Introduction ---- + + // If similar companies exist, add them to the company file + if (company.similarity && Object.keys(company.similarity).length > 0) { + companyFile += await createSimilarCompanies(company.similarity, companies, interactions) + } + + // If there are linked_interactions in the company then create the interaction list + if (Object.keys(company.linked_interactions).length > 0) { + companyFile += await interactionsBuilder.createInteractionsSection(company, interactions); + } + + // Add a line break + companyFile += mrMarkdownBuilder.cr() + + // Create the company table + companyFile += createCompanyWebLinkList(company) + + // Add a line break + companyFile += mrMarkdownBuilder.cr() + + // Add an h2 for the company's location + companyFile += createCompanyMap(company) + + // Add a line break + companyFile += mrMarkdownBuilder.cr() + + // Add a horizontal rule + companyFile += mrMarkdownBuilder.hr() + + // Add the creation date + companyFile += `[ ${mrMarkdownBuilder.b('Created:')} ${company.creation_date} by ${company.creator_name} | ${mrMarkdownBuilder.b('Modified:')} ${company.modification_date} ]` + + // Return the file content + companyReports.push({ + name: company.name, + path: `${prefix}${companyFileName}${suffix}`, + content: companyFile + }) + } + return companyReports +} + + +module.exports = { + createCompanyReport +} \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/index copy.js b/cli/actions/actions/basic-reporting/index copy.js new file mode 100644 index 0000000..4b67e20 --- /dev/null +++ b/cli/actions/actions/basic-reporting/index copy.js @@ -0,0 +1,53 @@ +// Load the modules +const { readObjects, readBranches, readWorkflows, saveReports } = require('./github.js') +const { createCompaniesReport, createCompanyReports, createMainReport } = require('./reports.js') + + +// Create the run function that creates the reports +async function run () { + // Define inputs + const inputs = { + companies: await readObjects('/Companies/Companies.json'), + interactions: await readObjects('/Interactions/Interactions.json'), + branches: await readBranches(), + workflows: await readWorkflows() + } + + // If there are no companies then return + if (inputs.companies.length === 0) { + return + } + + // Define reports + const reports = { + companies: `Companies/README.md`, + company: `Companies/`, + main: `README.md`, + } + + // Create the company files + const companyFiles = await createCompanyReports(inputs.companies, inputs.interactions, reports) + // Create the companies file + const companiesFile = createCompaniesReport(inputs.companies) + // Create the main file + const mainFile = createMainReport(inputs) + // Create the reports array + const markdownReports = [ + { + name: 'Companies', + path: reports.companies, + content: companiesFile + }, + { + name: 'Main', + path: reports.main, + content: mainFile + }, + ...companyFiles + ] + + // Write the reports + await saveReports(markdownReports, inputs) +} + +run() \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/index.js b/cli/actions/actions/basic-reporting/index.js index 4b67e20..b421935 100644 --- a/cli/actions/actions/basic-reporting/index.js +++ b/cli/actions/actions/basic-reporting/index.js @@ -1,6 +1,8 @@ // Load the modules const { readObjects, readBranches, readWorkflows, saveReports } = require('./github.js') -const { createCompaniesReport, createCompanyReports, createMainReport } = require('./reports.js') +const { createCompaniesReport } = require('./companies.js') +const { createCompanyReport } = require('./company.js') +const { createMainReport } = require('./main.js') // Create the run function that creates the reports @@ -26,7 +28,7 @@ async function run () { } // Create the company files - const companyFiles = await createCompanyReports(inputs.companies, inputs.interactions, reports) + const companyFiles = await createCompanyReport(inputs.companies, inputs.interactions, reports) // Create the companies file const companiesFile = createCompaniesReport(inputs.companies) // Create the main file diff --git a/cli/actions/actions/basic-reporting/interactions.js b/cli/actions/actions/basic-reporting/interactions.js new file mode 100644 index 0000000..52ae3ec --- /dev/null +++ b/cli/actions/actions/basic-reporting/interactions.js @@ -0,0 +1,75 @@ +const mrMarkdownBuilder = require('mr_markdown_builder') + +async function createTags (interaction) { + // Create a list of interaction tags + const interactionProperties = Object.keys(interaction) + const tags = interactionProperties.map((property) => { + return mrMarkdownBuilder.tag(property) + }) + return "\n" + tags.join(mrMarkdownBuilder.space()) + "\n" + +} + +async function createMetadataBadges(metadata) { + // Create a badge for the company role + const badgesRow = [ + await mrMarkdownBuilder.badge('Reading time', encodeURIComponent(`${metadata.reading_time} minutes`)), + await mrMarkdownBuilder.badge('Interaction type', encodeURIComponent(metadata.interaction_type)), + await mrMarkdownBuilder.badge('Page count', metadata.page_count), + await mrMarkdownBuilder.badge('Document type', encodeURIComponent(metadata.content_type)) + ] + return mrMarkdownBuilder.cr() + badgesRow.join(mrMarkdownBuilder.space()) + mrMarkdownBuilder.cr() +} + +async function createInteractionsList (company, interactions) { + // Create a list of interactions + const interactionNames = Object.keys(company.linked_interactions) + let totalReadingTime = 0 + + const interactionList = await Promise.all(interactionNames.map(async (interactionName) => { + // Find the interaction object that matches the interaction name + const interaction = interactions.find((interaction) => interaction.name === interactionName) + + // Skip interaction if any section is "Unknown" + if (interaction.description === "Unknown" || interaction.abstract === "Unknown" || interaction.reading_time === "Unknown" || interaction.tags === "Unknown") { + return null + } + + // Create link internal link to the interaction file + const interactionLink = mrMarkdownBuilder.link(interaction.name, `/${encodeURIComponent(interaction.url)}`) + + // Create the interaction section + let interactionSection = `${mrMarkdownBuilder.h3(interactionLink)}${mrMarkdownBuilder.cr()}` + interactionSection += await createMetadataBadges(interaction) + mrMarkdownBuilder.cr() + interactionSection += `${interaction.description}${mrMarkdownBuilder.cr()}` + interactionSection += `${mrMarkdownBuilder.h4('Discovered tags')}${mrMarkdownBuilder.cr()}` + interactionSection += `${await createTags(interaction.tags)}${mrMarkdownBuilder.cr()}` + totalReadingTime += parseInt(interaction.reading_time) + interactionSection += `${mrMarkdownBuilder.collapsible('Interaction abstract', interaction.abstract)}${mrMarkdownBuilder.cr()}` + + return interactionSection + })) + + // Filter out null values + const filteredInteractionList = interactionList.filter(Boolean) + const interactionString = filteredInteractionList.join(mrMarkdownBuilder.cr()); + + return [interactionString, totalReadingTime] +} + +async function createInteractionsSection(company, interactions) { + // Call createInteractionsList and add the result to interactionsSection + let [interactionsList, totalReadingTime] = await createInteractionsList(company, interactions) + + // Create the interactions section + let interactionsSection = mrMarkdownBuilder.h2('Interactions') + const companyInteractions = Object.keys(company.linked_interactions) + interactionsSection += `\`${company.name}\` has \`${companyInteractions.length}\` interactions in the repository, and the reading time for all interactions is \`${totalReadingTime}\` minutes.` + + return `${interactionsSection}${mrMarkdownBuilder.cr()}${interactionsList}${mrMarkdownBuilder.cr()}${mrMarkdownBuilder.hr()}` +} + +module.exports = { + createInteractionsSection, + createMetadataBadges +} \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/main.js b/cli/actions/actions/basic-reporting/main.js new file mode 100644 index 0000000..bc0ff94 --- /dev/null +++ b/cli/actions/actions/basic-reporting/main.js @@ -0,0 +1,86 @@ +const mrMarkdownBuilder = require('mr_markdown_builder') + +function createMainReport (inputs) { + const ACTION_WARNING = `**Notice:** This page is automatically generated by the custom GitHub action \`basic-reporting\`. It is scheduled to run at 12:00 AM everyday and will update \`README.md\` files in the \`/Companies\` directory, generate a \`.md\` file for each company in the \`/Companies\` directory, and update the main \`README.md\` file in the root of the repository. Manual updates to these files are not recommended as they will be overwritten the next time the action runs.\n` + + // Loop through all of the inputs.companies to find the one company with the role of "Owner" + const owner = inputs.companies.find((company) => company.role === 'Owner') + // Create the owner logo + const ownerLogo = mrMarkdownBuilder.imageWithSize(`${owner.name} Logo`, owner.logo_url, 25, owner.name) + + // Create the main report + let readme = mrMarkdownBuilder.h1(`${ownerLogo} Product and Service Discovery Repository for ${owner.name}`) + readme += `Welcome to your discovery repository meant to help you build products and services via an evidence based approach. Presently, the repository contains \`${inputs.companies.length}\` companies and \`${inputs.interactions.length}\` interactions. This repository means to be the warehouse for all evidence needed to generate the why\'s and what\'s for your product or service plans. It is intentionally integrated into GitHub to help you leverage the power of GitHub's ecosystem.` + + // Create a paragraph about companies, the title should be h2 and contain a link to the Companies/README.md file + readme += mrMarkdownBuilder.h2(`Companies [${mrMarkdownBuilder.link('View Companies', './Companies/README.md')}]`) + readme += `Companies are presently the primary object used and the repository contains \`${inputs.companies.length}\` of them. This means you'll be able to find information about these companies in the repository. Each company has a profile page containing information about the company including its name, description, industry, and location. Additionally, each company has a list of interactions that are linked to it.` + + // Create a paragraph about interactions, the title should be h2 and not contain a link to the Interactions/README.md file + readme += mrMarkdownBuilder.h2(`Interactions`) + readme += `Interaction objects are essentially content related to a company. They can include meeting notes, emails, product documentation, blog posts, audio transcripts, and more. While each interaction is linked to a company access to the interaction is presented handled by the company that owns it.` + + readme += mrMarkdownBuilder.h2(`Studies`) + readme += `Study objects will be a part of a future release of Mediumroast and will be a part of a paid subscription. Stay tuned for more information on this feature and a way to gain access to it.` + + readme += mrMarkdownBuilder.h2('Navigation and Modification') + readme +=`Direct navigation and modifcation of repository contents is not recommended. Instead this README file, and accompanying markdown files, will guide you through its contents. Additionally, the open source node module and CLI \`mediumroast_js\` [${mrMarkdownBuilder.link('GitHub', 'https://github.com/mediumroast/mediumroast_js')}, ${mrMarkdownBuilder.link('NPM', 'https://www.npmjs.com/package/mediumroast_js')}] can be used to create, update, and delete content in the repository.` + + // Add the notice + readme += mrMarkdownBuilder.h2('Notice') + readme += ACTION_WARNING + + // Create a paragraph that focuses on listing the active workflows and their last status + readme += mrMarkdownBuilder.h2('Workflows') + // Get the current month and year and put them in a single string + const date = new Date() + const month = date.toLocaleString('default', { month: 'long' }) + const year = date.getFullYear() + + readme += `The repository contains \`2\` active workflows. As of \`${month}-${year}\` \`${inputs.workflows.runTime} minutes\` have been consumed. A GitHub free plan has \`2000 minutes\` available per month meaning there is \`${2000 - inputs.workflows.runTime}\` remaining minutes for the month. Assuming a repository with 10s of company objects, each workflow runs about a minute at midnight everyday. This means a good hueristic for how many minutes are consumed in a month is 2 workflows/day x 1 min/workflow x 30 days/month or \`${2*1*30} min/month\`. To get an accurate view of your consumed minutes for your planning please run \`mrcli billing\`. The statuses of five most recent workflow runs are provided below, links are included to enable more information on the workflows.\n` + // Create the table header + const workflowTableHeader = mrMarkdownBuilder.tableHeader(['Workflow Name', 'Last Status', 'Run Time Message', 'Run Time']) + // Create the table rows + const myWorkflows = inputs.workflows.allWorkFlows.slice(1, 6) + const workflowTableRows = myWorkflows.map((workflow) => { + const workflowRow = [ + mrMarkdownBuilder.link(workflow.name, `./.github/workflows/${workflow.name}.yml`), + mrMarkdownBuilder.link(workflow.conclusion, workflow.html_url), + workflow.run_time_name, + `${workflow.run_time_minutes} minute(s)` + ] + return workflowRow + }) + // Create the table + const workflowTable = workflowTableHeader + "\n" + mrMarkdownBuilder.tableRows(workflowTableRows) + // Add the table to the README.md file + readme += workflowTable + + // Add a line break + readme += "\n" + + // Create a paragraph that lists the branches and their last commit + readme += mrMarkdownBuilder.h2('Branches') + readme += `The repository contains \`${inputs.branches.length}\` branches. The last commit of each branch is listed below. Click on the branch name to view the branch.\n` + // Create the table header + const branchTableHeader = mrMarkdownBuilder.tableHeader(['Branch Name', 'Last Commit']) + // Create the table rows + const branchTableRows = inputs.branches.map((branch) => { + const branchRow = [ + mrMarkdownBuilder.link(branch.name, `./tree/${branch.name}`), + branch.last_commit + ] + return branchRow + }) + // Create the table + const branchTable = branchTableHeader + "\n" + mrMarkdownBuilder.tableRows(branchTableRows) + // Add the table to the README.md file + readme += branchTable + + // Return the report content + return readme +} + +module.exports = { + createMainReport +} \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/package.json b/cli/actions/actions/basic-reporting/package.json index f3edad3..8f6a971 100644 --- a/cli/actions/actions/basic-reporting/package.json +++ b/cli/actions/actions/basic-reporting/package.json @@ -12,6 +12,6 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", - "mr_markdown_builder": "^0.9.3" + "mr_markdown_builder": "^1.1.0" } } diff --git a/cli/actions/actions/basic-reporting/reports.js b/cli/actions/actions/basic-reporting/reports.js index e88a8c0..1a6400d 100644 --- a/cli/actions/actions/basic-reporting/reports.js +++ b/cli/actions/actions/basic-reporting/reports.js @@ -43,14 +43,20 @@ function createCompanyWebLinkList (company) { ] // If the company is public then add the public properties if (company.company_type === 'Public') { - listItems.push( - [mrMarkdownBuilder.link(`Google Finance`, company.google_finance_url)], - [mrMarkdownBuilder.link(`Most Recent 10-K Filing`, company.recent10k_url)], - [mrMarkdownBuilder.link(`Most Recent 10-Q Filing`, company.recent10q_url)], - [mrMarkdownBuilder.link(`SEC EDGAR Firmographics`, company.firmographics_url)], - [mrMarkdownBuilder.link(`All Filings for ${company.name}`, company.filings_url)], - [mrMarkdownBuilder.link(`Shareholder Transactions`, company.owner_transactions_url)] - ) + const propertyToName = { + google_finance_url: 'Google Finance', + recent10k_url: 'Most Recent 10-K Filing', + recent10q_url: 'Most Recent 10-Q Filing', + firmographics_url: 'SEC EDGAR Firmographics', + filings_url: `All Filings for ${company.name}`, + owner_transactions_url: 'Shareholder Transactions' + } + for (const property in [ + 'google_finance_url', 'recent10k_url', 'recent10q_url', 'firmographics_url', 'filings_url', 'owner_transactions_url'] + ) { + if (company[property] !== 'Unknown') { continue } + listItems.push([mrMarkdownBuilder.link(propertyToName[property], company[property])]) + } } // Create the table return mrMarkdownBuilder.h2('Key Web Links') + "\n" + mrMarkdownBuilder.ul(listItems) diff --git a/cli/actions/workflows/prune-branches.yml b/cli/actions/workflows/prune-branches.yml index 309ed9d..e993545 100644 --- a/cli/actions/workflows/prune-branches.yml +++ b/cli/actions/workflows/prune-branches.yml @@ -3,7 +3,6 @@ run-name: ${{ github.actor }} executed prune-branches action on: schedule: - cron: '0 0 * * *' # Runs at midnight every day - workflow_dispatch: jobs: prune-branches: runs-on: ubuntu-latest From 42abd21299d51cc7766c4a6340caf27a081b9012 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 18 Aug 2024 12:38:07 -0700 Subject: [PATCH 12/23] Update actions, actions and storage CLIs, deprecate billing CLI --- cli/mrcli-actions.js | 33 +- cli/mrcli-billing.js | 11 +- cli/mrcli-setup.js | 2 +- cli/mrcli-storage.js | 91 ++++ cli/mrcli.js | 23 +- package-lock.json | 1127 ++++++++++++++++----------------------- package.json | 8 +- src/api/authorize.js | 40 ++ src/api/gitHubServer.js | 162 ++++-- src/api/github.js | 176 +++--- src/cli/output.js | 27 +- 11 files changed, 871 insertions(+), 829 deletions(-) create mode 100644 cli/mrcli-storage.js diff --git a/cli/mrcli-actions.js b/cli/mrcli-actions.js index 2f24bfd..a4d2b77 100644 --- a/cli/mrcli-actions.js +++ b/cli/mrcli-actions.js @@ -10,16 +10,18 @@ */ // Import required modules -import { Billings } from '../src/api/gitHubServer.js' +import { Actions } from '../src/api/gitHubServer.js' import Environmentals from '../src/cli/env.js' import CLIOutput from '../src/cli/output.js' +import ora from "ora" +import chalk from 'chalk' // Related object type const objectType = 'Actions' // Environmentals object const environment = new Environmentals( - '2.0', + '1.0.0', `${objectType}`, `Command line interface to report on and update actions.`, objectType @@ -56,6 +58,10 @@ myProgram = environment.removeArgByName(myProgram, '--find_by_id') myProgram = environment.removeArgByName(myProgram, '--report') myProgram = environment.removeArgByName(myProgram, '--package') myProgram = environment.removeArgByName(myProgram, '--splash') +myProgram = environment.removeArgByName(myProgram, '--update') +myProgram + .option('-u, --update', 'Update actions and workflows from Mediumroast for GitHub package') + .option('-b, --billing', 'Return all actions billing information for the GitHub organization') // Parse the command line arguments into myArgs and obtain the options let myArgs = myProgram.parse(process.argv) @@ -66,23 +72,32 @@ let myEnv = environment.getEnv(myArgs, myConfig) const accessToken = await environment.verifyAccessToken() const processName = 'mrcli-actions' -// Output object -const output = new CLIOutput(myEnv, objectType) - // Construct the controller objects -const actionsCtl = new Billings(accessToken, myEnv.gitHubOrg, processName) +const actionsCtl = new Actions(accessToken, myEnv.gitHubOrg, processName) // Predefine the results variable let [success, stat, results] = [null, null, null] if (myArgs.update) { - [success, stat, results] = await billingsCtl.getActionsBilling() + let spinner = ora(chalk.bold.blue('Updating actions and workflows on GitHub ... ')) + spinner.start() // Start the spinner + const updates = await actionsCtl.updateActions() + spinner.stop() // Stop the spinner + if (updates[0]) { + console.log(`SUCCESS: A total of [${updates[2].total}] actions and workflows updated successfully.`) + process.exit(0) + } else { + console.log(`ERROR: Installing actions and workflows failed.\nTotal attempted: [${updates[2].total}] -> total failed: ${updates[2].failCount}; total successul: ${updates[2].successCount}.\nError message: ${updates[1].status_msg}`) + process.exit(-1) + } +} else if (myArgs.billing) { + [success, stat, results] = await actionsCtl.getActionsBilling() const myUserOutput = new CLIOutput(myEnv, 'ActionsBilling') myUserOutput.outputCLI([results], myArgs.output) process.exit() } else { - [success, stat, results] = await billingsCtl.getAll() - const myUserOutput = new CLIOutput(myEnv, 'AllBilling') + [success, stat, results] = await actionsCtl.getAll() + const myUserOutput = new CLIOutput(myEnv, 'Workflows') myUserOutput.outputCLI(results, myArgs.output) process.exit() } diff --git a/cli/mrcli-billing.js b/cli/mrcli-billing.js index eea4e80..75787ee 100644 --- a/cli/mrcli-billing.js +++ b/cli/mrcli-billing.js @@ -8,11 +8,16 @@ * @license Apache-2.0 * @verstion 2.0.0 */ +import chalk from 'chalk' +console.log(chalk.bold.yellow('WARNING: The billing subcommand is deprecated in this release, and replaced by actions and storage subcommands,\n\ttry \'mrcli actions --help\' or \'mrcli storage --help\' for more information.')) +process.exit() // Import required modules -import { Billings } from '../src/api/gitHubServer.js' -import Environmentals from '../src/cli/env.js' -import CLIOutput from '../src/cli/output.js' +// import { Billings } from '../src/api/gitHubServer.js' +// import Environmentals from '../src/cli/env.js' +// import CLIOutput from '../src/cli/output.js' + + // Related object type const objectType = 'Billings' diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index 9a631c3..4d6800f 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -142,7 +142,7 @@ async function confirmGitHubOrg(token, env) { // Obtain the intel based upon the organization the user input const gitHubOrg = await gitHubCtl.getGitHubOrg() - // console.log(gitHubOrg) + if(!gitHubOrg[0]){ tryAgain = await wizardUtils.operationOrNot( `Unfortunately, no organization matching [${gitHubOrgName}] was found. Maybe you mistyped it, try again?` diff --git a/cli/mrcli-storage.js b/cli/mrcli-storage.js new file mode 100644 index 0000000..20c0271 --- /dev/null +++ b/cli/mrcli-storage.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +/** + * A CLI utility used for accessing and reporting on mediumroast.io user objects + * @author Michael Hay + * @file actions.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @verstion 1.0.0 + */ + +// Import required modules +import { Storage } from '../src/api/gitHubServer.js' +import Environmentals from '../src/cli/env.js' +import CLIOutput from '../src/cli/output.js' +import ora from "ora" +import chalk from 'chalk' + +// Related object type +const objectType = 'Storage' + +// Environmentals object +const environment = new Environmentals( + '1.0.0', + `${objectType}`, + `Command line interface to report on and update actions.`, + objectType +) + +/* + ----------------------------------------------------------------------- + + FUNCTIONS - Key functions needed for MAIN + + ----------------------------------------------------------------------- +*/ + + +/* + ----------------------------------------------------------------------- + + MAIN - Steps below represent the main function of the program + + ----------------------------------------------------------------------- +*/ + +// Create the environmental settings +let myProgram = environment.parseCLIArgs(true) + +// Remove command line options for reset_by_type, delete, update, and add_wizard by calling the removeArgByName method in the environmentals class +myProgram = environment.removeArgByName(myProgram, '--delete') +myProgram = environment.removeArgByName(myProgram, '--add_wizard') +myProgram = environment.removeArgByName(myProgram, '--reset_by_type') +myProgram = environment.removeArgByName(myProgram, '--report') +myProgram = environment.removeArgByName(myProgram, '--find_by_name') +myProgram = environment.removeArgByName(myProgram, '--find_by_x') +myProgram = environment.removeArgByName(myProgram, '--find_by_id') +myProgram = environment.removeArgByName(myProgram, '--report') +myProgram = environment.removeArgByName(myProgram, '--package') +myProgram = environment.removeArgByName(myProgram, '--splash') +myProgram = environment.removeArgByName(myProgram, '--update') +myProgram + .option('-b, --billing', 'Return all actions billing information for the GitHub organization') + +// Parse the command line arguments into myArgs and obtain the options +let myArgs = myProgram.parse(process.argv) +myArgs = myArgs.opts() + +const myConfig = environment.readConfig(myArgs.conf_file) +let myEnv = environment.getEnv(myArgs, myConfig) +const accessToken = await environment.verifyAccessToken() +const processName = 'mrcli-storage' + +// Construct the controller objects +const storageCtl = new Storage(accessToken, myEnv.gitHubOrg, processName) + +// Predefine the results variable +let [success, stat, results] = [null, null, null] + +if (myArgs.billing) { + [success, stat, results] = await storageCtl.getStorageBilling() + const myUserOutput = new CLIOutput(myEnv, 'StorageBilling') + myUserOutput.outputCLI([results], myArgs.output) + process.exit() +} else { + const storageResults = await storageCtl.getAll() + const myUserOutput = new CLIOutput(myEnv, 'Storage') + myUserOutput.outputCLI([storageResults[2]], myArgs.output) + process.exit() +} + diff --git a/cli/mrcli.js b/cli/mrcli.js index f53ed54..42d3ca3 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -11,17 +11,34 @@ // Import required modules import program from 'commander' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url'; + +// Function to update version from package.json +function updateVersionFromPackageJson() { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const versionNumber = packageJson.version; + + program.version(versionNumber); +} + +// Update the version from package.json +updateVersionFromPackageJson() program .name('mrcli') - .version('0.7.2') - .description('mediumroast.io command line interface') + .description('Mediumroast for GitHub command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') .command('company', 'manage and report on mediumroast.io company objects').alias('c') .command('study', 'manage and report on mediumroast.io study objects').alias('s') .command('user', 'report on mediumroast.io users in GitHub').alias('u') - .command('billing', 'report on GitHub actions and storage units consumed').alias('b') + .command('billing', 'DEPRECATED - report on GitHub actions and storage units consumed') + .command('storage', 'report on GitHub storage units consumed').alias('t') .command('actions', 'report on and update GitHub actions').alias('a') program.parse(process.argv) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b3d27d9..ef6d69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediumroast_js", - "version": "0.7.1", + "version": "0.7.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mediumroast_js", - "version": "0.7.1", + "version": "0.7.15", "license": "Apache-2.0", "dependencies": { "@json2csv/plainjs": "^7.0.4", @@ -19,9 +19,8 @@ "commander": "^8.3.0", "configparser": "^0.3.9", "docx": "^7.4.1", - "flatted": "^3.3.1", "inquirer": "^9.1.2", - "mediumroast_js": "^0.3.6", + "mediumroast_js": "^0.7.3", "node-fetch": "^3.2.10", "octokit": "^3.1.2", "open": "^9.1.0", @@ -32,6 +31,7 @@ }, "bin": { "mrcli": "cli/mrcli.js", + "mrcli-actions": "cli/mrcli-actions.js", "mrcli-billing": "cli/mrcli-billing.js", "mrcli-company": "cli/mrcli-company.js", "mrcli-interaction": "cli/mrcli-interaction.js", @@ -2391,17 +2391,23 @@ "node_modules/async": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==" + "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==", + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2410,9 +2416,11 @@ } }, "node_modules/aws-sdk": { - "version": "2.1522.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1522.0.tgz", - "integrity": "sha512-jQ3a7IiJm2g7ko5q7a/PzyFhdSDDTR2j5sv37hU+lBprHYq5xL+JTS5aO7yJDpZKo9syUaykmUkmgBD0qVSp5A==", + "version": "2.1674.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1674.0.tgz", + "integrity": "sha512-VTijN8+pKrf4sfM2t+ISXjypJ+k3AiP6OMzyLoWJ7jfMBtBfWbQc1rN07OndNb0CZRBBukOHoBhYDPuyae+/1Q==", + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -2423,7 +2431,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.5.0" + "xml2js": "0.6.2" }, "engines": { "node": ">= 10.0.0" @@ -2748,6 +2756,7 @@ "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", @@ -2812,12 +2821,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2973,6 +2989,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" @@ -2988,6 +3005,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -2998,12 +3016,14 @@ "node_modules/cli-truncate/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/cli-truncate/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3017,9 +3037,10 @@ } }, "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3140,6 +3161,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3335,6 +3357,23 @@ "clone": "^1.0.2" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -3350,6 +3389,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -3365,6 +3405,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -3531,56 +3572,25 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-abstract": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", - "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/escalade": { @@ -3651,6 +3661,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", "engines": { "node": ">=0.4.x" } @@ -3794,15 +3805,16 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -3816,6 +3828,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "license": "MIT", "dependencies": { "is-callable": "^1.1.3" } @@ -3837,6 +3850,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3886,31 +3900,10 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3934,13 +3927,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3957,21 +3956,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4001,10 +3985,23 @@ "node": ">=4" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4012,14 +4009,6 @@ "node": ">= 0.4.0" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4029,11 +4018,24 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4051,11 +4053,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4070,6 +4073,18 @@ "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", "dev": true }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -4119,7 +4134,8 @@ "node_modules/ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" }, "node_modules/immediate": { "version": "3.0.6", @@ -4230,49 +4246,11 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4306,9 +4284,10 @@ } }, "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4328,20 +4307,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -4378,6 +4343,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4422,31 +4388,6 @@ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -4474,6 +4415,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4485,17 +4427,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -4507,44 +4438,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", - "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4564,21 +4464,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -4654,6 +4544,7 @@ "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", "engines": { "node": ">= 0.6.0" } @@ -4981,9 +4872,58 @@ } }, "node_modules/mediumroast_js": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/mediumroast_js/-/mediumroast_js-0.7.3.tgz", + "integrity": "sha512-g0lr55ABGWPARlUrVCIucpo2MPtM70T7pgS3yBYIboxOTHzxmChl7IJTdYxU9UeRB5fikx4mBim44sCTHq83xw==", + "license": "Apache-2.0", + "dependencies": { + "@json2csv/plainjs": "^7.0.4", + "adm-zip": "^0.5.9", + "asciiart-logo": "^0.2.7", + "axios": "^1.4.0", + "box-plot": "^1.0.0", + "cli-progress": "^3.11.2", + "cli-table": "^0.3.11", + "commander": "^8.3.0", + "configparser": "^0.3.9", + "docx": "^7.4.1", + "flatted": "^3.3.1", + "inquirer": "^9.1.2", + "mediumroast_js": "^0.3.6", + "node-fetch": "^3.2.10", + "octokit": "^3.1.2", + "open": "^9.1.0", + "ora": "^6.1.2", + "uninstall": "^0.0.0", + "wrap-ansi": "^8.1.0", + "xlsx": "^0.18.5" + }, + "bin": { + "mrcli": "cli/mrcli.js", + "mrcli-billing": "cli/mrcli-billing.js", + "mrcli-company": "cli/mrcli-company.js", + "mrcli-interaction": "cli/mrcli-interaction.js", + "mrcli-setup": "cli/mrcli-setup.js", + "mrcli-study": "cli/mrcli-study.js", + "mrcli-user": "cli/mrcli-user.js" + } + }, + "node_modules/mediumroast_js/node_modules/axios": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/mediumroast_js/node_modules/mediumroast_js": { "version": "0.3.27", "resolved": "https://registry.npmjs.org/mediumroast_js/-/mediumroast_js-0.3.27.tgz", "integrity": "sha512-WO1xx/6jKOnxE6aYHwyXGTdzVohexPrz4hODSkC3x/wqphvjIP09RRt/WSRJnha/uS1YV0t7k7t1kzMd7Q4WkQ==", + "license": "Apache-2.0", "dependencies": { "@json2csv/plainjs": "^7.0.3", "adm-zip": "^0.5.9", @@ -5019,16 +4959,6 @@ "mrcli-study": "cli/mrcli-study.js" } }, - "node_modules/mediumroast_js/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5038,6 +4968,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5046,6 +4977,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -5217,6 +5149,7 @@ "resolved": "https://registry.npmjs.org/node-macaddress/-/node-macaddress-0.2.4.tgz", "integrity": "sha512-bATzyygsv634+tFeqUs3FHnxeXBkJ2PEocpU2Jnx2ORDNZ5GXfTwDdchGyzWIc7CC8BjCWtEjR75EfPp0CPgVQ==", "deprecated": "'node-macaddress' was renamed to just 'macaddress', please use that one.", + "license": "MIT", "dependencies": { "async": "^0.9.0" } @@ -5272,18 +5205,11 @@ "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -5292,6 +5218,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -5394,6 +5321,7 @@ "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", "deprecated": "The package has been renamed to `open`", + "license": "MIT", "dependencies": { "is-wsl": "^1.1.0" }, @@ -5547,6 +5475,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "7.0.39", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", @@ -5597,6 +5534,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", "integrity": "sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw==", + "license": "MIT", "dependencies": { "strip-ansi": "^5.0.0" } @@ -5605,6 +5543,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5613,6 +5552,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" }, @@ -5634,7 +5574,8 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/pseudomap": { "version": "1.0.2", @@ -5771,7 +5712,8 @@ "node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" }, "node_modules/querystring": { "version": "0.2.0", @@ -5946,22 +5888,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/regexpu-core": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.1.0.tgz", @@ -6346,7 +6272,8 @@ "node_modules/sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" }, "node_modules/scheduler": { "version": "0.20.2", @@ -6368,6 +6295,23 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -6392,19 +6336,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6414,6 +6345,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -6429,6 +6361,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -6440,6 +6373,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -6494,32 +6428,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6569,6 +6477,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" @@ -6581,6 +6490,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -6589,6 +6499,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6612,6 +6523,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz", "integrity": "sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==", + "license": "MIT", "dependencies": { "ansi-escapes": "^5.0.0", "supports-hyperlinks": "^2.2.0" @@ -6830,20 +6742,6 @@ "dev": true, "optional": true }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/underscore": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", @@ -6951,21 +6849,22 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "node_modules/util": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", - "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", - "safe-buffer": "^5.1.2", "which-typed-array": "^1.1.2" } }, @@ -6978,6 +6877,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -7108,32 +7008,17 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", - "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7318,9 +7203,10 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -7333,6 +7219,7 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", "engines": { "node": ">=4.0" } @@ -9089,14 +8976,17 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "requires": { + "possible-typed-array-names": "^1.0.0" + } }, "aws-sdk": { - "version": "2.1522.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1522.0.tgz", - "integrity": "sha512-jQ3a7IiJm2g7ko5q7a/PzyFhdSDDTR2j5sv37hU+lBprHYq5xL+JTS5aO7yJDpZKo9syUaykmUkmgBD0qVSp5A==", + "version": "2.1674.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1674.0.tgz", + "integrity": "sha512-VTijN8+pKrf4sfM2t+ISXjypJ+k3AiP6OMzyLoWJ7jfMBtBfWbQc1rN07OndNb0CZRBBukOHoBhYDPuyae+/1Q==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -9107,7 +8997,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.5.0" + "xml2js": "0.6.2" } }, "axios": { @@ -9385,12 +9275,15 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "camelcase": { @@ -9516,9 +9409,9 @@ } }, "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "requires": { "ansi-regex": "^6.0.1" } @@ -9755,6 +9648,16 @@ "clone": "^1.0.2" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -9764,6 +9667,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -9901,46 +9805,19 @@ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true }, - "es-abstract": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", - "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "get-intrinsic": "^1.2.4" } }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -10078,9 +9955,9 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", @@ -10137,25 +10014,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "gensync": { "version": "1.0.0-beta.2", @@ -10170,13 +10031,15 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-stream": { @@ -10184,15 +10047,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -10213,43 +10067,52 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" } }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" } }, "hash-sum": { @@ -10258,6 +10121,14 @@ "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", "dev": true }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -10387,16 +10258,6 @@ } } }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -10406,23 +10267,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -10439,9 +10283,9 @@ } }, "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { "version": "2.10.0", @@ -10452,14 +10296,6 @@ "has": "^1.0.3" } }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -10507,19 +10343,6 @@ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -10544,50 +10367,23 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } - }, "is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==" }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, "is-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", - "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.14" } }, "is-unicode-supported": { @@ -10595,14 +10391,6 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==" }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "requires": { - "call-bind": "^1.0.2" - } - }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", @@ -10931,45 +10719,72 @@ } }, "mediumroast_js": { - "version": "0.3.27", - "resolved": "https://registry.npmjs.org/mediumroast_js/-/mediumroast_js-0.3.27.tgz", - "integrity": "sha512-WO1xx/6jKOnxE6aYHwyXGTdzVohexPrz4hODSkC3x/wqphvjIP09RRt/WSRJnha/uS1YV0t7k7t1kzMd7Q4WkQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/mediumroast_js/-/mediumroast_js-0.7.3.tgz", + "integrity": "sha512-g0lr55ABGWPARlUrVCIucpo2MPtM70T7pgS3yBYIboxOTHzxmChl7IJTdYxU9UeRB5fikx4mBim44sCTHq83xw==", "requires": { - "@json2csv/plainjs": "^7.0.3", + "@json2csv/plainjs": "^7.0.4", "adm-zip": "^0.5.9", "asciiart-logo": "^0.2.7", - "aws-sdk": "^2.1096.0", "axios": "^1.4.0", "box-plot": "^1.0.0", "cli-progress": "^3.11.2", "cli-table": "^0.3.11", - "cli-truncate": "^3.1.0", "commander": "^8.3.0", "configparser": "^0.3.9", "docx": "^7.4.1", + "flatted": "^3.3.1", "inquirer": "^9.1.2", "mediumroast_js": "^0.3.6", "node-fetch": "^3.2.10", - "node-geocoder": "^4.2.0", - "node-macaddress": "^0.2.4", + "octokit": "^3.1.2", "open": "^9.1.0", - "opn": "^6.0.0", "ora": "^6.1.2", - "prompt-sync": "^4.2.0", - "terminal-link": "^3.0.0", + "uninstall": "^0.0.0", "wrap-ansi": "^8.1.0", "xlsx": "^0.18.5" }, "dependencies": { "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } + }, + "mediumroast_js": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/mediumroast_js/-/mediumroast_js-0.3.27.tgz", + "integrity": "sha512-WO1xx/6jKOnxE6aYHwyXGTdzVohexPrz4hODSkC3x/wqphvjIP09RRt/WSRJnha/uS1YV0t7k7t1kzMd7Q4WkQ==", + "requires": { + "@json2csv/plainjs": "^7.0.3", + "adm-zip": "^0.5.9", + "asciiart-logo": "^0.2.7", + "aws-sdk": "^2.1096.0", + "axios": "^1.4.0", + "box-plot": "^1.0.0", + "cli-progress": "^3.11.2", + "cli-table": "^0.3.11", + "cli-truncate": "^3.1.0", + "commander": "^8.3.0", + "configparser": "^0.3.9", + "docx": "^7.4.1", + "inquirer": "^9.1.2", + "mediumroast_js": "^0.3.6", + "node-fetch": "^3.2.10", + "node-geocoder": "^4.2.0", + "node-macaddress": "^0.2.4", + "open": "^9.1.0", + "opn": "^6.0.0", + "ora": "^6.1.2", + "prompt-sync": "^4.2.0", + "terminal-link": "^3.0.0", + "wrap-ansi": "^8.1.0", + "xlsx": "^0.18.5" + } } } }, @@ -11134,20 +10949,17 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -11322,6 +11134,11 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" + }, "postcss": { "version": "7.0.39", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", @@ -11680,16 +11497,6 @@ "@babel/runtime": "^7.8.4" } }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, "regexpu-core": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.1.0.tgz", @@ -12008,6 +11815,19 @@ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -12026,16 +11846,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -12100,26 +11910,6 @@ "strip-ansi": "^6.0.1" } }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -12356,17 +12146,6 @@ "dev": true, "optional": true }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, "underscore": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", @@ -12445,15 +12224,14 @@ } }, "util": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", - "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "requires": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", - "safe-buffer": "^5.1.2", "which-typed-array": "^1.1.2" } }, @@ -12570,29 +12348,16 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, "which-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", - "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" } }, "window-size": { @@ -12720,9 +12485,9 @@ } }, "xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" diff --git a/package.json b/package.json index 741fe51..6aa5b59 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediumroast_js", - "version": "0.7.2", - "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", + "version": "0.7.34", + "description": "A Command Line Interface (CLI) and Javascript SDK to interact with Mediumroast for GitHub.", "main": "cli/mrcli.js", "scripts": { "docs": "jsdoc -u ./cli -R ./README.md -d ./docs -c ./docs_config.json -P ./package.json ./src/api" @@ -13,6 +13,7 @@ "mrcli-setup": "cli/mrcli-setup.js", "mrcli-user": "cli/mrcli-user.js", "mrcli-billing": "cli/mrcli-billing.js", + "mrcli-actions": "cli/mrcli-actions.js", "mrcli": "cli/mrcli.js" }, "type": "module", @@ -70,9 +71,8 @@ "commander": "^8.3.0", "configparser": "^0.3.9", "docx": "^7.4.1", - "flatted": "^3.3.1", "inquirer": "^9.1.2", - "mediumroast_js": "^0.3.6", + "mediumroast_js": "^0.7.3", "node-fetch": "^3.2.10", "octokit": "^3.1.2", "open": "^9.1.0", diff --git a/src/api/authorize.js b/src/api/authorize.js index 5856908..65daa38 100644 --- a/src/api/authorize.js +++ b/src/api/authorize.js @@ -219,8 +219,48 @@ class Auth0Auth { } class GitHubAuth { + constructor (env) { + this.env = env + } + + async _checkTokenExpiration(token) { + const response = await fetch('https://api.github.com/applications/:client_id/token', { + method: 'POST', + headers: { + 'Authorization': `Basic ${Buffer.from(`client_id:client_secret`).toString('base64')}`, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github.v3+json' + }, + body: JSON.stringify({ + access_token: token + }) + }) + + if (!response.ok) { + return [false, {status_cde: 500, status_msg: response.statusText}, null] + } + const data = await response.json() + return [true, {status_cde: 200, status_msg: response.statusText}, data] + } + getAccessTokenPat(defaultExpiryDays = 30) { + // Read the PAT from the file + const pat = fs.readFileSync(this.secretFile, 'utf8').trim() + + // Check to see if the token remains valid + const isTokenValid = this._checkTokenExpiration(pat) + + if (!isTokenValid[0]) { + return isTokenValid + } + + return { + token: pat, + auth_type: 'pat' + } + } + /** * diff --git a/src/api/gitHubServer.js b/src/api/gitHubServer.js index 419867d..f49dc9a 100644 --- a/src/api/gitHubServer.js +++ b/src/api/gitHubServer.js @@ -33,6 +33,9 @@ // Import required modules import GitHubFunctions from './github.js' import { createHash } from 'crypto' +import fs from 'fs' +import * as path from 'path' +import { fileURLToPath } from 'url' class baseObjects { @@ -285,7 +288,7 @@ class Users extends baseObjects { } // Create a subclass called Users that inherits from baseObjects -class Billings extends baseObjects { +class Storage extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the user objects @@ -299,31 +302,7 @@ class Billings extends baseObjects { // Create a new method for getAll that is specific to the Billings class using getBillings() in github.js async getAll() { - const storageBillingsResp = await this.serverCtl.getStorageBillings() - const actionsBillingsResp = await this.serverCtl.getActionsBillings() - const allBillings = [ - { - resourceType: 'Storage', - includedUnits: Math.abs( - storageBillingsResp[2].estimated_paid_storage_for_month - - storageBillingsResp[2].estimated_storage_for_month - ) + ' GiB', - paidUnitsUsed: storageBillingsResp[2].estimated_paid_storage_for_month + ' GiB', - totalUnitsUsed: storageBillingsResp[2].estimated_storage_for_month + ' GiB' - }, - { - resourceType: 'Actions', - includedUnits: actionsBillingsResp[2].total_minutes_used + ' min', - paidUnitsUsed: actionsBillingsResp[2].total_paid_minutes_used + ' min', - totalUnitsUsed: actionsBillingsResp[2].total_minutes_used + actionsBillingsResp[2].total_paid_minutes_used + ' min' - } - ] - return [true, {status_code: 200, status_msg: `found all billings`}, allBillings] - } - - // Create a new method of to get the actions billing status only - async getActionsBilling() { - return await this.serverCtl.getActionsBillings() + return await this.serverCtl.getRepoSize() } // Create a new method of to get the storage billing status only @@ -483,46 +462,111 @@ class Actions extends baseObjects { * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { - super(token, org, processName, 'Interactions') + super(token, org, processName, 'Actions') } - async updateObj(objToUpdate, dontWrite=false, system=false) { - // Destructure objToUpdate - const { name, key, value } = objToUpdate - // Define the attributes that can be updated by the user - const whiteList = [ - 'status', 'content_type', 'file_size', 'reading_time', 'word_count', 'page_count', 'description', 'abstract', + _generateManifest(dir, filelist) { + // Define which content to skip + const skipContent = ['.DS_Store', 'node_modules'] + const __filename = fileURLToPath(import.meta.url) + const __dirname = path.dirname(__filename); + // Use regex to prune everything after mediumroast_js/ + const basePath = __dirname.match(/.*mediumroast_js\//)[0]; + // Append cli/actions to the base path + dir = dir || path.resolve(path.join(basePath, 'cli/actions')) + + + const files = fs.readdirSync(dir) + filelist = filelist || []; + files.forEach((file) => { + // Skip unneeded directories + if (skipContent.includes(file)) { + return + } + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + filelist = this._generateManifest(path.join(dir, file), filelist) + } else { + // Substitute .github for the first part of the path, in the variable dir + if (dir.includes('./')) { + dir = dir.replace('./', '') + } + // This will be the repository name + let dotGitHub = dir.replace(/.*(workflows|actions)/, '.github/$1') + + filelist.push({ + fileName: file, + containerName: dotGitHub, + srcURL: new URL(`file://${fullPath}`) + }) + + } + }) + return filelist + } + + async updateActions() { + // Discover the manifest + const actionsManifest = this._generateManifest() + // Capture detailed install status + let installStatus = { + successCount: 0, + failCount: 0, + success: [], + fail: [], + total: actionsManifest.length + } + for (const action of actionsManifest) { + // Loop through the actionsManifest and install each action + let status = false + let blobData + try { + // Read in the blob file + blobData = fs.readFileSync(action.srcURL, 'base64') + status = true + } catch (err) { + console.log(`Unable to read file [${action.fileName}] because: ${err}`) + return [false,{status_code: 500, status_msg: `Unable to read file [${action.fileName}] because: ${err}`}, installStatus] + } + if(status) { + // Get the sha for the current branch/object + const sha = await this.serverCtl.getSha( + action.containerName, + action.fileName, + 'main' + ) + // Keep action update failures + // Install the action + const installResp = await this.serverCtl.writeBlob( + action.containerName, + action.fileName, + blobData, + 'main', + sha[2] + ) + if(installResp[0]){ + installStatus.success.push({fileName: action.fileName, containerName: action.catchContainer, installMsg: installResp[1].status_msg}) + installStatus.successCount++ + } else { + installStatus.fail.push({fileName: action.fileName, containerName: action.catchContainer, installMsg: installResp[1].status_msg}) + installStatus.failCount++ + } + } else { + return [false, {status_code: 503,status_msg:`Failed to read item [${action.fileName}]`}, installStatus] + } + } + return [true, {status_code: 200, status_msg:`All actions installed`}, installStatus] + } - 'region', 'country', 'city', 'state_province', 'zip_postal', 'street_address', 'latitude', 'longitude', - - 'public', 'groups' - ] - return await super.updateObj(name, key, value, dontWrite, system, whiteList) + // Create a new method of to get the actions billing status only + async getActionsBilling() { + return await this.serverCtl.getActionsBillings() } async getAll() { - const storageBillingsResp = await this.serverCtl.getStorageBillings() - const actionsBillingsResp = await this.serverCtl.getActionsBillings() - const allBillings = [ - { - resourceType: 'Storage', - includedUnits: Math.abs( - storageBillingsResp[2].estimated_paid_storage_for_month - - storageBillingsResp[2].estimated_storage_for_month - ) + ' GiB', - paidUnitsUsed: storageBillingsResp[2].estimated_paid_storage_for_month + ' GiB', - totalUnitsUsed: storageBillingsResp[2].estimated_storage_for_month + ' GiB' - }, - { - resourceType: 'Actions', - includedUnits: actionsBillingsResp[2].total_minutes_used + ' min', - paidUnitsUsed: actionsBillingsResp[2].total_paid_minutes_used + ' min', - totalUnitsUsed: actionsBillingsResp[2].total_minutes_used + actionsBillingsResp[2].total_paid_minutes_used + ' min' - } - ] - return [true, {status_code: 200, status_msg: `found all billings`}, allBillings] + return await this.serverCtl.getWorkflowRuns() } } // Export classes for consumers -export { Studies, Companies, Interactions, Users, Billings } \ No newline at end of file +export { Studies, Companies, Interactions, Users, Storage, Actions } \ No newline at end of file diff --git a/src/api/github.js b/src/api/github.js index ba50330..e093c0e 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -183,6 +183,120 @@ class GitHubFunctions { } } + /** + * @async + * @function getWorkflowRuns + * @description Gets all of the workflow runs for the repository + * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the response or error message. + */ + async getWorkflowRuns () { + let workflows + try { + workflows = await this.octCtl.rest.actions.listWorkflowRunsForRepo({ + owner: this.orgName, + repo: this.repoName + }) + } catch (err) { + return [false, {status_code: 500, status_msg: err.message}, err] + } + + const workflowList = [] + let totalRunTimeThisMonth = 0 + for (const workflow of workflows.data.workflow_runs) { + // Get the current month + const currentMonth = new Date().getMonth() + + // Compute the runtime and if the time is less than 60s round it to 1m + const runTime = Math.ceil((new Date(workflow.updated_at) - new Date(workflow.created_at)) / 1000 / 60) < 1 ? 1 : Math.ceil((new Date(workflow.updated_at) - new Date(workflow.created_at)) / 1000 / 60) + + // If the month of the workflow is not the current month, then skip it + if (new Date(workflow.updated_at).getMonth() !== currentMonth) { + continue + } + totalRunTimeThisMonth += runTime + + // Add the workflow to the workflowList + workflowList.push({ + // Create name where the path is the name of the workflow, but remove the path and the .yml extension + name: workflow.path.replace('.github/workflows/', '').replace('.yml', ''), + title: workflow.display_title, + id: workflow.id, + workflowId: workflow.workflow_id, + runTimeMinutes: runTime, + status: workflow.status, + conclusion: workflow.conclusion, + event: workflow.event, + path: workflow.path, + }) + } + + // Sort the worflowList to put the most recent workflows first + workflowList.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) + + return [true, { + status_code: 200, + status_msg: `discovered [${workflowList.length}] workflow runs for [${this.repoName}]`,}, + workflowList + ] + } + + // Create a method using the octokit to get the size of the repository and return the size in MB + async getRepoSize() { + let repoData = { + size: 0, + numFiles: 0, + name: this.repoName, + org: this.orgName, + } + // Count the number of files in the repository + const countFiles = async (path = '') => { + try { + const response = await this.octCtl.rest.repos.getContent({ + owner: this.orgName, + repo: this.repoName, + path: path + }) + + let fileCount = 0; + for (const item of response.data) { + if (item.type === 'file') { + fileCount += 1; + } else if (item.type === 'dir') { + fileCount += await countFiles(item.path) + } + } + return fileCount + } catch (err) { + return 0 + } + } + try { + repoData.numFiles = await countFiles() + } catch (err) { + repoData.numFiles = 'Unknown' + } + + const getRepoSize = async () => { + try { + const response = await this.octCtl.rest.repos.get({ + owner: this.orgName, + repo: this.repoName + }) + const sizeInKB = response.data.size; + const sizeInMB = sizeInKB / 1024; + return sizeInMB.toFixed(2); // Convert to MB and format to 2 decimal places + } catch (err) { + return 0 + } + } + try { + repoData.size = await getRepoSize() + } catch (err) { + repoData.size = 'Unknown' + } + return [true, {status_code: 200, status_msg: `discovered size of [${this.repoName}]`}, repoData] + } + /** * @function createContainers * @description Creates the top level Study, Company and Interaction containers for all mediumroast.io assets @@ -904,68 +1018,6 @@ class GitHubFunctions { return [true, {status_code: 200, status_msg: `Released [${repoMetadata.containers.length}] containers.`}, null] } - // Use fs to read all the files in the actions directory recursively - generateActionsManifest(dir, filelist) { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename) - dir = dir || path.resolve(path.join(__dirname, './actions') ) - const files = fs.readdirSync(dir) - filelist = filelist || [] - files.forEach((file) => { - // Skip .DS_Store files and node_modules directories - if (file === '.DS_Store' || file === 'node_modules') { - return - } - if (fs.statSync(path.join(dir, file)).isDirectory()) { - filelist = generateActionsManifest(path.join(dir, file), filelist) - } - else { - // Substitute .github for the first part of the path, in the variable dir - // Log dir to the console including if there are any special characters - if (dir.includes('./')) { - dir = dir.replace('./', '') - } - // This will be the repository name - let dotGitHub = dir.replace(/.*(workflows|actions)/, '.github/$1') - - filelist.push({ - fileName: file, - containerName: dotGitHub, - srcURL: new URL(path.join(dir, file), import.meta.url) - }) - } - }) - return filelist - } - - async installActions() { - let actionsManifest = this.generateActionsManifest() - // Loop through the actionsManifest and install each action - await actionsManifest.forEach(async (action) => { - let status = false - let blobData - try { - // Read in the blob file - blobData = fs.readFileSync(action.srcURL, 'base64') - status = true - } catch (err) { - return [false, 'Unable to read file [' + action.fileName + '] because: ' + err, null] - } - if(status) { - // Install the action - const installResp = await this.writeBlob( - action.containerName, - action.fileName, - blobData, - 'main' - ) - } else { - return [false, 'Failed to read item [' + action.fileName + ']', null] - } - }) - return [true, 'All actions installed', null] - } - } export default GitHubFunctions \ No newline at end of file diff --git a/src/cli/output.js b/src/cli/output.js index a19391d..a56d721 100644 --- a/src/cli/output.js +++ b/src/cli/output.js @@ -157,16 +157,29 @@ class CLIOutput { objects[myObj].days_left_in_billing_cycle + ' days', ]) } - } else if (this.objectType === 'AllBilling') { + } else if (this.objectType === 'Workflows') { table = new Table({ - head: ['Resource Type', 'Included Units Used', 'Paid Units Used', 'Total Units Used'], + head: ['Name', 'Id', 'Status', 'Trigger', 'Runtime (min)'], }) - for (const myObj in objects) { + for (const myObj in objects.slice(-5)) { + table.push([ + objects[myObj].name, + objects[myObj].workflowId, + objects[myObj].conclusion, + objects[myObj].event, + objects[myObj].runTimeMinutes + ' min' + ]) + } + } else if (this.objectType === 'Storage') { + table = new Table({ + head: ['Reposistory', 'Organization', 'File Count', 'Size (MB)'], + }) + for (const myObj in objects.slice(-5)) { table.push([ - objects[myObj].resourceType, - objects[myObj].includedUnits, - objects[myObj].paidUnitsUsed, - objects[myObj].totalUnitsUsed, + objects[myObj].name, + objects[myObj].org, + objects[myObj].numFiles, + objects[myObj].size + ' MB', ]) } } else { From fdc1479257889f5d92207824f93e9dcd8f5617cd Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 18 Aug 2024 13:25:54 -0700 Subject: [PATCH 13/23] Add manual execution and needed perms for prune action --- cli/actions/workflows/prune-branches.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/actions/workflows/prune-branches.yml b/cli/actions/workflows/prune-branches.yml index e993545..2eff0bd 100644 --- a/cli/actions/workflows/prune-branches.yml +++ b/cli/actions/workflows/prune-branches.yml @@ -3,9 +3,12 @@ run-name: ${{ github.actor }} executed prune-branches action on: schedule: - cron: '0 0 * * *' # Runs at midnight every day + workflow_dispatch: # Allows manual execution jobs: prune-branches: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 From cc4e5e63620e54cbd1c06c15291fca917451ed7c Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sat, 24 Aug 2024 11:23:09 -0700 Subject: [PATCH 14/23] Fix inconsistency between chart and most/least similar, cleanups --- cli/mrcli-company.js | 98 +--- package.json | 3 +- src/report/charts.js | 6 +- src/report/common.js | 910 ----------------------------------- src/report/companies.js | 96 ++-- src/report/dashboard.js | 307 +----------- src/report/helpers.js | 144 ++++++ src/report/interactions.js | 86 ++-- src/report/settings.js | 15 +- src/report/tools.js | 74 --- src/report/widgets/Tables.js | 42 +- src/report/widgets/Text.js | 31 +- 12 files changed, 361 insertions(+), 1451 deletions(-) delete mode 100644 src/report/common.js create mode 100644 src/report/helpers.js delete mode 100644 src/report/tools.js diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 869b9ac..5e6f0a0 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -4,14 +4,14 @@ * A CLI utility used for accessing and reporting on mediumroast.io company objects * @author Michael Hay * @file company.js - * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 2.2.0 */ // Import required modules import { CompanyStandalone } from '../src/report/companies.js' import { Interactions, Companies, Studies, Users } from '../src/api/gitHubServer.js' +import DOCXUtilities from '../src/report/helpers.js' import GitHubFunctions from '../src/api/github.js' import AddCompany from '../src/cli/companyWizard.js' import Environmentals from '../src/cli/env.js' @@ -27,7 +27,7 @@ const objectType = 'Companies' // Environmentals object const environment = new Environmentals( - '3.1.0', + '3.2.0', `${objectType}`, `Command line interface for mediumroast.io ${objectType} objects.`, objectType @@ -36,6 +36,7 @@ const environment = new Environmentals( // Filesystem object const fileSystem = new FilesystemOperators() + // Process the command line options let myProgram = environment.parseCLIArgs(true) myProgram @@ -52,6 +53,9 @@ myEnv.company = 'Unknown' const accessToken = await environment.verifyAccessToken() const processName = 'mrcli-company' +// Construct the DOCXUtilities object +const docxUtils = new DOCXUtilities(myEnv) + // Output object const output = new CLIOutput(myEnv, objectType) @@ -65,91 +69,12 @@ const gitHubCtl = new GitHubFunctions(accessToken, myEnv.gitHubOrg, processName) // const studyCtl = new Studies(accessToken, myEnv.gitHubOrg, processName) const userCtl = new Users(accessToken, myEnv.gitHubOrg, processName) -function initializeSource() { - return { - company: [], - interactions: [], - allInteractions: [], - competitors: { - leastSimilar: {}, - mostSimilar: {}, - all: [] - }, - totalInteractions: 0, - totalCompanies: 0, - averageInteractionsPerCompany: 0, - } -} - async function fetchData() { const [intStatus, intMsg, allInteractions] = await interactionCtl.getAll() const [compStatus, compMsg, allCompanies] = await companyCtl.getAll() return { allInteractions, allCompanies } } -function getSourceCompany(allCompanies, companyName) { - return allCompanies.mrJson.filter(company => company.name === companyName) -} - -function getInteractions(sourceCompany, allInteractions) { - const interactionNames = Object.keys(sourceCompany[0].linked_interactions); - return interactionNames.map(interactionName => - allInteractions.mrJson.find(interaction => interaction.name === interactionName) - ).filter(interaction => interaction !== undefined); -} - -function getCompetitors(sourceCompany, allCompanies) { - const competitorNames = Object.keys(sourceCompany[0].similarity); - const allCompetitors = competitorNames.map(competitorName => - allCompanies.mrJson.find(company => company.name === competitorName) - ).filter(company => company !== undefined) - - const mostSimilar = competitorNames.reduce((mostSimilar, competitorName) => { - const competitor = allCompanies.mrJson.find(company => company.name === competitorName); - if (!competitor) return mostSimilar; - - const similarityScore = sourceCompany[0].similarity[competitorName].similarity; - if (!mostSimilar || similarityScore > mostSimilar.similarity) { - return { ...competitor, similarity: similarityScore } - } - return mostSimilar - }, null) - - const leastSimilar = competitorNames.reduce((leastSimilar, competitorName) => { - const competitor = allCompanies.mrJson.find(company => company.name === competitorName); - if (!competitor) return leastSimilar; - - const similarityScore = sourceCompany[0].similarity[competitorName].similarity; - if (!leastSimilar || similarityScore < leastSimilar.similarity) { - return { ...competitor, similarity: similarityScore } - } - return leastSimilar - }, null); - - return { allCompetitors, mostSimilar, leastSimilar } -} - -async function _prepareData(companyName) { - let source = initializeSource() - - const { allInteractions, allCompanies } = await fetchData() - - source.company = getSourceCompany(allCompanies, companyName) - source.totalCompanies = allCompanies.mrJson.length - - source.interactions = getInteractions(source.company, allInteractions) - source.allInteractions = allInteractions.mrJson - source.totalInteractions = allInteractions.mrJson.length - source.averageInteractionsPerCompany = Math.round(source.totalInteractions / source.totalCompanies) - - const { allCompetitors, mostSimilar, leastSimilar } = getCompetitors(source.company, allCompanies) - source.competitors.all = allCompetitors - source.competitors.mostSimilar = mostSimilar - source.competitors.leastSimilar = leastSimilar - - return source -} - // Predefine the results variable let [success, stat, results] = [null, null, null] @@ -157,16 +82,19 @@ let [success, stat, results] = [null, null, null] // TODO consider moving this out into at least a separate function to make main clean if (myArgs.report) { // Prepare the data for the report - const reportData = await _prepareData(myArgs.report) + // const reportData = await _prepareData(myArgs.report) + const { allInteractions, allCompanies } = await fetchData() // Set the root name to be used for file and directory names in case of packaging - const baseName = reportData.company[0].name.replace(/ /g, "_") + const baseName = myArgs.report.replace(/ /g, "_") // Set the directory name for the package const baseDir = myEnv.workDir + '/' + baseName // Define location and name of the report output, depending upon the package switch this will change let fileName = process.env.HOME + '/Documents/' + baseName + '.docx' // Set up the document controller const docController = new CompanyStandalone( - reportData, + myArgs.report, + allCompanies.mrJson, + allInteractions.mrJson, myEnv ) diff --git a/package.json b/package.json index 6aa5b59..4a332bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.7.34", + "version": "0.7.00.42", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with Mediumroast for GitHub.", "main": "cli/mrcli.js", "scripts": { @@ -13,6 +13,7 @@ "mrcli-setup": "cli/mrcli-setup.js", "mrcli-user": "cli/mrcli-user.js", "mrcli-billing": "cli/mrcli-billing.js", + "mrcli-storage": "cli/mrcli-storage.js", "mrcli-actions": "cli/mrcli-actions.js", "mrcli": "cli/mrcli.js" }, diff --git a/src/report/charts.js b/src/report/charts.js index aeaa140..a1d578b 100755 --- a/src/report/charts.js +++ b/src/report/charts.js @@ -1,11 +1,11 @@ /** - * @fileOverview This file contains the Charting class which is used to generate charts for the report + * @description This file contains the Charting class which is used to generate charts for the report * * @license Apache-2.0 * @version 2.0.0 * * @author Michael Hay - * @file github.js + * @file charts.js * @copyright 2024 Mediumroast, Inc. All rights reserved. * * @module Charting @@ -36,7 +36,7 @@ */ - +// Import required modules import axios from 'axios' import docxSettings from './settings.js' import path from 'path' diff --git a/src/report/common.js b/src/report/common.js deleted file mode 100644 index 85229f3..0000000 --- a/src/report/common.js +++ /dev/null @@ -1,910 +0,0 @@ -/** - * A set of common utilities for creating HTML and DOCX reports for mediumroast.io objects - * @author Michael Hay - * @file common.js - * @copyright 2022 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - */ - -// Import modules -import docx from 'docx' -import * as fs from 'fs' -import boxPlot from 'box-plot' -import docxSettings from './settings.js' -import FilesystemOperators from '../cli/filesystem.js' - - -// TODO Change class names to: GenericUtilities, DOCXUtilities and HTMLUtilities -// TODO rankTags belongs to GenericUtilities - -class DOCXUtilities { - /** - * To make machine authoring of a Microsoft DOCX file consistent and easier this class has been - * developed. Key functions are available that better describe the intent of the operation - * by name which makes it simpler author a document instead of sweating the details. - * Further through trial and error the idiosyncrasies of the imported docx module - * have been worked out so that the developer doesn't have to accidently find out and struggle - * with document generation. - * @constructor - * @classdesc Core utilities for generating elements in a Microsoft word DOCX file - * @param {Object} env - * @todo when we get to HTML report generation for the front end we will rename this class and create a new one for HTML - */ - constructor (env) { - this.env = env - this.generalSettings = docxSettings.general - this.themeSettings = docxSettings[this.env.theme] - this.font = docxSettings.general.font - this.heavyFont = docxSettings.general.heavyFont - this.halfFontSize = docxSettings.general.halfFontSize - this.fullFontSize = docxSettings.general.fullFontSize - this.fontFactor = docxSettings.general.fontFactor - this.theme = this.env.theme - this.documentColor = this.themeSettings.documentColor - this.textFontColor = `#${docxSettings[this.theme].textFontColor.toLowerCase()}` - this.titleFontColor = `#${docxSettings[this.theme].titleFontColor.toLowerCase()}` - this.tableBorderColor = `#${docxSettings[this.theme].tableBorderColor.toLowerCase()}` - this.styling = this.initStyles() - this.fileSystem = new FilesystemOperators() - this.regions = { - AMER: 'Americas', - EMEA: 'Europe, Middle East and Africa', - APAC: 'Asia Pacific and Japan' - } - } - - // Initials the working directories - initDirectories() { - const subdirs = ['interactions', 'images'] - for(const myDir in subdirs) { - this.fileSystem.safeMakedir(this.env.workDir + '/' + subdirs[myDir]) - } - } - - // Initialize the common styles for the docx - initStyles () { - const hangingSpace = 0.18 - return { - default: { - heading1: { - run: { - size: this.fullFontSize, - bold: true, - font: this.font, - color: this.textFontColor - }, - paragraph: { - spacing: { - before: 160, - after: 80, - }, - }, - }, - heading2: { - run: { - size: 0.75 * this.fullFontSize, - bold: true, - font: this.font, - color: this.textFontColor - }, - paragraph: { - spacing: { - before: 240, - after: 120, - }, - }, - }, - heading3: { - run: { - size: 0.8 * this.fullFontSize, - bold: true, - font: this.font, - color: this.textFontColor - }, - paragraph: { - spacing: { - before: 240, - after: 120, - }, - }, - }, - listParagraph: { - run: { - font: this.font, - size: 1.5 * this.halfFontSize, - }, - }, - paragraph: { - font: this.font, - size: this.halfFontSize, - } - }, - paragraphStyles: [ - { - id: "mrNormal", - name: "MediumRoast Normal", - basedOn: "Normal", - next: "Normal", - quickFormat: true, - run: { - font: this.font, - size: this.halfFontSize, - }, - }, - ], - numbering: { - config: [ - { - reference: 'number-styles', - levels: [ - { - level: 0, - format: docx.LevelFormat.DECIMAL, - text: "%1.", - alignment: docx.AlignmentType.START, - style: { - paragraph: { - indent: { - left: docx.convertInchesToTwip(0.25), - hanging: docx.convertInchesToTwip(hangingSpace) - }, - spacing: { - before: 75 - } - }, - }, - }, - { - level: 1, - format: docx.LevelFormat.LOWER_LETTER, - text: "%2.", - alignment: docx.AlignmentType.START, - style: { - paragraph: { - indent: { left: docx.convertInchesToTwip(0.50), hanging: docx.convertInchesToTwip(hangingSpace) }, - }, - }, - }, - { - level: 2, - format: docx.LevelFormat.LOWER_ROMAN, - text: "%3.", - alignment: docx.AlignmentType.START, - style: { - paragraph: { - indent: { left: docx.convertInchesToTwip(0.75), hanging: docx.convertInchesToTwip(hangingSpace) }, - }, - }, - }, - { - level: 3, - format: docx.LevelFormat.UPPER_LETTER, - text: "%4.", - alignment: docx.AlignmentType.START, - style: { - paragraph: { - indent: { left: docx.convertInchesToTwip(1.0), hanging: docx.convertInchesToTwip(hangingSpace) }, - }, - }, - }, - { - level: 4, - format: docx.LevelFormat.UPPER_ROMAN, - text: "%5.", - alignment: docx.AlignmentType.START, - style: { - paragraph: { - indent: { left: docx.convertInchesToTwip(1.25), hanging: docx.convertInchesToTwip(hangingSpace) }, - }, - }, - }, - ] - }, - { - reference: "bullet-styles", - levels: [ - { - level: 0, - format: docx.LevelFormat.BULLET, - text: "-", - alignment: docx.AlignmentType.LEFT, - style: { - paragraph: { - indent: { left: docx.convertInchesToTwip(0.5), hanging: docx.convertInchesToTwip(0.25) }, - }, - }, - }, - { - level: 1, - format: docx.LevelFormat.BULLET, - text: "\u00A5", - alignment: docx.AlignmentType.LEFT, - style: { - paragraph: { - indent: { left: docx.convertInchesToTwip(1), hanging: docx.convertInchesToTwip(0.25) }, - }, - }, - }, - { - level: 2, - format: docx.LevelFormat.BULLET, - text: "\u273F", - alignment: docx.AlignmentType.LEFT, - style: { - paragraph: { - indent: { left: 2160, hanging: docx.convertInchesToTwip(0.25) }, - }, - }, - }, - { - level: 3, - format: docx.LevelFormat.BULLET, - text: "\u267A", - alignment: docx.AlignmentType.LEFT, - style: { - paragraph: { - indent: { left: 2880, hanging: docx.convertInchesToTwip(0.25) }, - }, - }, - }, - { - level: 4, - format: docx.LevelFormat.BULLET, - text: "\u2603", - alignment: docx.AlignmentType.LEFT, - style: { - paragraph: { - indent: { left: 3600, hanging: docx.convertInchesToTwip(0.25) }, - }, - }, - }, - ] - } - ]} - } - } - - /** - * @function makeBullet - * @description Create a bullet for a bit of prose - * @param {String} text - text/prose for the bullet - * @param {Integer} level - the level of nesting for the bullet - * @returns {Object} new docx paragraph object as a bullet - */ - makeBullet(text, level=0) { - return new docx.Paragraph({ - text: text, - numbering: { - reference: 'bullet-styles', - level: level - } - }) - } - - /** - * @function makeHeader - * @description Generate a header with an item's name and the document type fields - * @param {String} itemName - * @param {String} documentType - * @param {Object} options - */ - makeHeader(itemName, documentType, options={}) { - const { - landscape = false, - fontColor = this.textFontColor, - } = options - let separator = "\t".repeat(3) - if (landscape) { separator = "\t".repeat(4)} - return new docx.Header({ - children: [ - new docx.Paragraph({ - alignment: docx.AlignmentType.CENTER, - children: [ - new docx.TextRun({ - children: [documentType], - font: this.font, - size: this.generalSettings.headerFontSize, - color: fontColor ? fontColor : this.textFontColor - }), - new docx.TextRun({ - children: [itemName], - font: this.font, - size: this.generalSettings.headerFontSize, - color: fontColor ? fontColor : this.textFontColor - }) - ], - }), - ], - }) - } - - makeFooter(documentAuthor, datePrepared, options={}) { - const { - landscape = false, - fontColor = this.textFontColor, - } = options - let separator = "\t" - if (landscape) { separator = "\t".repeat(2)} - return new docx.Paragraph({ - alignment: docx.AlignmentType.CENTER, - children: [ - new docx.TextRun({ - children: ['Page ', docx.PageNumber.CURRENT, ' of ', docx.PageNumber.TOTAL_PAGES, separator], - font: this.font, - size: this.generalSettings.footerFontSize, - color: fontColor ? fontColor : this.textFontColor - }), - new docx.TextRun({ - children: ['|', separator, documentAuthor, separator], - font: this.font, - size: this.generalSettings.footerFontSize, - color: fontColor ? fontColor : this.textFontColor - }), - new docx.TextRun({ - children: ['|', separator, datePrepared], - font: this.font, - size: this.generalSettings.footerFontSize, - color: fontColor ? fontColor : this.textFontColor - }) - ] - }) - } - - /** - * @function makeParagraph - * @description For a section of prose create a paragraph - * @param {String} paragraph - text/prose for the paragraph - * @param {Object} objects - an object that contains the font size, color, and other styling options - * @returns {Object} a docx paragraph object - */ - makeParagraph (paragraph,options={}) { - const { - fontSize, - bold=false, - fontColor, - font='Avenir Next', - center=false, - italics=false, - underline=false, - spaceAfter=0, - } = options - // const fontSize = 2 * this.fullFontSize // Font size is measured in half points, multiply by to is needed - return new docx.Paragraph({ - alignment: center ? docx.AlignmentType.CENTER : docx.AlignmentType.LEFT, - children: [ - new docx.TextRun({ - text: paragraph, - font: font ? font : this.font, - size: fontSize ? fontSize : this.fullFontSize, // Default font size size 10pt or 2 * 10 = 20 - bold: bold ? bold : false, // Bold is off by default - italics: italics ? italics : false, // Italics off by default - underline: underline ? underline : false, // Underline off by default - break: spaceAfter ? spaceAfter : 0, // Defaults to no trailing space - color: fontColor ? fontColor : this.textFontColor - }) - ] - }) - } - - - - /** - * @function pageBreak - * @description Create a page break - * @returns {Object} a docx paragraph object with a PageBreak - */ - pageBreak() { - return new docx.Paragraph({ - children: [ - new docx.PageBreak() - ] - }) - } - - /** - * @function makeHeading1 - * @description Create a text of heading style 1 - * @param {String} text - text/prose for the function - * @returns {Object} a new paragraph as a heading - */ - makeHeading1(text) { - return new docx.Paragraph({ - text: text, - heading: docx.HeadingLevel.HEADING_1 - }) - } - - /** - * @function makeHeading2 - * @description Create a text of heading style 2 - * @param {String} text - text/prose for the function - * @returns {Object} a new paragraph as a heading - */ - makeHeading2(text) { - return new docx.Paragraph({ - text: text, - heading: docx.HeadingLevel.HEADING_2 - }) - } - - - /** - * @function makeHeading3 - * @description Create a text of heading style 3 - * @param {String} text - text/prose for the function - * @returns {Object} a new paragraph as a heading - */ - makeHeading3(text) { - return new docx.Paragraph({ - text: text, - heading: docx.HeadingLevel.HEADING_3 - }) - } - - /** - * @function makeExternalHyperLink - * @description Create an external hyperlink - * @param {String} text - text/prose for the function - * @param {String} link - the URL for the hyperlink - * @returns {Object} a new docx ExternalHyperlink object - */ - makeExternalHyperLink(text, link) { - return new docx.ExternalHyperlink({ - children: [ - new docx.TextRun({ - text: text, - style: 'Hyperlink', - font: this.font, - size: 16 - }) - ], - link: link - }) - } - - /** - * @function makeInternalHyperLink - * @description Create an external hyperlink - * @param {String} text - text/prose for the function - * @param {String} link - the URL for the hyperlink within the document - * @returns {Object} a new docx InternalHyperlink object - */ - makeInternalHyperLink(text, link) { - return new docx.InternalHyperlink({ - children: [ - new docx.TextRun({ - text: text, - style: 'Hyperlink', - font: this.font, - size: 16, - }), - ], - anchor: link, - }) - } - - /** - * @function makeBookmark - * @description Create a target within a document to link to with an internal hyperlink - * @param {String} text - text/prose for the function - * @param {String} ident - the unique name of the bookmark - * @returns {Object} a new docx paragraph object with a bookmark - * @todo test and revise this function as it may need to be a textrun which can be embedded in something else - */ - makeBookmark(text, ident) { - return new docx.Paragraph({ - children: [ - new docx.Bookmark({ - id: String(ident), - children: [ - new docx.TextRun({text: text}) - ] - }) - ] - }) - } - - /** - * @function makeHeadingBookmark1 - * @description Create a target within a document to link to with an internal hyperlink of heading 1 - * @param {String} text - text/prose for the function - * @param {String} ident - the unique name of the bookmark - * @returns {Object} a new docx paragraph object with a bookmark at the heading level 1 - * @todo could we generalize this function and make the heading level a parameter in the future? - */ - makeHeadingBookmark1(text, ident) { - return new docx.Paragraph({ - heading: docx.HeadingLevel.HEADING_1, - children: [ - new docx.Bookmark({ - id: String(ident), - children: [ - new docx.TextRun({text: text}) - ] - }) - ] - }) - } - - /** - * @function makeHeadingBookmark2 - * @description Create a target within a document to link to with an internal hyperlink of heading 2 - * @param {String} text - text/prose for the function - * @param {String} ident - the unique name of the bookmark - * @returns {Object} a new docx paragraph object with a bookmark at the heading level 2 - */ - makeHeadingBookmark2(text, ident) { - return new docx.Paragraph({ - heading: docx.HeadingLevel.HEADING_2, - children: [ - new docx.Bookmark({ - id: String(ident), - children: [ - new docx.TextRun({text: text}) - ] - }) - ] - }) - } - - - - /** - * @function basicRow - * @description Basic table row to produce a name/value pair table with 2 columns - * @param {String} name - text/prose for the cell - * @param {String} data - text/prose for the cell - * @returns {Object} a new docx TableRow object - */ - basicRow (name, data) { - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(name, {fontSize: this.fontFactor * this.fontSize, bold: true})] - }), - new docx.TableCell({ - width: { - size: 80, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(data, {fontSize: this.fontFactor * this.fontSize})] - }) - ] - }) - } - - /** - * @function descriptionRow - * @description Description table row to produce a name/value pair table with 2 columns - * @param {String} id - text/prose for the cell - * @param {String} description - text/prose for the cell - * @returns {Object} a new docx TableRow object - */ - descriptionRow(id, description, bold=false) { - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 10, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(id, {fontSize: 16, bold: bold ? true : false})] - }), - new docx.TableCell({ - width: { - size: 90, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(description, {fontSize: 16, bold: bold ? true : false})] - }) - ] - }) - } - - /** - * @function urlRow - * @description Hyperlink table row to produce a name/value pair table with 2 columns and an external hyperlink - * @param {String} category - text/prose for the first column - * @param {String} name - text/prose for the hyperlink in the second column - * @param {String} link - the URL for the hyperlink - * @returns {Object} a new docx TableRow object with an external hyperlink - */ - urlRow(category, name, link) { - // define the link to the target URL - const myUrl = new docx.ExternalHyperlink({ - children: [ - new docx.TextRun({ - text: name, - style: 'Hyperlink', - font: this.font, - size: this.fontSize - }) - ], - link: link - }) - - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(category, this.fontFactor * this.fontSize, true)] - }), - new docx.TableCell({ - width: { - size: 80, - type: docx.WidthType.PERCENTAGE - }, - children: [new docx.Paragraph({children:[myUrl]})] - }) - ] - }) - } - - /** - * @function basicTopicRow - * @description Create a 3 column row for displaying topics which are the results of term extraction - * @param {String} theme - text/prose for the theme in col 1 - * @param {Float} score - the numerical score for the term in col 2 - * @param {String} rank - a textual description of the relative priority for the term in col 3 - * @param {Boolean} bold - whether or not to make the text/prose bold typically used for header row - * @returns {Object} a new 3 column docx TableRow object - */ - basicTopicRow (theme, score, rank, bold) { - const myFontSize = 16 - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 60, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.makeParagraph(theme, myFontSize, bold ? true : false)] - }), - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.makeParagraph(score, myFontSize, bold ? true : false)] - }), - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.makeParagraph(rank, myFontSize, bold ? true : false)] - }), - ] - }) - } - - /** - * @function basicComparisonRow - * @description Create a 4 column row for displaying comparisons which are the results of similarity comparisons - * @param {String} company - text/prose for the company in col 1 - * @param {String} role - the role of the company in col 2 - * @param {Float} score - the numerical score for the term in col 3 - * @param {String} rank - a textual description of the relative priority for the company in col 4 - * @param {Boolean} bold - whether or not to make the text/prose bold typically used for header row - * @returns {Object} a new 4 column docx TableRow object - */ - basicComparisonRow (company, role, distance, bold) { - const myFontSize = 16 - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 40, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.makeParagraph(company, myFontSize, bold ? true : false)] - }), - new docx.TableCell({ - width: { - size: 30, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.makeParagraph(role, myFontSize, bold ? true : false)] - }), - new docx.TableCell({ - width: { - size: 30, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.makeParagraph(distance, myFontSize, bold ? true : false)] - }), - ] - }) - } - - - - /** - * @async - * @function writeReport - * @description safely write a DOCX report to a desired location - * @param {Object} docObj - a complete and error free document object that is ready to be saved - * @param {String} fileName - the file name for the DOCX object - * @returns {Array} an array containing if the save operation succeeded, the message, and null - */ - async writeReport (docObj, fileName) { - try { - await docx.Packer.toBuffer(docObj).then((buffer) => { - fs.writeFileSync(fileName, buffer) - }) - return [true, 'SUCCESS: Created file [' + fileName + '] for object.', null] - } catch(err) { - return [false, 'ERROR: Failed to create report for object.', null] - } - } - - /** - * @function rankTags - * @description Rank supplied topics and return an object that can be rendered - * @param {Object} tags - the tags from the source object to be ranked - * @returns {Object} the final tags which now have ranking and are suitable for a basicTopicRow - */ - rankTags (tags) { - const ranges = boxPlot(Object.values(tags)) - let finalTags = {} - for (const tag in tags) { - // Rank the tag score using the ranges derived from box plots - // if > Q3 then the ranking is high - // if in between Q2 and Q3 then the ranking is medium - // if < Q3 then the ranking is low - let rank = null - if (tags[tag] > ranges.upperQuartile) { - rank = 'High' - } else if (tags[tag] < ranges.lowerQuartile) { - rank = 'Low' - } else if (ranges.lowerQuartile <= tags[tag] <= ranges.upperQuartile) { - rank = 'Medium' - } - - finalTags[tag] = { - score: tags[tag], // Math.round(tags[tag]), - rank: rank - } - - } - return finalTags - } - - /** - * @function topicTable - * @description A higher level function that calls basicTopicRow to create a complete table - * @param {Object} topics - the result of rankTags - * @returns {Object} a complete docx table that includes topics - * @todo Sort based upon rank from highest to lowest - */ - topicTable(topics) { - let myRows = [this.basicTopicRow('Keywords', 'Term Frequency', 'Rank', true)] - // TODO When the score and rank are switched the program will fail and will not create a report. - // This appears to potentially be bug in docx, but as for now we will have to not change - // the code to swap the two columns. Utilmately, the goal would be to swap the columns to - // meet customer needs. - // TODO sort the columns based upon rank - for (const topic in topics) { - myRows.push(this.basicTopicRow(topic, topics[topic].score.toFixed(2), topics[topic].rank)) - } - // define the table with the summary theme information - const myTable = new docx.Table({ - columnWidths: [60, 20, 20], - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - - return myTable - } - - _tagCell(tag) { - return new docx.TableCell({ - margins: { - top: this.generalSettings.tagMargin, - right: this.generalSettings.tagMargin, - bottom: this.generalSettings.tagMargin, - left: this.generalSettings.tagMargin - }, - borders: { - top: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, // 2 points thick, black color - bottom: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, - left: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, - right: { size: 20, color: this.themeSettings.documentColor, style: docx.BorderStyle.SINGLE }, - }, - shading: {fill: this.themeSettings.tagColor}, - children: [this.makeParagraph(tag, {fontSize: this.fontSize, fontColor: this.themeSettings.tagFontColor, center: true})], - verticalAlign: docx.AlignmentType.CENTER, - }) - } - - _distributeTags(tagsList) { - // Calculate the number of lists as the ceiling of the square root of the length of tagsList - const numLists = Math.ceil(Math.sqrt(tagsList.length)) - - // Initialize result array with numLists empty arrays - const result = Array.from({ length: numLists }, () => []) - // Initialize lengths array with numLists zeros - const lengths = Array(numLists).fill(0) - - // Sort tagsList in descending order based on the length of the tags - tagsList.sort((a, b) => b.length - a.length) - - // Distribute tags - tagsList.forEach(tag => { - // Find the index of the child list with the minimum total character length - const minIndex = lengths.indexOf(Math.min(...lengths)) - // Add the tag to this child list - result[minIndex].push(tag); - // Update the total character length of this child list - lengths[minIndex] += tag.length - }) - - return result; - } - - tagsTable(tags) { - // Get the length of the tags - const tagsList = Object.keys(tags) - const distributedTags = this._distributeTags(tagsList) - let myRows = [] - distributedTags.forEach(tags => { - let cells = [] - tags.forEach(tag => { - cells.push(this._tagCell(tag)) - }) - myRows.push(new docx.TableRow({ - children: cells - })) - }) - // define the table with the summary theme information - const myTable = new docx.Table({ - columnWidths: Array(distributedTags.length).fill(100/distributedTags.length), - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - - return myTable - } - - - - // Create an introductory section - /** - * @function makeIntro - * @description Creates a complete document with a heading of level 1 - * @param {String} introText - text/prose for the introduction - * @returns {Object} a complete introduction with heading level 1 and a paragraph - */ - makeIntro (introText) { - return [ - this.makeHeading1('Introduction'), - this.makeParagraph(introText) - ] - } -} - -export default DOCXUtilities \ No newline at end of file diff --git a/src/report/companies.js b/src/report/companies.js index 1ef74d6..5a9f560 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -2,32 +2,35 @@ * Two classes to create sections and documents for company objects in mediumroast.io * @author Michael Hay * @file companies.js - * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 1.0.0 + * @version 1.2.0 */ // Import required modules import docx from 'docx' import boxPlot from 'box-plot' -import DOCXUtilities from './common.js' +import Utilities from './helpers.js' import { InteractionSection } from './interactions.js' import { CompanyDashbord } from './dashboard.js' -import { getMostSimilarCompany } from './tools.js' +import { getMostSimilarCompany } from './deprecate_tools.js' import TextWidgets from './widgets/Text.js' import TableWidgets from './widgets/Tables.js' class BaseCompanyReport { - constructor(company, env) { - this.company = company + constructor(companyName, companies, interactions, env) { + this.util = new Utilities(env) + const sourceData = this.util.initializeCompanyData(companyName, companies, interactions) + // console.log('sourceData>>>', sourceData) + this.sourceData = sourceData + this.company = sourceData.company + this.companyName = companyName + this.companies = companies + this.interactions = interactions this.env = env - this.company.stock_symbol === 'Unknown' && this.company.cik === 'Unknown' ? - this.companyType = 'Private' : - this.companyType = 'Public' - this.util = new DOCXUtilities(env) this.baseDir = this.env.outputDir this.workDir = this.env.workDir - this.baseName = company.name.replace(/ /g,"_") + this.baseName = companyName.replace(/ /g,"_") this.textWidgets = new TextWidgets(env) this.tableWidgets = new TableWidgets(env) } @@ -46,8 +49,8 @@ class CompanySection extends BaseCompanyReport { * @todo Since the ingestion function detects the companyType this property is deprecated and should be removed * @todo separate this class into a separate file */ - constructor(company, env) { - super(company, env) + constructor(companyName, companies, interactions, env) { + super(companyName, companies, interactions, env) } // Create a URL on Google maps to search for the address @@ -96,6 +99,9 @@ class CompanySection extends BaseCompanyReport { * @returns {Object} A docx table is return to the caller */ makeFirmographicsDOCX() { + this.company.stock_symbol === 'Unknown' && this.company.cik === 'Unknown' ? + this.companyType = 'Private' : + this.companyType = 'Public' return new docx.Table({ columnWidths: [20, 80], rows: [ @@ -131,12 +137,9 @@ class CompanySection extends BaseCompanyReport { _rankComparisons (comparisons, competitors) { // Set up a blank object to help determine the top score let rankPicker = {} - - // Using the Euclidean distance find the closest company - const rankedCompanies = getMostSimilarCompany(comparisons, competitors) // const ranges = boxPlot(similarityScores) - const ranges = boxPlot(rankedCompanies.distances) + const ranges = boxPlot(competitors.distances) // Restructure the objects into the final object for return let finalComparisons = {} @@ -146,9 +149,9 @@ class CompanySection extends BaseCompanyReport { // if in between Q2 and Q3 then the ranking is Nearby // if < Q3 then the ranking is Closest let rank = null - if (rankedCompanies.companyMap[compare] >= ranges.upperQuartile) { + if (competitors.companyMap[compare] >= ranges.upperQuartile) { rank = 'Furthest' - } else if (rankedCompanies.companyMap[compare] <= ranges.lowerQuartile) { + } else if (competitors.companyMap[compare] <= ranges.lowerQuartile) { rank = 'Closest' // NOTE: this should work, but for some reason it isn't, head scratcher // } else if (ranges.lowerQuartile < comparisons[compare].similarity < ranges.upperQuartile) { @@ -157,12 +160,12 @@ class CompanySection extends BaseCompanyReport { } // Populate the rank picker to determine the top score - rankPicker[rankedCompanies.companyMap[compare]] = compare + rankPicker[competitors.companyMap[compare]] = compare // Build the final comparison object finalComparisons[compare] = { // Normalize to two decimal places and turn into % - score: Math.ceil(rankedCompanies.companyMap[compare] * 10), + score: Math.ceil(competitors.companyMap[compare] * 10), rank: rank, role: comparisons[compare].role, name: comparisons[compare].name @@ -227,7 +230,8 @@ class CompanySection extends BaseCompanyReport { let competitivePage = [] // Construct the object to create company related document sections - const comp = new CompanySection(similarCompany, this.env) + // NOTE: We are here and will need to think about how to handle the call, perhaps we need similarCompany.name, allCompanies, and allInteractions for the call. Should think more about it. + const comp = new CompanySection(similarCompany.name, this.companies, this.interactions, this.env) // Create the company firmographics table const firmographicsTable = comp.makeFirmographicsDOCX() const tagsTable = this.tableWidgets.tagsTable(similarCompany.tags) @@ -235,10 +239,10 @@ class CompanySection extends BaseCompanyReport { // Create a section for the most/least similar interactions const mostSimIntName = similarity[similarCompany.name].most_similar.name const mostSimIntScore = Math.round(parseFloat(similarity[similarCompany.name].most_similar.score) * 100) - const mostSimInt = interactions.filter(interaction => interaction.name === mostSimIntName)[0] + const mostSimInt = this.interactions.filter(interaction => interaction.name === mostSimIntName)[0] const leastSimIntName = similarity[similarCompany.name].least_similar.name const leastSimIntScore = Math.round(parseFloat(similarity[similarCompany.name].least_similar.score) * 100) - const leastSimInt = interactions.filter(interaction => interaction.name === leastSimIntName)[0] + const leastSimInt = this.interactions.filter(interaction => interaction.name === leastSimIntName)[0] // Compute reading time const totalReadingTime = parseInt(mostSimInt.reading_time) + parseInt(leastSimInt.reading_time) @@ -308,26 +312,24 @@ class CompanyStandalone extends BaseCompanyReport { * @todo Rename this class as report and rename the file as companyDocx.js * @todo Adapt to settings.js for consistent application of settings, follow dashboard.js */ - constructor(sourceData, env, author='Mediumroast for GitHub') { - super(sourceData.company[0], env) - this.sourceData = sourceData + constructor(companyName, companies, interactions, env, author='Mediumroast for GitHub') { + super(companyName, companies, interactions, env) this.objectType = 'Company' this.creator = author this.author = author this.authoredBy = author - this.title = `${this.company.name} Company Report` - this.companyInteractions = sourceData.interactions - this.interactions = sourceData.allInteractions - this.competitors = sourceData.competitors.all - this.mostSimilar = sourceData.competitors.mostSimilar - this.leastSimilar = sourceData.competitors.leastSimilar + this.title = `${this.companyName} Company Report` + this.companyInteractions = this.sourceData.interactions + this.competitors = this.sourceData.competitors + this.mostSimilar = this.sourceData.competitors.mostSimilar + this.leastSimilar = this.sourceData.competitors.leastSimilar this.description = `A Company report for ${this.company.name} that includes firmographics, key information on competitors, and Interactions data.` this.introduction = `Mediumroast for GitHub automatically generated this document. It includes company firmographics, key information on competitors, and Interactions data for ${this.company.name}. If this report is produced as a package, then the hyperlinks are active and will link to documents on the local folder after the package is opened.` this.similarity = this.company.similarity, this.noInteractions = String(Object.keys(this.company.linked_interactions).length) - this.totalInteractions = sourceData.totalInteractions - this.totalCompanies = sourceData.totalCompanies - this.averageInteractions = sourceData.averageInteractionsPerCompany + this.totalInteractions = this.sourceData.totalInteractions + this.totalCompanies = this.sourceData.totalCompanies + this.averageInteractions = this.sourceData.averageInteractionsPerCompany } /** @@ -341,7 +343,7 @@ class CompanyStandalone extends BaseCompanyReport { */ async makeDOCX(fileName, isPackage) { // Initialize the working directories to create a package and/or download relevant images - this.util.initDirectories() + this.util.initReportWorkspace() // Set the file name fileName = fileName ? fileName : `${this.env.outputDir}/${this.baseName}.docx` @@ -360,10 +362,10 @@ class CompanyStandalone extends BaseCompanyReport { const preparedFor = `${this.authoredBy} report for: ` // Construct the companySection and interactionSection objects - const companySection = new CompanySection(this.company, this.env) + const companySection = new CompanySection(this.companyName, this.companies, this.interactions, this.env) const interactionSection = new InteractionSection( this.companyInteractions, - this.company.name, + this.companyName, this.objectType, this.env ) @@ -385,7 +387,7 @@ class CompanyStandalone extends BaseCompanyReport { // Set up the default options for the document const myDocument = [].concat( - this.util.makeIntro(this.introduction), + this.textWidgets.makeIntro(this.introduction), // Generate the firmographics and tags sections for the Company being reported on [ this.textWidgets.makeHeading1('Firmographics'), @@ -419,10 +421,10 @@ class CompanyStandalone extends BaseCompanyReport { title: this.title, description: this.description, background: { - color: this.util.documentColor, + color: this.textWidgets.themeSettings.documentColor, }, - styles: {default: this.util.styling.default}, - numbering: this.util.styling.numbering, + styles: {default: this.textWidgets.styles.default}, + numbering: this.textWidgets.styles.numbering, sections: [ { properties: { @@ -433,11 +435,11 @@ class CompanyStandalone extends BaseCompanyReport { }, }, headers: { - default: this.util.makeHeader(this.company.name, preparedFor, {landscape: true}) + default: this.textWidgets.makeHeader(this.company.name, preparedFor, {landscape: true}) }, footers: { default: new docx.Footer({ - children: [this.util.makeFooter(authoredBy, preparedOn, {landscape: true})] + children: [this.textWidgets.makeFooter(authoredBy, preparedOn, {landscape: true})] }) }, children: [ @@ -455,11 +457,11 @@ class CompanyStandalone extends BaseCompanyReport { { properties: {}, headers: { - default: this.util.makeHeader(this.company.name, preparedFor) + default: this.textWidgets.makeHeader(this.company.name, preparedFor) }, footers: { default: new docx.Footer({ - children: [this.util.makeFooter(authoredBy, preparedOn)] + children: [this.textWidgets.makeFooter(authoredBy, preparedOn)] }) }, children: myDocument, diff --git a/src/report/dashboard.js b/src/report/dashboard.js index 706e8b8..3165071 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -2,15 +2,15 @@ * Classes to create dashboards for company, interaction and study objects in mediumroast.io documents * @author Michael Hay * @file dashboard.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. + * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 0.1.0 + * @version 2.0.0 */ // Import required modules import docx from 'docx' import * as fs from 'fs' -import DOCXUtilities from './common.js' +import Utilities from './helpers.js' import docxSettings from './settings.js' import Charting from './charts.js' import TableWidgets from './widgets/Tables.js' @@ -25,193 +25,11 @@ class Dashboards { */ constructor(env) { this.env = env - this.util = new DOCXUtilities(env) + this.util = new Utilities(env) this.charting = new Charting(env) this.tableWidgets = new TableWidgets(env) this.themeStyle = docxSettings[env.theme] // Set the theme for the report this.generalStyle = docxSettings.general // Pull in all of the general settings - - // Define specifics for table borders - this.noneStyle = { - style: this.generalStyle.noBorderStyle - } - this.borderStyle = { - style: this.generalStyle.tableBorderStyle, - size: this.generalStyle.tableBorderSize, - color: this.themeStyle.tableBorderColor - } - // No borders - this.noBorders = { - left: this.noneStyle, - right: this.noneStyle, - top: this.noneStyle, - bottom: this.noneStyle - } - // Right border only - this.rightBorder = { - left: this.noneStyle, - right: this.borderStyle, - top: this.noneStyle, - bottom: this.noneStyle - } - // Bottom border only - this.bottomBorder = { - left: this.noneStyle, - right: this.noneStyle, - top: this.noneStyle, - bottom: this.borderStyle - } - // Bottom and right borders - this.bottomAndRightBorders = { - left: this.noneStyle, - right: this.borderStyle, - top: this.noneStyle, - bottom: this.borderStyle - } - // Top and right borders - this.topAndRightBorders = { - left: this.noneStyle, - right: this.borderStyle, - top: this.borderStyle, - bottom: this.noneStyle - } - // All borders, helpful for debugging - this.allBorders = { - left: this.borderStyle, - right: this.borderStyle, - top: this.borderStyle, - bottom: this.borderStyle - } - - // TODO need baseDir to be passed in or defined in the constructor - - } - - /** - * - * - * @param {*} statistics - * @returns - * @todo turn into a loop instead of having the code repeated - * @todo if the length of any number is greater than 3 digits shrink the font size by 15% and round down - * @todo add a check for the length of the title and shrink the font size by 15% and round down - * @todo add a check for the length of the value and shrink the font size by 15% and round down - */ - descriptiveStatisticsTable(statistics) { - let myRows = [] - for(const stat in statistics) { - myRows.push( - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.util.makeParagraph( - statistics[stat].value, - { - fontSize: this.generalStyle.metricFontSize, - fontColor: this.themeStyle.titleFontColor, - font: this.generalStyle.heavyFont, - bold: true, - center: true - } - ) - ], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin - } - }), - ] - }), - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [ - this.util.makeParagraph( - statistics[stat].title, - { - fontSize: this.generalStyle.metricFontSize/2, - fontColor: this.themeStyle.titleFontColor, - bold: false, - center: true - } - ) - ], - borders: this.noBorders, - margins: { - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - } - }), - ] - }) - ) - } - return new docx.Table({ - columnWidths: [95], - borders: this.noBorders, - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - } - - twoCellRow (name, data) { - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE - }, - children: [this.util.makeParagraph(name, {fontSize: this.generalStyle.dashFontSize, bold: true})], - borders: this.bottomBorder, - margins: { - top: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin - }, - }), - new docx.TableCell({ - width: { - size: 80, - type: docx.WidthType.PERCENTAGE - }, - children: [this.util.makeParagraph(data, {fontSize: this.generalStyle.dashFontSize})], - borders: this.bottomAndRightBorders, - margins: { - top: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin - }, - }) - ] - }) - } - - // Using DOCXUtilties basicRow method create a similar method for all dashboards that returns a table with a single row - /** - * - * @param {*} data - * @returns - */ - simpleDescriptiveTable(title, text) { - return new docx.Table({ - columnWidths: [95], - borders: this.noBorders, - margins: { - left: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin, - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - }, - rows: [this.twoCellRow(title, text)], - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) } // Create a utility that takes text as an input, and an integer called numSentences, splits the text into sentences and returns the first numSentences as a string @@ -286,99 +104,16 @@ class InteractionDashboard extends Dashboards { // Loop through the top 2 topics and create a table row for each topic for (const topic in top2Topics) { myTopics.push( - super.simpleDescriptiveTable('Priority Proto-requirement', top2Topics[topic].label) + this.tableWidgets.oneRowTwoColumnsTable( + ['Priority Proto-requirement', top2Topics[topic].label] + ) ) } return myTopics } - _mergeLeftContents (contents) { - let myRows = [] - for (const content in contents) { - myRows.push( - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [contents[content]], - borders: this.noBorders, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - }, - }), - ] - }) - ) - } - return new docx.Table({ - columnWidths: [95], - borders: this.noBorders, - margins: { - left: this.generalStyle.tableMargin, - right: this.generalStyle.tableMargin, - bottom: this.generalStyle.tableMargin, - top: this.generalStyle.tableMargin - }, - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - } - - // Create the dashboard shell which will contain all of the outputs - _createDashboardShell (leftContents, rightContents) { - return new docx.Table({ - columnWidths: [70, 30], - rows: [ - new docx.TableRow({ - children: [ - new docx.TableCell({ - children: [leftContents], - borders: this.noBorders - }), - new docx.TableCell({ - children: [rightContents], - borders: this.noBorders - }), - ] - }) - ], - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - }, - height: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - } - async makeDashboard(interaction, company) { - // Define the right contents - // Add key metadata for the interaction to fit within the right contents - /* - ------------ - | Meta | - | -------- | - | Data | - | | - | Meta | - | -------- | - | Data | - | | - | Meta | - | -------- | - | Data | - | | - | Meta | - | -------- | - | Data | - ------------ - */ - const rightContents = super.descriptiveStatisticsTable([ + const rightContents = this.tableWidgets.descriptiveStatisticsTable([ {title: 'Type', value: interaction.interaction_type}, {title: 'Est. reading time (min)', value: interaction.reading_time}, {title: 'Page(s)', value: interaction.page_count}, @@ -386,23 +121,21 @@ class InteractionDashboard extends Dashboards { {title: 'Proto-requirements', value: Object.keys(interaction.topics).length}, ]) - // Create individual tables for name, description and company - /* - ------------------------------ - | | | - ------------------------------ - - */ - const interactionNameTable = super.simpleDescriptiveTable('Name', interaction.name) - const interactionDescriptionTable = super.simpleDescriptiveTable('Description', super.shortenText(interaction.description)) - const associatedCompanyTable = super.simpleDescriptiveTable(`Linked company: ${company.name}`, super.shortenText(company.description)) + const interactionNameTable = this.tableWidgets.oneColumnTwoRowsTable( + ['Name', interaction.name] + ) + const interactionDescriptionTable = this.tableWidgets.oneColumnTwoRowsTable( + ['Description', super.shortenText(interaction.description)] + ) + const associatedCompanyTable = this.tableWidgets.oneColumnTwoRowsTable( + [`Linked company: ${company.name}`, super.shortenText(company.description)] + ) const top2Topics = this._getTopTwoTopics(interaction.topics) const protorequirementsTable = this._priorityTopicsTable(top2Topics)[0] - const leftContents = this._mergeLeftContents([interactionNameTable, interactionDescriptionTable, associatedCompanyTable, protorequirementsTable]) + const leftContents = this.tableWidgets.packContents([interactionNameTable, interactionDescriptionTable, associatedCompanyTable, protorequirementsTable]) - - return this._createDashboardShell(leftContents, rightContents) + return this.tableWidgets.createDashboardShell(leftContents, rightContents, {leftWidth: 80, rightWidth: 20}) } } @@ -477,6 +210,7 @@ class CompanyDashbord extends Dashboards { const chartsTable = this._createChartsTable(bubbleChartFile, pieChartFile) // Create the most similar company description table + // TODO this needs to be fixed because the Ecludian distance doesn't match the most similar company const mostSimilarCompanyDescTable = this.tableWidgets.oneColumnTwoRowsTable([ `Most similar company to ${company.name}: ${competitors.mostSimilar.name}`, this.shortenText(competitors.mostSimilar.description, 3) @@ -516,7 +250,6 @@ class CompanyDashbord extends Dashboards { {title: 'Total Companies', value: totalCompanies}, ]) - return this.tableWidgets.createDashboardShell(leftContents, rightContents, {leftWidth: 85, rightWidth: 15}) } diff --git a/src/report/helpers.js b/src/report/helpers.js new file mode 100644 index 0000000..b2b3df3 --- /dev/null +++ b/src/report/helpers.js @@ -0,0 +1,144 @@ +/** + * A set of common utilities for creating HTML and DOCX reports for mediumroast.io objects + * @author Michael Hay + * @file helpers.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + */ + +// Import modules +import docx from 'docx' +import * as fs from 'fs' +import docxSettings from './settings.js' +import FilesystemOperators from '../cli/filesystem.js' + +class Utilities { + /** + * To make machine authoring of a Microsoft DOCX file consistent and easier this class has been + * developed. Key functions are available that better describe the intent of the operation + * by name which makes it simpler author a document instead of sweating the details. + * Further through trial and error the idiosyncrasies of the imported docx module + * have been worked out so that the developer doesn't have to accidently find out and struggle + * with document generation. + * @constructor + * @classdesc Core utilities for generating elements in a Microsoft word DOCX file + * @param {Object} env + * @todo Remove text and table widgets as they are not used and now in another class + */ + constructor(env) { + this.env = env + this.generalSettings = docxSettings.general + this.themeSettings = docxSettings[this.env.theme] + // this.styling = this.initStyles() + this.fileSystem = new FilesystemOperators() + this.regions = { + AMER: 'Americas', + EMEA: 'Europe, Middle East and Africa', + APAC: 'Asia Pacific and Japan' + } + } + + /** + * @function initReportWorkspace + * @description Initialize working directories needed for report production + * @returns {void} + * + * + */ + initReportWorkspace() { + const subdirs = ['interactions', 'images'] + for (const myDir in subdirs) { + this.fileSystem.safeMakedir(this.env.workDir + '/' + subdirs[myDir]) + } + } + + /** + * @async + * @function writeReport + * @description safely write a DOCX report to a desired location + * @param {Object} docObj - a complete and error free document object that is ready to be saved + * @param {String} fileName - the file name for the DOCX object + * @returns {Array} an array containing if the save operation succeeded, the message, and null + */ + async writeReport(docObj, fileName) { + try { + await docx.Packer.toBuffer(docObj).then((buffer) => { + fs.writeFileSync(fileName, buffer) + }) + return [true, 'SUCCESS: Created file [' + fileName + '] for object.', null] + } catch (err) { + return [false, 'ERROR: Failed to create report for object.', null] + } + } + + // ----------------- Company Data Manipulation Utilities ----------------- // + getCompetitors(similarities, companies, interactions) { + // x1 = 1 and y1 = 1 because this the equivalent of comparing a company to itself + const x1 = 1 + const y1 = 1 + let distanceToCompany = {} + let companyToDistance = {} + for (const companyName in similarities) { + // Compute the distance using d = sqrt((x2 - x1)^2 + (y2 - y1)^2) + const myDistance = Math.sqrt( + (similarities[companyName].most_similar.score - x1) ** 2 + + (similarities[companyName].least_similar.score - y1) ** 2 + ) + distanceToCompany[myDistance] = companyName + companyToDistance[companyName] = myDistance + } + + // Obtain the closest company using max, note min returns the least similar + const leastSimilarName = distanceToCompany[Math.max(...Object.keys(distanceToCompany))] + let leastSimilarCompany = this.getCompany(leastSimilarName, companies) + leastSimilarCompany[0].interactions = this.getInteractions(leastSimilarCompany, interactions) + + const mostSimilarName = distanceToCompany[Math.min(...Object.keys(distanceToCompany))] + let mostSimilarCompany = this.getCompany(mostSimilarName, companies) + mostSimilarCompany[0].interactions = this.getInteractions(mostSimilarCompany, interactions) + + // Transform the strings into floats prior to return + const allDistances = Object.keys(distanceToCompany).map( + (distance) => { + return parseFloat(distance) + } + ) + // return both the most similar id and all computed distanceToCompany + return { + mostSimilar: mostSimilarCompany[0], + leastSimilar: leastSimilarCompany[0], + distances: allDistances, + companyMap: companyToDistance, + all: companies + } + } + + // Retrieve the company by name + getCompany(companyName, companies) { + return companies.filter(company => company.name === companyName) + } + // Retrieve the interactions for a company + getInteractions(company, interactions) { + // console.log(interactions) + const interactionNames = Object.keys(company[0].linked_interactions); + return interactionNames.map(interactionName => + interactions.find(interaction => interaction.name === interactionName) + ).filter(interaction => interaction !== undefined) + } + + initializeCompanyData(companyName, companies, interactions) { + const sourceCompany = this.getCompany(companyName, companies) + const competitors = this.getCompetitors(sourceCompany[0].similarity, companies, interactions) + return { + company: sourceCompany[0], + interactions: this.getInteractions(sourceCompany, interactions), + competitors: competitors, + totalInteractions: interactions.length, + totalCompanies: companies.length, + averageInteractionsPerCompany: Math.round(interactions.length / companies.length), + } + } + +} + +export default Utilities \ No newline at end of file diff --git a/src/report/interactions.js b/src/report/interactions.js index 8218816..1b91d7c 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -9,7 +9,7 @@ // Import required modules import docx from 'docx' -import DOCXUtilities from './common.js' +import Utilities from './helpers.js' import { CompanySection } from './companies.js' import { InteractionDashboard } from './dashboard.js' import docxSettings from './settings.js' @@ -28,7 +28,7 @@ class BaseInteractionsReport { this.objectName = objectName this.objectType = objectType this.env = env - this.util = new DOCXUtilities(env) + this.util = new Utilities(env) this.textWidgets = new TextWidgets(env) this.tableWidgets = new TableWidgets(env) this.themeStyle = docxSettings[env.theme] // Set the theme for the report @@ -51,7 +51,16 @@ class BaseInteractionsReport { } } + /** + * @function protorequirementsTable + * @todo Determine if the existing TableWidgets class can be used for this + */ protorequirementsTable(topics) { + // If the length of the topics is zero, then return a simple message + if (Object.keys(topics).length === 0) { + return this.textWidgets.makeParagraph('No proto-requirements were discovered or are associated to this interaction.') + } + let myRows = [ new docx.TableRow({ children: [ @@ -96,7 +105,10 @@ class BaseInteractionsReport { new docx.TableRow({ children: [ new docx.TableCell({ - children: [this.textWidgets.makeParagraph(topic, {fontSize: this.generalStyle.tableFontSize})], + children: [this.textWidgets.makeParagraph( + String(topic), + {fontSize: this.generalStyle.tableFontSize}) + ], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -107,7 +119,10 @@ class BaseInteractionsReport { }, }), new docx.TableCell({ - children: [this.textWidgets.makeParagraph(topics[topic].frequency, {fontSize: this.generalStyle.tableFontSize})], + children: [this.textWidgets.makeParagraph( + String(topics[topic].frequency), + {fontSize: this.generalStyle.tableFontSize}) + ], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -118,7 +133,10 @@ class BaseInteractionsReport { }, }), new docx.TableCell({ - children: [this.textWidgets.makeParagraph(topics[topic].label, {fontSize: this.generalStyle.tableFontSize})], + children: [this.textWidgets.makeParagraph( + String(topics[topic].label), + {fontSize: this.generalStyle.tableFontSize}) + ], borders: this.bottomBorder, margins: { top: this.generalStyle.tableMargin @@ -175,16 +193,28 @@ class InteractionSection extends BaseInteractionsReport { // Create the header row for the descriptions let myRows = [this.tableWidgets.twoColumnRowBasic(['Name', 'Description'], {allColumnsBold: true})] - // Loop over the interactions and pull out the interaction ids and descriptions + // Loop over the interactions and pull out the interaction names and descriptions for (const interaction in this.interactions) { - const interactionBaseLink = String(`interaction_${this.interactions[interaction].file_hash}`).substring(0, 20) + // const interactionBaseLink = String(`interaction_${this.interactions[interaction].file_hash}`).substring(0, 20) myRows.push(this.tableWidgets.twoColumnRowBasic( [ - this.textWidgets.makeInternalHyperLink( - this.interactions[interaction].name, interactionBaseLink - ), + this.interactions[interaction].name, this.interactions[interaction].description ], + // NOTE: This is the original code that was replaced by the line above + // because the hyperlink was not working in the DOCX format. If we can + // figure out how to make the hyperlink work in the DOCX format, we can + // switch back to this code in the future. + // + // Note that this works in a development environment, but not in the + // production environment, and unsure why. The hyperlink is not + // visible in the production environment. + // [ + // this.textWidgets.makeInternalHyperLink( + // this.interactions[interaction].name, interactionBaseLink + // ), + // this.interactions[interaction].description + // ], {firstColumnBold: false} ) ) @@ -263,7 +293,7 @@ class InteractionSection extends BaseInteractionsReport { }) } else { // Non isPackage version of the strip - metadataRow = this.tableWidgets.fourColumnRowBasic( + metadataRow = this.tableWidgets.threeColumnRowBasic( [ `Created on: ${this.interactions[interaction].creation_date}`, `Est. reading time: ${this.interactions[interaction].reading_time} min`, @@ -291,7 +321,7 @@ class InteractionSection extends BaseInteractionsReport { // Push all of the content into the references array references.push( // Create the bookmark for the interaction - this.util.makeHeadingBookmark2( + this.textWidgets.makeHeadingBookmark2( this.interactions[interaction].name, String( 'interaction_' + @@ -299,7 +329,7 @@ class InteractionSection extends BaseInteractionsReport { ).substring(0, 20) ), // Create the abstract for the interaction - this.util.makeParagraph(this.interactions[interaction].abstract), + this.textWidgets.makeParagraph(this.interactions[interaction].abstract), metadataStrip, this.textWidgets.makeHeading2('Tags'), tagsTable, @@ -352,7 +382,7 @@ class InteractionStandalone extends BaseInteractionsReport { ' hyperlinks are active and will link to documents on the local folder after the' + ' package is opened.' this.abstract = interaction.abstract - this.tags = this.util.rankTags(this.interaction.tags) + this.tags = this.interaction.tags this.topics = this.interaction.topics } @@ -392,7 +422,7 @@ class InteractionStandalone extends BaseInteractionsReport { */ async makeDOCX(fileName, isPackage) { // Initialize the working directories - this.util.initDirectories() + this.util.initReportWorkspace() // If fileName isn't specified create a default fileName = fileName ? fileName : `${this.env.outputDir}/${this.interaction.name.replace(/ /g,"_")}.docx` @@ -418,13 +448,13 @@ class InteractionStandalone extends BaseInteractionsReport { // Set up the default options for the document const myDocument = [].concat( - this.util.makeIntro(this.introduction), + this.textWidgets.makeIntro(this.introduction), [ - this.util.makeHeading1('Abstract'), - this.util.makeParagraph(this.abstract), - this.util.makeHeading1('Tags'), - this.util.tagsTable(this.tags), - this.util.makeHeading1('Proto-Requirements'), + this.textWidgets.makeHeading1('Abstract'), + this.textWidgets.makeParagraph(this.abstract), + this.textWidgets.makeHeading1('Tags'), + this.tableWidgets.tagsTable(this.tags), + this.textWidgets.makeHeading1('Proto-Requirements'), super.protorequirementsTable(this.topics), ]) @@ -435,10 +465,10 @@ class InteractionStandalone extends BaseInteractionsReport { title: this.title, description: this.description, background: { - color: this.util.documentColor, + color: this.textWidgets.themeSettings.documentColor, }, - styles: {default: this.util.styling.default}, - numbering: this.util.styling.numbering, + styles: {default: this.textWidgets.styles.default}, + numbering: this.textWidgets.styles.numbering, sections: [ { properties: { @@ -449,11 +479,11 @@ class InteractionStandalone extends BaseInteractionsReport { }, }, headers: { - default: this.util.makeHeader(this.interaction.name, preparedFor, {landscape: true}) + default: this.textWidgets.makeHeader(this.interaction.name, preparedFor, {landscape: true}) }, footers: { default: new docx.Footer({ - children: [this.util.makeFooter(authoredBy, preparedOn, {landscape: true})] + children: [this.textWidgets.makeFooter(authoredBy, preparedOn, {landscape: true})] }) }, children: [ @@ -466,11 +496,11 @@ class InteractionStandalone extends BaseInteractionsReport { { properties: {}, headers: { - default: this.util.makeHeader(this.interaction.name, preparedFor) + default: this.textWidgets.makeHeader(this.interaction.name, preparedFor) }, footers: { default: new docx.Footer({ - children: [this.util.makeFooter(authoredBy, preparedOn)] + children: [this.textWidgets.makeFooter(authoredBy, preparedOn)] }) }, children: myDocument, diff --git a/src/report/settings.js b/src/report/settings.js index c48d985..cec8733 100644 --- a/src/report/settings.js +++ b/src/report/settings.js @@ -9,7 +9,7 @@ const docxSettings = { fontFactor: 1, dashFontSize: 22, tableFontSize: 9 * 2, // Note these are in half points, so 9 * 2 = 18 - titleFontSize: 30 * 2, + titleFontSize: 30, companyNameFontSize: 11.5, metricFontTitleSize: 11, metricFontSize: 48, @@ -26,7 +26,6 @@ const docxSettings = { font: "Avenir Next", heavyFont: "Avenir Next Heavy", lightFont: "Avenir Next Light" - }, coffee: { tableBorderColor: "4A7E92", // Light Blue @@ -50,12 +49,12 @@ const docxSettings = { documentColor: "0F0D0E", // Coffee black titleFontColor: "C7701E", // Saturated Light Blue textFontColor: "C7701E", // Ultra light Blue - chartAxisLineColor: "#374246", + chartAxisLineColor: "rgba(134,84,28, 0.9)", chartAxisFontColor: "rgba(199,112,30, 0.7)", chartAxisTickFontColor: "rgba(199,112,30, 0.6)", chartItemFontColor: "rgba(199,112,30, 0.9)", chartSeriesColor: "rgb(199,112,30, 0.7)", - chartSeriesBorderColor: "rgba(199,112,30, 0.9)", + chartSeriesBorderColor: "rgba(90,56,19, 0.9)", highlightFontColor: "" }, latte: { @@ -65,12 +64,12 @@ const docxSettings = { documentColor: "F1F0EE", // Coffee black titleFontColor: "25110f", // Saturated Light Blue textFontColor: "25110f", // Ultra light Blue - chartAxisLineColor: "#374246", + chartAxisLineColor: "rgba(66,24,17, 0.9)", chartAxisFontColor: "rgba(37,17,15, 0.7)", - chartAxisTickFontColor: "rgba(37,17,15, 0.6)", - chartItemFontColor: "rgba(37,17,15, 0.9)", + chartAxisTickFontColor: "rgba(205,183,160, 0.6)", + chartItemFontColor: "rgb(27,12,10, 0.7)", chartSeriesColor: "rgb(27,12,10, 0.7)", - chartSeriesBorderColor: "rgba(27,12,10, 0.9)", + chartSeriesBorderColor: "rgba(205,183,160, 0.9)", highlightFontColor: "" } } diff --git a/src/report/tools.js b/src/report/tools.js deleted file mode 100644 index 826835b..0000000 --- a/src/report/tools.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Common functions and tools used for report processing - * @author Michael Hay - * @file tools.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @version 1.0.0 - */ - -import FilesystemOperators from '../cli/filesystem.js' - -/** - * @function getMostSimilarCompany - * @description Find the closest competitor using the Euclidean distance - * @param {Object} similarities - the similarities from a company object - * @returns {Object} An array containing a section description and a table of interaction descriptions - */ -export function getMostSimilarCompany(similarities, companies) { - // x1 = 1 and y1 = 1 because this the equivalent of comparing a company to itself - const x1 = 1 - const y1 = 1 - let distanceToCompany = {} - let companyToDistance = {} - for(const companyName in similarities) { - // Compute the distance using d = sqrt((x2 - x1)^2 + (y2 - y1)^2) - const myDistance = Math.sqrt( - (similarities[companyName].most_similar.score - x1) ** 2 + - (similarities[companyName].least_similar.score - y1) ** 2 - ) - distanceToCompany[myDistance] = companyName - companyToDistance[companyName] = myDistance - } - // Obtain the closest company using max, note min returns the least similar - const mostSimilarId = distanceToCompany[Math.max(...Object.keys(distanceToCompany))] - // Get the id for the most similar company - const mostSimilarCompany = companies.filter(company => { - if (parseInt(company.name) === parseInt(mostSimilarId)) { - return company - } - }) - // Transform the strings into floats prior to return - const allDistances = Object.keys(distanceToCompany).map ( - (distance) => { - return parseFloat(distance) - } - ) - // return both the most similar id and all computed distanceToCompany - return {mostSimilarCompany: mostSimilarCompany[0], distances: allDistances, companyMap: companyToDistance} -} - - -/** - * @function initWorkingDirs - * @description Prepare working directories for report and package creation - * @param {String} baseDir - full path to the directory to initialize with key working directories - */ -export function initWorkingDirs(baseDir) { - const fileSystem = new FilesystemOperators() - const subdirs = ['interactions', 'images'] - for(const myDir in subdirs) { - fileSystem.safeMakedir(baseDir + '/' + subdirs[myDir]) - } -} - -/** - * @function cleanWorkingDirs - * @description Clean up working directories after report and/or package creation - * @param {String} baseDir - full path to the directory to initialize with key working directories - * @returns {Array} containing the status of the rmdir operation, status message and null - */ -export function cleanWorkingDirs(baseDir) { - const fileSystem = new FilesystemOperators() - return fileSystem.rmDir(baseDir) -} \ No newline at end of file diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index e22b79c..55c010f 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -1,4 +1,25 @@ -// report/widgets/TableWidget.js +/** + * @module TableWidgets + * @description This class contains row and column primitives for creating tables in a docx document and specific table structures + * @extends Widgets + * @requires Widgets + * @requires TextWidgets + * @requires docx + * + * @author Michael Hay + * @file Text.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 1.2.0 + * + * @example + * import TableWidgets from './Table.js' + * const tableWidgets = new TableWidgets() + * const myTable = tableWidgets.twoColumnRowBasic(["Name", "Michael Hay"], {firstColumnBold: true, allColumnsBold: false}) + * + */ + +// Import required modules import Widgets from './Widgets.js' import TextWidgets from './Text.js' import docx from 'docx' @@ -130,7 +151,7 @@ class TableWidgets extends Widgets { /** * @function twoColumnRowWithHyperlink * @description Hyperlink table row to produce a name/value pair table with 2 columns and an external hyperlink - @param {Array} cols - an array of 3 strings for the row first column, second column text, and the hyperlink URL + * @param {Array} cols - an array of 3 strings for the row first column, second column text, and the hyperlink URL * @param {Object} options - options for the cell to control bolding in the first column and all columns and border styles * @returns {Object} a new docx TableRow object with an external hyperlink */ @@ -474,7 +495,7 @@ class TableWidgets extends Widgets { lengths[minIndex] += tag.length }) - return result; + return result } /** @@ -496,6 +517,10 @@ class TableWidgets extends Widgets { * }) */ tagsTable(tags) { + // If there are no tags, return a message saying so + if (Object.keys(tags).length === 0) { + return this.textWidgets.makeParagraph('No proto-requirements were discovered or are associated to this interaction.') + } // Get the length of the tags const tagsList = Object.keys(tags) const distributedTags = this._distributeTags(tagsList) @@ -636,6 +661,17 @@ class TableWidgets extends Widgets { }) } + oneRowTwoColumnsTable(cols) { + return new docx.Table({ + columnWidths: [50, 50], + rows: [this.twoColumnRowBasic(cols)], + width: { + size: 100, + type: docx.WidthType.PERCENTAGE + } + }) + } + packContents (contents) { let myRows = [] for (const content in contents) { diff --git a/src/report/widgets/Text.js b/src/report/widgets/Text.js index ddd88fc..48192d4 100644 --- a/src/report/widgets/Text.js +++ b/src/report/widgets/Text.js @@ -1,4 +1,24 @@ -// report/widgets/TableWidget.js +/** + * @module TextWidgets + * @description This class contains Text widgets for building reports in docx + * @extends Widgets + * @requires Widgets + * @requires docx + * @version 1.2.0 + * + * @author Michael Hay + * @file Text.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * + * @example + * import TextWidgets from './Text.js' + * const text = new TextWidgets() + * const myParagraph = text.makeParagraph('This is a paragraph') + * + */ + +// Import required modules import Widgets from './Widgets.js' import docx from 'docx' @@ -234,17 +254,18 @@ class TextWidgets extends Widgets { * @returns {Object} a new docx InternalHyperlink object */ makeInternalHyperLink(text, link) { - return new docx.InternalHyperlink({ + const myLink = new docx.InternalHyperlink({ children: [ new docx.TextRun({ - text: text, + text: String(text), style: 'Hyperlink', - font: this.font, - size: 16, + font: this.generalSettings.font, + size: this.fullFontSize, }), ], anchor: link, }) + return myLink } /** From 76855ef77a503efbc176bc2709ba489415033698 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 25 Aug 2024 15:12:21 -0700 Subject: [PATCH 15/23] Interactions packager works, and various cleanups --- cli/mrcli-interaction.js | 59 +++++++++++++++----------- src/api/github.js | 34 +++++++++++++++ src/cli/common.js | 80 +++++++++++++++--------------------- src/report/companies.js | 1 - src/report/dashboard.js | 24 +++++++++-- src/report/interactions.js | 29 +++++++------ src/report/widgets/Tables.js | 15 +++++-- src/report/widgets/Text.js | 23 ++++++++++- 8 files changed, 170 insertions(+), 95 deletions(-) diff --git a/cli/mrcli-interaction.js b/cli/mrcli-interaction.js index 37f42d7..c873d63 100755 --- a/cli/mrcli-interaction.js +++ b/cli/mrcli-interaction.js @@ -6,7 +6,7 @@ * @file interactions.js * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 3.1.0 + * @version 3.2.0 */ @@ -18,6 +18,7 @@ import GitHubFunctions from '../src/api/github.js' import AddInteraction from '../src/cli/interactionWizard.js' import Environmentals from '../src/cli/env.js' import CLIOutput from '../src/cli/output.js' +import CLIUtilities from '../src/cli/common.js' import FilesystemOperators from '../src/cli/filesystem.js' import ArchivePackage from '../src/cli/archive.js' import ora from 'ora' @@ -57,7 +58,7 @@ const objectType = 'Interactions' // Environmentals object const environment = new Environmentals( - '3.1.0', + '3.2.0', `${objectType}`, `Command line interface for mediumroast.io ${objectType} objects.`, objectType @@ -76,6 +77,9 @@ const processName = 'mrcli-interaction' // Output object const output = new CLIOutput(myEnv, objectType) +// CLI Utilities object +const cliUtils = new CLIUtilities() + // Common wizard utilities const wutils = new WizardUtils(objectType) @@ -94,25 +98,33 @@ let results = Array() || [] // Process the cli options if (myArgs.report) { - // Retrive the interaction by Id - const [int_success, int_stat, int_results] = await interactionCtl.findByName(myArgs.report) - const companyName = Object.keys(int_results[0].linked_companies)[0] - // Retrive the company by Name - const [comp_success, comp_stat, comp_results] = await companyCtl.findByName(companyName) + // Use CLIUtils to get all objects + const allObjects = await cliUtils.getAllObjects({interactions: interactionCtl, companies: companyCtl}) + if(!allObjects[0]) { + console.error(`ERROR: ${allObjects[1].status_msg}`) + process.exit(-1) + } + + // Get the interaction by name + const interaction = cliUtils.getObject(myArgs.report, allObjects[2].interactions) + const companyName = Object.keys(interaction[0].linked_companies)[0] + // Get the company by Name + const company = cliUtils.getObject(companyName, allObjects[2].companies) // Set the root name to be used for file and directory names in case of packaging - const baseName = int_results[0].name.replace(/ /g,"_") + const baseName = interaction[0].name.replace(/ /g,"_") // Set the directory name for the package const baseDir = myEnv.workDir + '/' + baseName // Define location and name of the report output, depending upon the package switch this will change - let fileName = process.env.HOME + '/Documents/' + int_results[0].name.replace(/ /g,"_") + '.docx' + let fileName = process.env.HOME + '/Documents/' + interaction[0].name.replace(/ /g,"_") + '.docx' // Set up the document controller const docController = new InteractionStandalone( - int_results[0], // Interaction to report on - comp_results[0], // The company associated to the interaction - 'Mediumroast for GitHub robot', // The author - 'Mediumroast, Inc.', // The authoring company/org - myEnv // The environment settings + interaction, // Interaction to report on + company, // The company associated to the interaction + myEnv, // The environment settings + allObjects, // All objects + fileName, // The file name + myArgs.package // The package flag ) if(myArgs.package) { @@ -122,16 +134,15 @@ if (myArgs.report) { // If the directory creations was successful download the interaction if(dir_success) { fileName = baseDir + '/' + baseName + '_report.docx' - /* - TODO the below only assumes we're storing data in S3, this is intentionally naive. - In the future we will need to be led by the URL string to determine where and what - to download from. Today we only support S3, but this could be Sharepoint, - a local file system, OneDrive, GDrive, etc. There might be an initial less naive - implementation that looks at OneDrive, GDrive, DropBox, etc. as local file system - access points, but the tradeoff would be that caffeine would need to run on a - system with file system access to these objects. - */ - // await s3.s3DownloadObjs(int_results, baseDir + '/interactions', sourceBucket) + // Resolve the file name which is in int_results[0].url and it is everything after the last '/' + const interactionFileName = interaction[0].url.split('/').pop() + const downloadResults = await gitHubCtl.readBlob(interaction[0].url, baseDir + '/interactions') + if(downloadResults[0]) { + fileSystem.saveTextFile(`${baseDir}/interactions/${interactionFileName}`, downloadResults[2]) + } else { + console.error(`ERROR: ${downloadResults[1]}`) + process.exit(-1) + } // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) diff --git a/src/api/github.js b/src/api/github.js index e093c0e..862e806 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -20,6 +20,7 @@ */ import { Octokit } from "octokit" +import axios from "axios" class GitHubFunctions { @@ -513,6 +514,39 @@ class GitHubFunctions { } } + /** + * Read a blob (file) from a container (directory) in a specific branch. + * + * @param {string} fileName - The name of the blob to read with a complete path to the file (e.g. dirname/filename.ext). + * @returns {Array} A list containing a boolean indicating success or failure, a status message, and the blob's raw data (or the error message in case of failure). + */ + async readBlob(fileName) { + + const encodedFileName = encodeURIComponent(fileName) + const objectUrl = `https://api.github.com/repos/${this.orgName}/${this.repoName}/contents/${encodedFileName}` + const headers = { 'Authorization': `token ${this.token}` } + + try { + const result = await axios.get(objectUrl, { headers }) + const resultJson = result.data + const downloadUrl = resultJson.download_url + const downloadResult = await axios.get(downloadUrl, { responseType: 'arraybuffer' }); + const binFile = downloadResult.data + + return [ + true, + { status_code: 200, status_msg: `read object [${fileName}] from container [${fileName}]` }, + binFile + ] + } catch (e) { + return [ + false, + { status_code: 503, status_msg: `unable to read object [${fileName}] due to [${e}].` }, + e + ] + } + } + // Create a method using the octokit called deleteBlob to delete a file from the repo async deleteBlob(containerName, fileName, branchName, sha) { // Using the github API delete a file from the container diff --git a/src/cli/common.js b/src/cli/common.js index b15c90c..da1e2af 100644 --- a/src/cli/common.js +++ b/src/cli/common.js @@ -12,60 +12,48 @@ import axios from 'axios' import * as fs from 'fs' import * as path from 'path' -class Utilities { +class CLIUtilities { + /** * @async - * @function downloadBinary - * @description When given a full URL to an image download the image to the defined location - * @param {String} url - the full URL to the targeted image - * @param {String} dir - the target directory to save the file to - * @param {String} filename - the name of the file to save the image to + * @function getAllObjects + * @description Retrieve all objects from all controllers + * @param {Object} apiControllers - an object containing all controllers + * @returns {Array} an array containing if the operation succeeded, the message, and the objects + * + * @example + * const allObjects = await utilities.getAllObjects({companies: companyCtl, interactions: interactionCtl, studies: studyCtl}) */ - async downloadBinary(url, directory, filename, showDownloadStatus=false) { - const myFullPath = path.resolve(directory, filename) - const myConfig = { - responseType: "stream", + async getAllObjects(apiControllers) { + let allObjects = { + companies: [], + interactions: [], + studies: [] } - try { - const resp = await axios.get(url, myConfig) - const imageFile = fs.createWriteStream(myFullPath) - const myDownload = resp.data.pipe(imageFile) - const foo = await myDownload.on('finish', () => { - imageFile.close() - if(showDownloadStatus) { - console.log(`SUCCESS: Downloaded [${myFullPath}]`) - } - }) - return myFullPath - } catch (err) { - console.log(`ERROR: Unable to download file due to [${err}]`) + const controllerNames = Object.keys(apiControllers) + for (const controllerName of controllerNames) { + try { + const objs = await apiControllers[controllerName].getAll() + allObjects[controllerName] = objs[2].mrJson + } catch (err) { + console.error(`ERROR: Unable to retrieve all objects from ${controllerName} due to [${err}]`) + return [false, {status_code: 500, status_msg: `ERROR: Unable to retrieve all objects from ${controllerName} due to [${err}]`}, null] + } } + return [true, {status_code: 200, status_msg: `SUCCESS: Retrieved all objects from all controllers.`}, allObjects] } - async getBinary(url, fileName, directory) { - const binaryPath = path.resolve(directory, fileName) - const response = await axios.get(url, { responseType: 'stream' }); - return new Promise((resolve, reject) => { - const fileStream = fs.createWriteStream(binaryPath) - response.data.pipe(fileStream) - - let failed = false - fileStream.on('error', err => { - reject([false, {status_code: 500, status_msg: `FAILED: could not download ${url} to ${binaryPath}`}, err]) - failed = true - }) - - fileStream.on('close', () => { - if (!failed) { - resolve([true, {status_code: 200, status_msg: `SUCCESS: downloaded ${url} to ${binaryPath}`}, binaryPath]) - } else { - resolve([false, {status_code: 500, status_msg: `FAILED: could not download ${url} to ${binaryPath}`}, err]) - } - }) - - }) + /** + * @function getObject + * @description Retrieve a single object from an array of objects + * @param {String} objName - the name of the object to retrieve + * @param {Array} objects - the array of objects to search + * @returns {Array} an array containing if the operation succeeded, the message, and the object + */ + getObject(objName, objects) { + return objects.filter(obj => obj.name === objName) } } -export {Utilities} +export default CLIUtilities diff --git a/src/report/companies.js b/src/report/companies.js index 5a9f560..f4dc8ae 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -13,7 +13,6 @@ import boxPlot from 'box-plot' import Utilities from './helpers.js' import { InteractionSection } from './interactions.js' import { CompanyDashbord } from './dashboard.js' -import { getMostSimilarCompany } from './deprecate_tools.js' import TextWidgets from './widgets/Text.js' import TableWidgets from './widgets/Tables.js' diff --git a/src/report/dashboard.js b/src/report/dashboard.js index 3165071..68cb215 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -14,6 +14,7 @@ import Utilities from './helpers.js' import docxSettings from './settings.js' import Charting from './charts.js' import TableWidgets from './widgets/Tables.js' +import TextWidgets from './widgets/Text.js' class Dashboards { /** @@ -28,6 +29,7 @@ class Dashboards { this.util = new Utilities(env) this.charting = new Charting(env) this.tableWidgets = new TableWidgets(env) + this.textWidgets = new TextWidgets(env) this.themeStyle = docxSettings[env.theme] // Set the theme for the report this.generalStyle = docxSettings.general // Pull in all of the general settings } @@ -104,7 +106,7 @@ class InteractionDashboard extends Dashboards { // Loop through the top 2 topics and create a table row for each topic for (const topic in top2Topics) { myTopics.push( - this.tableWidgets.oneRowTwoColumnsTable( + this.tableWidgets.oneColumnTwoRowsTable( ['Priority Proto-requirement', top2Topics[topic].label] ) ) @@ -112,7 +114,7 @@ class InteractionDashboard extends Dashboards { return myTopics } - async makeDashboard(interaction, company) { + async makeDashboard(interaction, company, isPackage=false) { const rightContents = this.tableWidgets.descriptiveStatisticsTable([ {title: 'Type', value: interaction.interaction_type}, {title: 'Est. reading time (min)', value: interaction.reading_time}, @@ -120,9 +122,23 @@ class InteractionDashboard extends Dashboards { {title: 'Region', value: interaction.region}, {title: 'Proto-requirements', value: Object.keys(interaction.topics).length}, ]) - + let interactionName = interaction.name + let interactionOptions = { includesHyperlink: false } + if (isPackage) { + let myObj = interaction.url.split('/').pop() + console.log(myObj) + // Replace spaces with underscores and keep the file extension + myObj = myObj.replace(/ /g, '_') + console.log(myObj) + interactionName = this.textWidgets.makeExternalHyperLink( + interaction.name, + `./interactions/${myObj}` + ) + interactionOptions = { includesHyperlink: true } + } const interactionNameTable = this.tableWidgets.oneColumnTwoRowsTable( - ['Name', interaction.name] + ['Name', interactionName], + interactionOptions ) const interactionDescriptionTable = this.tableWidgets.oneColumnTwoRowsTable( ['Description', super.shortenText(interaction.description)] diff --git a/src/report/interactions.js b/src/report/interactions.js index 1b91d7c..0bc77fc 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -366,22 +366,23 @@ class InteractionStandalone extends BaseInteractionsReport { * @param {String} creator - A string defining the creator for this document * @param {String} authorCompany - A string containing the company who authored the document */ - constructor(interaction, company, creator, authorCompany, env) { - super([interaction], 'Interaction', interaction.name, env) - this.creator = creator - this.author = authorCompany - this.authorCompany = authorCompany - this.authoredBy = 'Mediumroast for GitHub' - this.title = interaction.name + ' Interaction Report' - this.interaction = interaction - this.company = company - this.description = `An Interaction report summarizing ${interaction.name}.` + constructor(interaction, company, env, allObjects, author='Mediumroast for GitHub') { + super(interaction, 'Interaction', interaction[0].name, env) + this.creator = author + this.author = author + this.authorCompany = author + this.authoredBy = author + this.title = `${interaction[0].name} Interaction Report` + this.interaction = interaction[0] + this.company = company[0] + this.allObjects = allObjects + this.description = `An Interaction report summarizing ${this.interaction.name}.` this.introduction = 'This document was automatically generated by Mediumroast for GitHub.' + ' It includes an abstract, tags and proto-requirement for this Interaction.' + ' If this report is produced as a package, then the' + ' hyperlinks are active and will link to documents on the local folder after the' + ' package is opened.' - this.abstract = interaction.abstract + this.abstract = this.interaction.abstract this.tags = this.interaction.tags this.topics = this.interaction.topics } @@ -440,9 +441,6 @@ class InteractionStandalone extends BaseInteractionsReport { const authoredBy = `Authored by: ${this.authoredBy}` const preparedFor = `${this.objectType}: ` - // Construct the company section - const companySection = new CompanySection(this.company, this.env) - // Construct the dashboard section const myDash = new InteractionDashboard(this.env) @@ -489,7 +487,8 @@ class InteractionStandalone extends BaseInteractionsReport { children: [ await myDash.makeDashboard( this.interaction, - this.company + this.company, + isPackage ) ], }, diff --git a/src/report/widgets/Tables.js b/src/report/widgets/Tables.js index 55c010f..699c41d 100644 --- a/src/report/widgets/Tables.js +++ b/src/report/widgets/Tables.js @@ -388,7 +388,8 @@ class TableWidgets extends Widgets { allRowsBold = false, allBorders = false, bottomBorders = true, - centerFirstRow = false + centerFirstRow = false, + includesHyperlink = false } = options // Desctructure the rows array @@ -409,6 +410,12 @@ class TableWidgets extends Widgets { borderStyle = this.bottomAndRightBorders } + // If the second row is a hyperlink then create a hyperlink object else create a paragraph object + let row2Object = this.textWidgets.makeParagraph(row2, {fontSize: this.generalSettings.tableFontSize, bold: allRowsBold}) + if (includesHyperlink) { + row2Object = new docx.Paragraph({children:[row2]}) + } + // return the row return [ new docx.TableRow({ @@ -436,7 +443,7 @@ class TableWidgets extends Widgets { size: 100, type: docx.WidthType.PERCENTAGE, }, - children: [this.textWidgets.makeParagraph(row2, {fontSize: this.generalSettings.tableFontSize, bold: allRowsBold})], + children: [row2Object], borders: this.bottomAndRightBorders, margins: { bottom: this.generalSettings.tableMargin, @@ -649,8 +656,8 @@ class TableWidgets extends Widgets { }) } - oneColumnTwoRowsTable(rows) { - const tableRows = this.oneColumnTwoRowsBasic(rows) + oneColumnTwoRowsTable(rows, options={}) { + const tableRows = this.oneColumnTwoRowsBasic(rows, options) return new docx.Table({ columnWidths: [95], rows: tableRows, diff --git a/src/report/widgets/Text.js b/src/report/widgets/Text.js index 48192d4..a87b6d6 100644 --- a/src/report/widgets/Text.js +++ b/src/report/widgets/Text.js @@ -260,7 +260,7 @@ class TextWidgets extends Widgets { text: String(text), style: 'Hyperlink', font: this.generalSettings.font, - size: this.fullFontSize, + size: this.generalSettings.fullFontSize, }), ], anchor: link, @@ -268,6 +268,27 @@ class TextWidgets extends Widgets { return myLink } + /** + * @function makeExternalHyperLink + * @description Create an external hyperlink + * @param {String} text - text/prose for the function + * @param {String} link - the URL for the hyperlink + * @returns {Object} a new docx ExternalHyperlink object + */ + makeExternalHyperLink(text, link) { + return new docx.ExternalHyperlink({ + children: [ + new docx.TextRun({ + text: String(text), + style: 'Hyperlink', + font: this.generalSettings.font, + size: this.generalSettings.fullFontSize, + }), + ], + link: link + }) + } + /** * @function makeBookmark * @description Create a target within a document to link to with an internal hyperlink From 7854bda05234b74e7291bc48eb4e881dba1d98d6 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Tue, 27 Aug 2024 20:00:31 -0700 Subject: [PATCH 16/23] Packager works for reports. --- cli/mrcli-company.js | 74 ++++++++++++++++---------- cli/mrcli-interaction.js | 6 +-- src/api/github.js | 105 +++++++++++++++++++++++++++++++------ src/cli/filesystem.js | 16 +++--- src/cli/output.js | 2 +- src/report/companies.js | 1 - src/report/dashboard.js | 3 +- src/report/interactions.js | 16 +++--- 8 files changed, 157 insertions(+), 66 deletions(-) diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 5e6f0a0..01d0fc9 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -12,6 +12,7 @@ import { CompanyStandalone } from '../src/report/companies.js' import { Interactions, Companies, Studies, Users } from '../src/api/gitHubServer.js' import DOCXUtilities from '../src/report/helpers.js' +import CLIUtilities from '../src/cli/common.js' import GitHubFunctions from '../src/api/github.js' import AddCompany from '../src/cli/companyWizard.js' import Environmentals from '../src/cli/env.js' @@ -56,6 +57,9 @@ const processName = 'mrcli-company' // Construct the DOCXUtilities object const docxUtils = new DOCXUtilities(myEnv) +// Construct the CLIUtilities object +const cliUtils = new CLIUtilities() + // Output object const output = new CLIOutput(myEnv, objectType) @@ -69,21 +73,21 @@ const gitHubCtl = new GitHubFunctions(accessToken, myEnv.gitHubOrg, processName) // const studyCtl = new Studies(accessToken, myEnv.gitHubOrg, processName) const userCtl = new Users(accessToken, myEnv.gitHubOrg, processName) -async function fetchData() { - const [intStatus, intMsg, allInteractions] = await interactionCtl.getAll() - const [compStatus, compMsg, allCompanies] = await companyCtl.getAll() - return { allInteractions, allCompanies } -} - // Predefine the results variable let [success, stat, results] = [null, null, null] // Process the cli options // TODO consider moving this out into at least a separate function to make main clean if (myArgs.report) { - // Prepare the data for the report - // const reportData = await _prepareData(myArgs.report) - const { allInteractions, allCompanies } = await fetchData() + // Use CLIUtils to get all objects + const allObjects = await cliUtils.getAllObjects({interactions: interactionCtl, companies: companyCtl}) + if(!allObjects[0]) { + console.error(`ERROR: ${allObjects[1].status_msg}`) + process.exit(-1) + } + const allInteractions = allObjects[2].interactions + const allCompanies = allObjects[2].companies + // Set the root name to be used for file and directory names in case of packaging const baseName = myArgs.report.replace(/ /g, "_") // Set the directory name for the package @@ -93,32 +97,47 @@ if (myArgs.report) { // Set up the document controller const docController = new CompanyStandalone( myArgs.report, - allCompanies.mrJson, - allInteractions.mrJson, + allCompanies, + allInteractions, myEnv ) + let mySpinner = null if (myArgs.package) { + mySpinner = new ora(`Generating report package for [${myArgs.report}] ...`) + mySpinner.start() // Create the working directory const [dir_success, dir_msg, dir_res] = fileSystem.safeMakedir(baseDir + '/interactions') // If the directory creations was successful download the interaction if (dir_success) { fileName = baseDir + '/' + baseName + '_report.docx' - /* - TODO the below only assumes we're storing data in S3, this is intentionally naive. - In the future we will need to be led by the URL string to determine where and what - to download from. Today we only support S3, but this could be Sharepoint, - a local file system, OneDrive, GDrive, etc. There might be an initial less naive - implementation that looks at OneDrive, GDrive, DropBox, etc. as local file system - access points, but the tradeoff would be that caffeine would need to run on a - system with file system access to these objects. - */ - // Append the competitive interactions on the list and download all + // Get the company interactions + let interactions = docController.sourceData.interactions + // Get the competitive interactions + const competitiveInteractions = [ + ...docController.sourceData.competitors.mostSimilar.interactions, + ...docController.sourceData.competitors.leastSimilar.interactions + ] + // Add the competitive interactions to the interactions interactions = [...interactions, ...competitiveInteractions] + // Download the interactions + for (const interaction of interactions) { + let interactionFileName = interaction.url.split('/').pop() + // Replace all spaces with underscores in the file name + interactionFileName = interactionFileName.replace(/ /g, '_') + const downloadResults = await gitHubCtl.readBlob(interaction.url) + if(downloadResults[0]) { + fileSystem.saveTextOrBlobFile(`${baseDir}/interactions/${interactionFileName}`, downloadResults[2]) + } else { + console.error(`ERROR: ${downloadResults[1]}`) + process.exit(-1) + } + } + // TODO: We need to rewrite the logic for obtaining the interactions as they are from GitHub // await s3.s3DownloadObjs(interactions, baseDir + '/interactions', sourceBucket) - null + // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) @@ -128,7 +147,7 @@ if (myArgs.report) { } // Create the document - const [report_success, report_stat, report_result] = await docController.makeDOCX(fileName, myArgs.package) + const [report_success, sourceData] = await docController.makeDOCX(fileName, myArgs.package) // Create the package and cleanup as needed @@ -136,12 +155,14 @@ if (myArgs.report) { const archiver = new ArchivePackage(myEnv.outputDir + '/' + baseName + '.zip') const [package_success, package_stat, package_result] = await archiver.createZIPArchive(baseDir) if (package_success) { - console.log(package_stat) fileSystem.rmDir(baseDir) + mySpinner.stop() + console.log(package_stat) process.exit(0) } else { - console.error(package_stat, -1) fileSystem.rmDir(baseDir) + mySpinner.stop() + console.error(package_stat, -1) process.exit(-1) } @@ -155,9 +176,6 @@ if (myArgs.report) { console.error(report_stat, -1) process.exit(-1) } - // NOTICE: For Now we won't have any ids available for companies, so we'll need to use names - /* } else if (myArgs.find_by_id) { - [success, stat, results] = await companyCtl.findById(myArgs.find_by_id) */ } else if (myArgs.find_by_name) { [success, stat, results] = await companyCtl.findByName(myArgs.find_by_name) // TODO: Need to reimplment the below to account for GitHub diff --git a/cli/mrcli-interaction.js b/cli/mrcli-interaction.js index c873d63..578e3a5 100755 --- a/cli/mrcli-interaction.js +++ b/cli/mrcli-interaction.js @@ -133,12 +133,12 @@ if (myArgs.report) { // If the directory creations was successful download the interaction if(dir_success) { - fileName = baseDir + '/' + baseName + '_report.docx' + fileName = `${baseDir}/${baseName}_report.docx` // Resolve the file name which is in int_results[0].url and it is everything after the last '/' const interactionFileName = interaction[0].url.split('/').pop() - const downloadResults = await gitHubCtl.readBlob(interaction[0].url, baseDir + '/interactions') + const downloadResults = await gitHubCtl.readBlob(interaction[0].url) if(downloadResults[0]) { - fileSystem.saveTextFile(`${baseDir}/interactions/${interactionFileName}`, downloadResults[2]) + fileSystem.saveTextOrBlobFile(`${baseDir}/interactions/${interactionFileName}`, downloadResults[2]) } else { console.error(`ERROR: ${downloadResults[1]}`) process.exit(-1) diff --git a/src/api/github.js b/src/api/github.js index 862e806..3e11855 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -521,30 +521,101 @@ class GitHubFunctions { * @returns {Array} A list containing a boolean indicating success or failure, a status message, and the blob's raw data (or the error message in case of failure). */ async readBlob(fileName) { - + // Encode the file name including files with special characters like question marks + // Custom encoding function to handle special characters + const customEncodeURIComponent = (str) => { + return str.split('').map(char => { + return encodeURIComponent(char).replace(/[!'()*]/g, (c) => { + return '%' + c.charCodeAt(0).toString(16).toUpperCase(); + }); + }).join(''); + } + const originalFileNameEncoded = customEncodeURIComponent(fileName) + + + // Try to download the file from the repository using the download URL + const downloadFile = async (url) => { + try { + const downloadResult = await axios.get(url, { responseType: 'arraybuffer' }) + return [true, downloadResult.data] + } catch (e) { + if (e instanceof TypeError && (e.message.includes('Request path contains unescaped characters') || e.message.includes('ERR_UNESCAPED_CHARACTERS'))) { + // Handle the specific error here + // For example, you can re-encode the URL or log the error + return [false, 'ERR_UNESCAPED_CHARACTERS'] + } + return [false, e] + } + } + + // Re-encode the download URL + const reEncodeDownloadUrl = (url, originalFileName) => { + // Extract the base URL and the file name part + let urlParts = url.split('/'); + const lastPart = urlParts.pop(); // Get the last part of the URL which contains the file name and possibly query parameters + // Remove the last item from the URL parts + urlParts.pop() + + // Find the position of the first question mark that indicates the start of query parameters + const altLastPart = lastPart.split('?') + const queryParams = altLastPart[altLastPart.length - 1] + + // Encode the file name part using encodeURIComponent + // const encodedFileNamePart = encodeURIComponent(fileNamePart); + + // Reconstruct the download URL + return `${urlParts.join('/')}/${originalFileName}${queryParams ? '?' + queryParams : ''}`; + } + + + // Encode the file name and obtain the download URL const encodedFileName = encodeURIComponent(fileName) + + // Set the object URL const objectUrl = `https://api.github.com/repos/${this.orgName}/${this.repoName}/contents/${encodedFileName}` + + // Set the headers const headers = { 'Authorization': `token ${this.token}` } - - try { - const result = await axios.get(objectUrl, { headers }) - const resultJson = result.data - const downloadUrl = resultJson.download_url - const downloadResult = await axios.get(downloadUrl, { responseType: 'arraybuffer' }); - const binFile = downloadResult.data - + + // Obtain the download URL + const result = await axios.get(objectUrl, { headers }) + let downloadUrl = result.data.download_url + + // Attempt to download the file from the repository + let blobData = await downloadFile(downloadUrl) + + // Check if the file was downloaded successfully + if (blobData[0]) { return [ true, - { status_code: 200, status_msg: `read object [${fileName}] from container [${fileName}]` }, - binFile - ] - } catch (e) { - return [ - false, - { status_code: 503, status_msg: `unable to read object [${fileName}] due to [${e}].` }, - e + { status_code: 200, status_msg: `read object [${fileName}]` }, + blobData[1] ] + + // In this case, the error is due to unescaped characters in the URL and we need to re-encode the file name + } else { + // Put an if statement here to check if the error is due to unescaped characters by checking the error message + if (blobData[1] === 'ERR_UNESCAPED_CHARACTERS') { + + downloadUrl = reEncodeDownloadUrl(downloadUrl, originalFileNameEncoded) + + // Try to download the file from the repository again + blobData = await downloadFile(downloadUrl) + if (blobData[0]) { + return [ + true, + { status_code: 200, status_msg: `read object [${fileName}]` }, + blobData[1] + ] + } + } } + return [ + false, + { status_code: 503, status_msg: `unable to read object [${fileName}] due to [${blobData[1]}].` }, + blobData[1] + ] + } // Create a method using the octokit called deleteBlob to delete a file from the repo diff --git a/src/cli/filesystem.js b/src/cli/filesystem.js index 6d084b8..8a0e91e 100644 --- a/src/cli/filesystem.js +++ b/src/cli/filesystem.js @@ -17,15 +17,15 @@ class FilesystemOperators { */ /** - * @function saveTextFile - * @description Save textual data to a file + * @function saveTextOrBlobFile + * @description Save textual or BLOB data to a file * @param {String} fileName - full path to the file and the file name to save to - * @param {String} text - the string content to save to a file which could be JSON, XML, TXT, etc. + * @param {*} data - the content to save to a file which could be JSON, XML, TXT, etc. * @returns {Array} containing the status of the save operation, status message and null/error * * @example * const myFile = new FilesystemOperators() - * const myFileData = await myFile.saveTextFile('myFile.txt', 'This is my file data') + * const myFileData = await myFile.saveTextOrBlobFile('myFile.txt', 'This is my file data') * if(myFileData[0]) { * console.log(myFileData[1]) * } else { @@ -33,13 +33,13 @@ class FilesystemOperators { * } * */ - saveTextFile(fileName, text) { - fs.writeFileSync(fileName, text, err => { + saveTextOrBlobFile(fileName, data) { + fs.writeFileSync(fileName, data, err => { if (err) { - return [false, 'Did not save file [' + fileName + '] because: ' + err, null] + return [false, `Did not save file [${fileName}] because: ${err}`, null] } }) - return [true, 'Saved file [' + fileName + ']', null] + return [true, `Saved file [${fileName}]`, null] } /** diff --git a/src/cli/output.js b/src/cli/output.js index a56d721..fab2696 100644 --- a/src/cli/output.js +++ b/src/cli/output.js @@ -205,7 +205,7 @@ class CLIOutput { const csvParser = new Parser() try { const csv = csvParser.parse(objects) - this.fileSystem.saveTextFile(myFile, csv) + this.fileSystem.saveTextOrBlobFile(myFile, csv) return [true, {status_code: 200, status_msg: `wrote [${this.objectType}] objects to [${myFile}]`}, null] } catch (err) { return [false, {}, err] diff --git a/src/report/companies.js b/src/report/companies.js index f4dc8ae..73b7ed3 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -20,7 +20,6 @@ class BaseCompanyReport { constructor(companyName, companies, interactions, env) { this.util = new Utilities(env) const sourceData = this.util.initializeCompanyData(companyName, companies, interactions) - // console.log('sourceData>>>', sourceData) this.sourceData = sourceData this.company = sourceData.company this.companyName = companyName diff --git a/src/report/dashboard.js b/src/report/dashboard.js index 68cb215..5b6c205 100644 --- a/src/report/dashboard.js +++ b/src/report/dashboard.js @@ -126,10 +126,9 @@ class InteractionDashboard extends Dashboards { let interactionOptions = { includesHyperlink: false } if (isPackage) { let myObj = interaction.url.split('/').pop() - console.log(myObj) // Replace spaces with underscores and keep the file extension + // NOTE: This may not work, need to text on other interactions myObj = myObj.replace(/ /g, '_') - console.log(myObj) interactionName = this.textWidgets.makeExternalHyperLink( interaction.name, `./interactions/${myObj}` diff --git a/src/report/interactions.js b/src/report/interactions.js index 0bc77fc..937235d 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -269,17 +269,21 @@ class InteractionSection extends BaseInteractionsReport { // isPackage version of the strip // Create the link to the underlying interaction document // TODO consider making this a hyperlink to the interaction document in GitHub - const myObj = this.interactions[interaction].url.split('/').pop() - let interactionLink = this.util.makeExternalHyperLink( + // creation_date is of this format 2024-05-24T12:29:22.053Z, create a substring of the date only + const myDate = this.interactions[interaction].creation_date.substring(0, 10) + let myObj = this.interactions[interaction].url.split('/').pop() + // Replace spaces with underscores + myObj = myObj.replace(/ /g, '_') + // NOTE: Need to follow how this was done in tables with the two column that includes a hyperlink + let interactionLink = this.textWidgets.makeExternalHyperLink( 'Document', `./interactions/${myObj}` ) - metadataRow = this.tableWidgets.fourColumnRowBasic( + metadataRow = this.tableWidgets.threeColumnRowBasic( [ - interactionLink, - `Created on: ${this.interactions[interaction].creation_date}`, + `Created on: ${myDate}`, `Est. reading time: ${this.interactions[interaction].reading_time} min`, - descriptionsLink + new docx.Paragraph({children:[interactionLink]}) ], {firstColumnBold: false} ) From 478012d74c1ca5926f03d091a54988e764f9fd03 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sat, 31 Aug 2024 20:43:43 -0700 Subject: [PATCH 17/23] Implemented PAT, updated token verifier, cleanups --- .gitignore | 5 +- cli/mrcli-actions.js | 13 +- cli/mrcli-billing.js | 5 - cli/mrcli-company.js | 11 +- cli/mrcli-interaction.js | 13 +- cli/mrcli-storage.js | 16 +- cli/mrcli-user.js | 13 +- package-lock.json | 5 +- src/api/authorize.js | 345 ++++++++++++++------------------------- src/api/github.js | 24 +++ src/cli/env.js | 115 ++++++------- src/report/tools.js | 74 +++++++++ 12 files changed, 338 insertions(+), 301 deletions(-) create mode 100644 src/report/tools.js diff --git a/.gitignore b/.gitignore index 4a8aebb..2c7b395 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules/ .DS_Store *.py -test.js +test*.* companies.json *.pdf -sample_pdf/ \ No newline at end of file +sample_pdf/ +foo*.* \ No newline at end of file diff --git a/cli/mrcli-actions.js b/cli/mrcli-actions.js index a4d2b77..a60a30b 100644 --- a/cli/mrcli-actions.js +++ b/cli/mrcli-actions.js @@ -15,13 +15,14 @@ import Environmentals from '../src/cli/env.js' import CLIOutput from '../src/cli/output.js' import ora from "ora" import chalk from 'chalk' +import { GitHubAuth } from '../src/api/authorize.js' // Related object type const objectType = 'Actions' // Environmentals object const environment = new Environmentals( - '1.0.0', + '1.1.0', `${objectType}`, `Command line interface to report on and update actions.`, objectType @@ -69,7 +70,15 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) -const accessToken = await environment.verifyAccessToken() +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const verifiedToken = await myAuth.verifyAccessToken() +let accessToken = null +if (!verifiedToken[0]) { + console.error(`ERROR: ${verifiedToken[1].status_msg}`) + process.exit(-1) +} else { + accessToken = verifiedToken[2].token +} const processName = 'mrcli-actions' // Construct the controller objects diff --git a/cli/mrcli-billing.js b/cli/mrcli-billing.js index 75787ee..41d2db3 100644 --- a/cli/mrcli-billing.js +++ b/cli/mrcli-billing.js @@ -12,11 +12,6 @@ import chalk from 'chalk' console.log(chalk.bold.yellow('WARNING: The billing subcommand is deprecated in this release, and replaced by actions and storage subcommands,\n\ttry \'mrcli actions --help\' or \'mrcli storage --help\' for more information.')) process.exit() -// Import required modules -// import { Billings } from '../src/api/gitHubServer.js' -// import Environmentals from '../src/cli/env.js' -// import CLIOutput from '../src/cli/output.js' - // Related object type diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 01d0fc9..5711aa2 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -16,6 +16,7 @@ import CLIUtilities from '../src/cli/common.js' import GitHubFunctions from '../src/api/github.js' import AddCompany from '../src/cli/companyWizard.js' import Environmentals from '../src/cli/env.js' +import { GitHubAuth } from '../src/api/authorize.js' import CLIOutput from '../src/cli/output.js' import FilesystemOperators from '../src/cli/filesystem.js' import ArchivePackage from '../src/cli/archive.js' @@ -51,7 +52,15 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) myEnv.company = 'Unknown' -const accessToken = await environment.verifyAccessToken() +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const verifiedToken = await myAuth.verifyAccessToken() +let accessToken = null +if (!verifiedToken[0]) { + console.error(`ERROR: ${verifiedToken[1].status_msg}`) + process.exit(-1) +} else { + accessToken = verifiedToken[2].token +} const processName = 'mrcli-company' // Construct the DOCXUtilities object diff --git a/cli/mrcli-interaction.js b/cli/mrcli-interaction.js index 578e3a5..249fe12 100755 --- a/cli/mrcli-interaction.js +++ b/cli/mrcli-interaction.js @@ -23,6 +23,7 @@ import FilesystemOperators from '../src/cli/filesystem.js' import ArchivePackage from '../src/cli/archive.js' import ora from 'ora' import WizardUtils from "../src/cli/commonWizard.js" +import { GitHubAuth } from '../src/api/authorize.js' // Reset the status of objects for caffiene reprocessing async function resetStatuses(interactionType, interactionCtl, objStatus=0) { @@ -58,7 +59,7 @@ const objectType = 'Interactions' // Environmentals object const environment = new Environmentals( - '3.2.0', + '3.3.0', `${objectType}`, `Command line interface for mediumroast.io ${objectType} objects.`, objectType @@ -71,7 +72,15 @@ const fileSystem = new FilesystemOperators() const myArgs = environment.parseCLIArgs() const myConfig = environment.readConfig(myArgs.conf_file) const myEnv = environment.getEnv(myArgs, myConfig) -const accessToken = await environment.verifyAccessToken() +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const verifiedToken = await myAuth.verifyAccessToken() +let accessToken = null +if (!verifiedToken[0]) { + console.error(`ERROR: ${verifiedToken[1].status_msg}`) + process.exit(-1) +} else { + accessToken = verifiedToken[2].token +} const processName = 'mrcli-interaction' // Output object diff --git a/cli/mrcli-storage.js b/cli/mrcli-storage.js index 20c0271..2d5a62a 100644 --- a/cli/mrcli-storage.js +++ b/cli/mrcli-storage.js @@ -13,15 +13,15 @@ import { Storage } from '../src/api/gitHubServer.js' import Environmentals from '../src/cli/env.js' import CLIOutput from '../src/cli/output.js' -import ora from "ora" -import chalk from 'chalk' +import { GitHubAuth } from '../src/api/authorize.js' + // Related object type const objectType = 'Storage' // Environmentals object const environment = new Environmentals( - '1.0.0', + '1.1.0', `${objectType}`, `Command line interface to report on and update actions.`, objectType @@ -68,7 +68,15 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) -const accessToken = await environment.verifyAccessToken() +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const verifiedToken = await myAuth.verifyAccessToken() +let accessToken = null +if (!verifiedToken[0]) { + console.error(`ERROR: ${verifiedToken[1].status_msg}`) + process.exit(-1) +} else { + accessToken = verifiedToken[2].token +} const processName = 'mrcli-storage' // Construct the controller objects diff --git a/cli/mrcli-user.js b/cli/mrcli-user.js index c2632b9..0a6fde8 100755 --- a/cli/mrcli-user.js +++ b/cli/mrcli-user.js @@ -13,13 +13,14 @@ import { Users } from '../src/api/gitHubServer.js' import Environmentals from '../src/cli/env.js' import CLIOutput from '../src/cli/output.js' +import { GitHubAuth } from '../src/api/authorize.js' // Related object type const objectType = 'Users' // Environmentals object const environment = new Environmentals( - '2.0', + '2.1.0', `${objectType}`, `Command line interface for mediumroast.io ${objectType} objects.`, objectType @@ -63,7 +64,15 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) -const accessToken = await environment.verifyAccessToken() +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const verifiedToken = await myAuth.verifyAccessToken() +let accessToken = null +if (!verifiedToken[0]) { + console.error(`ERROR: ${verifiedToken[1].status_msg}`) + process.exit(-1) +} else { + accessToken = verifiedToken[2].token +} const processName = 'mrcli-user' // Output object diff --git a/package-lock.json b/package-lock.json index ef6d69d..4808fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediumroast_js", - "version": "0.7.15", + "version": "0.7.00.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mediumroast_js", - "version": "0.7.15", + "version": "0.7.00.42", "license": "Apache-2.0", "dependencies": { "@json2csv/plainjs": "^7.0.4", @@ -36,6 +36,7 @@ "mrcli-company": "cli/mrcli-company.js", "mrcli-interaction": "cli/mrcli-interaction.js", "mrcli-setup": "cli/mrcli-setup.js", + "mrcli-storage": "cli/mrcli-storage.js", "mrcli-study": "cli/mrcli-study.js", "mrcli-user": "cli/mrcli-user.js" }, diff --git a/src/api/authorize.js b/src/api/authorize.js index 65daa38..46b06ef 100644 --- a/src/api/authorize.js +++ b/src/api/authorize.js @@ -1,7 +1,7 @@ /** * @fileoverview This file contains the code to authorize the user to the GitHub API * @license Apache-2.0 - * @version 1.0.0 + * @version 2.0.0 * * @author Michael Hay * @file authorize.js @@ -11,270 +11,90 @@ * @classdesc This class is used to authorize the user to the GitHub API * * @requires axios - * @requires crypto * @requires open * @requires octoDevAuth * @requires chalk * @requires cli-table + * @requires configparser + * @requires FilesystemOperators * * @exports GitHubAuth * * @example * import {GitHubAuth} from './api/authorize.js' - * const github = new GitHubAuth() - * const githubToken = github.getAccessToken(env) + * const github = new GitHubAuth(env, environ, configFile) + * const githubToken = github.verifyAccessToken() * */ -import axios from "axios" -import crypto from "node:crypto" import open from "open" import * as octoDevAuth from '@octokit/auth-oauth-device' import chalk from "chalk" import Table from 'cli-table' +import FilesystemOperators from '../cli/filesystem.js' -class Auth0Auth { - constructor(domain, contentType, clientId, callbackUrl, state, scope) { - this.domain = domain ? domain : 'dev-tfmnyye458bzcq0u.us.auth0.com' - this.codePath = '/oauth/device/code' - this.tokenPath = '/oauth/token' - this.callbackUrl = callbackUrl ? callbackUrl : 'https://app.mediumroast.io' - // this.audience = 'https://app.mediumroast.io/app' - this.audience = 'mediumroast-endpoint' - this.state = state ? state : 'mrCLIstate' - this.scope = scope ? scope : 'companies:read' - this.algorithm = 'S256' - this.contentType = contentType ? contentType : 'application/x-www-form-urlencoded' - // this.clientId = clientId ? clientId : 'sDflkHs3V3sg0QaZnrLEkuinXnTftkKk' - this.clientId = clientId ? clientId : '0ZhDegyCotxYL8Ov9Cj4K7Z0MugtgaY0' - // NOTE: Only a native app can do PKCE, question: can the native app authenticate to the API? - // https://dev-tfmnyye458bzcq0u.us.auth0.com/oauth/device/code - } - - _base64URLEncode(str) { - return str.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, '') - } - - createCodeVerifier (bytesLength=32) { - const randString = crypto.randomBytes(bytesLength) - return this._base64URLEncode(randString) - } - - createChallengeCode(codeVerifier) { - const codeVerifierHash = crypto.createHash('sha256').update(codeVerifier).digest() - return this._base64URLEncode(codeVerifierHash) - } - - async getDeviceCode() { - const options = { - method: 'POST', - url: `https://${this.domain}${this.codePath}`, - headers: { - 'content-type': this.contentType - }, - data: new URLSearchParams({ - client_id: this.clientId, - scope: this.scope, - audience: this.audience - }) - } - let authorized - try { - authorized = await axios.request(options) - return [true, authorized.data] - } catch (err) { - return [false, err] - } - } - - async openPKCEUrl(config) { - // Construct the URL to build the client challenge - const pkceUrl = `https://${this.domain}/authorize?` + - `response_type=code&` + - `code_challenge=${config.challenge_code}&` + - `code_challenge_method=${this.algorithm}&` + - `client_id=${this.clientId}&` + - `redirect_uri=${this.callbackUrl}&` + - `scope='openid%20profile'&` + - `state=${this.state}` - - console.log(`URL>>> [${pkceUrl}]`) - // Call the browser - const myCmd = await open(pkceUrl) - } - - async authorizeClient(authorizationCode, codeVerifier) { - const options = { - method: 'POST', - url: `https://${this.domain}${this.codePath}`, - headers: { - 'content-type': this.contentType - }, - data: new URLSearchParams({ - // grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - grant_type: 'authorization_code', - client_id: this.clientId, - code_verifier: codeVerifier, - code: authorizationCode, - redirect_uri: this.callbackUrl, - }) - } - let authorized - try { - authorized = await axios.request(options) - return [true, authorized.data] - } catch (err) { - return [false, err] - } - } - - async verifyClientAuth (verificationUri) { - const myCmd = await open(verificationUri) - return [true, null] - } - - async getTokens(authorizationCode, codeVerifier) { - const options = { - method: 'POST', - url: `https://${this.domain}${this.tokenPath}`, - headers: { - 'content-type': this.contentType - }, - data: new URLSearchParams({ - // grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - grant_type: 'authorization_code', - client_id: this.clientId, - code_verifier: codeVerifier, - code: authorizationCode, - redirect_uri: this.callbackUrl - }) - } - let tokens - try { - tokens = await axios.request(options) - return [true, tokens.data] - } catch (err) { - return [false, err] - } +class GitHubAuth { + constructor (env, environ, configFile) { + this.env = env + this.clientType = 'github-app' + this.configFile = configFile + this.environ = environ + this.config = environ.readConfig(configFile) + this.filesystem = new FilesystemOperators() } - async getTokensDeviceCode(deviceCode) { - const options = { - method: 'POST', - url: `https://${this.domain}${this.tokenPath}`, - headers: { - 'content-type': this.contentType - }, - data: new URLSearchParams({ - grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - client_id: this.clientId, - device_code: deviceCode - }) - } - let tokens - try { - tokens = await axios.request(options) - return [true, tokens.data] - } catch (err) { - return [false, err] - } + verifyGitHubSection () { + return this.config.hasSection('GitHub') } - - - login(env) { - const token = `${env.DEFAULT.token_type} ${env.DEFAULT.access_token}` - return { - apiKey: token, - restServer: env.DEFAULT.mr_erver, - tokenType: env.DEFAULT.token_type, - user: `${env.DEFAULT.first_name}<${env.DEFAULT.email_address}>` + _getFromConfig (section, option) { + const hasOption = this.config.hasKey(section, option) + if (hasOption) { + return this.config.get(section, option) + } else { + return null } } - - logout() { - return true + getAccessTokenFromConfig () { + return this._getFromConfig('GitHub', 'token') } - decodeJWT (token) { - if(token !== null || token !== undefined){ - const base64String = token.split('.')[1] - const decodedValue = JSON.parse( - Buffer.from( - base64String, - 'base64') - .toString('ascii') - ) - return decodedValue - } - return null + getAuthTypeFromConfig () { + return this._getFromConfig('GitHub', 'authType') } - -} -class GitHubAuth { - constructor (env) { - this.env = env - } - - async _checkTokenExpiration(token) { - const response = await fetch('https://api.github.com/applications/:client_id/token', { - method: 'POST', + async checkTokenExpiration(token) { + const response = await fetch('https://api.github.com/user', { + method: 'GET', headers: { - 'Authorization': `Basic ${Buffer.from(`client_id:client_secret`).toString('base64')}`, - 'Content-Type': 'application/json', + 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json' - }, - body: JSON.stringify({ - access_token: token - }) + } }) if (!response.ok) { - return [false, {status_cde: 500, status_msg: response.statusText}, null] + return [false, {status_code: 500, status_msg: response.statusText}, null] } const data = await response.json() - return [true, {status_cde: 200, status_msg: response.statusText}, data] - } - - getAccessTokenPat(defaultExpiryDays = 30) { - // Read the PAT from the file - const pat = fs.readFileSync(this.secretFile, 'utf8').trim() - - // Check to see if the token remains valid - const isTokenValid = this._checkTokenExpiration(pat) - - if (!isTokenValid[0]) { - return isTokenValid - } - - return { - token: pat, - auth_type: 'pat' - } - } - + return [true, {status_code: 200, status_msg: response.statusText}, data] + } /** - * - * @param {Object} env - The environment object constructed from the configuration file - * @param {String} clientType - The type of client, either 'github-app' or 'github' + * @async + * @function getAccessTokenDeviceFlow + * @description Get the access token using the device flow * @returns {Object} The access token object */ - async getAccessToken(env, clientType='github-app') { - + async getAccessTokenDeviceFlow() { // Construct the oAuth device flow object which starts the browser let deviceCode // Provide a place for the device code to be captured const deviceauth = octoDevAuth.createOAuthDeviceAuth({ - clientType: clientType, - clientId: env.clientId, + clientType: this.clientType, + clientId: this.env.clientId, onVerification(verifier) { deviceCode = verifier.device_code // Print the verification artifact to the console @@ -296,14 +116,89 @@ class GitHubAuth { let accessToken = await deviceauth({type: 'oauth'}) // NOTE: The token is not returned with the expires_in and expires_at fields, this is a workaround - let now = new Date() - now.setHours(now.getHours() + 8) - accessToken.expiresAt = now.toUTCString() + // let now = new Date() + // now.setHours(now.getHours() + 8) + // accessToken.expiresAt = now.toUTCString() // Add the device code to the accessToken object accessToken.deviceCode = deviceCode return accessToken } - + + /** + * @async + * @function verifyAccessToken + * @description Verify if the access token is valid and if not get a new one depending on this.env.authType + * @param {Boolean} saveToConfig - Save to the configuration file, default is true + */ + async verifyAccessToken (saveToConfig=true) { + // Get key variables from the config file + const hasGitHubSection = this.verifyGitHubSection() + // If the GitHub section is not available, then the token is not available, return false. + // This is only to be used when called from a function that intendes to setup the configuration file, but + // just in case this condition occurs we want to return clearly to the caller. + if (!hasGitHubSection) { + return [false, {status_code: 500, status_msg: 'The GitHub section is not available in the configuration file'}, null] + } + + // Get the access token and authType from the config file since the section is available + let accessToken = this.getAccessTokenFromConfig() + const authType = this.getAuthTypeFromConfig() + + // Check to see if the token is valid + const validToken = await this.checkTokenExpiration(accessToken) + if (validToken[0]) { + return [ + true, + {status_code: 200, status_msg: validToken[1].status_msg}, + {token: accessToken, authType: authType} + ] + // If the token is not valid, then we need to return to the caller (PAT) or get a new token (deviceFlow) + } else { + // Case for a Personal Access Token + if (authType === 'pat') { + // Return the error message to the caller + return [ + false, + {status_code: 500, status_msg: `The Personal Access Token appears to be invalid and was rejected with an error message [${validToken[1].status_msg}].\n\tPlease obtain a new PAT and update the GitHub token setting in the configuration file [${this.configFile}].`}, + null + ] + // Case for the device flow + } else if (authType === 'deviceFlow') { + // Get the new access token + accessToken = await this.getAccessTokenDeviceFlow() + // Update the config + let tmpConfig = this.environ.updateConfigSetting(this.config, 'GitHub', 'token', accessToken.token) + tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'authType', authType) + tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'deviceCode', accessToken.deviceCode) + + // Save the config file if needed + this.config = tmpConfig[1] + if (saveToConfig) { + await this.config.write(this.configFile) + } + + return [ + true, + {status_code: 200, status_msg: `The access token has been successfully updated and saved to the configuration file [${this.configFile}]`}, + {token: accessToken.token, authType: authType, deviceCode: accessToken.deviceCode} + ] + } + } + } + + decodeJWT (token) { + if(token !== null || token !== undefined){ + const base64String = token.split('.')[1] + const decodedValue = JSON.parse( + Buffer.from( + base64String, + 'base64') + .toString('ascii') + ) + return decodedValue + } + return null + } } -export {Auth0Auth, GitHubAuth} \ No newline at end of file +export {GitHubAuth} \ No newline at end of file diff --git a/src/api/github.js b/src/api/github.js index 3e11855..4322596 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -618,6 +618,30 @@ class GitHubFunctions { } + // async readBlob(fileName) { + // // Encode the file name and obtain the download URL + // const encodedFileName = encodeURIComponent(fileName) + + // // Set the object URL + // const objectUrl = `https://api.github.com/repos/${this.orgName}/${this.repoName}/contents/${encodedFileName}` + + // // Set the headers + // const headers = { 'Authorization': `token ${this.token}` } + + // // Obtain the download URL + // const result = await axios.get(objectUrl, { headers }) + // console.log(result) + // let downloadUrl = result.data.download_url + // // try { + // const downloadResult = await axios.get(downloadUrl, { responseType: 'arraybuffer' }) + // // console.log(downloadResult) + // return [true, downloadResult, downloadUrl] + // // } catch (e) { + // // console.log(e) + // // return [false, e, downloadUrl] + // // } + // } + // Create a method using the octokit called deleteBlob to delete a file from the repo async deleteBlob(containerName, fileName, branchName, sha) { // Using the github API delete a file from the container diff --git a/src/cli/env.js b/src/cli/env.js index d5bfdc0..5744551 100644 --- a/src/cli/env.js +++ b/src/cli/env.js @@ -4,7 +4,7 @@ * @file env.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 2.3.1 + * @version 2.4.0 */ // Import required modules @@ -281,6 +281,7 @@ class Environmentals { env.deviceCodeUrl = config.get('GitHub', 'deviceCodeUrl') env.accessTokenUrl = config.get('GitHub', 'accessTokenUrl') env.gitHubOrg = config.get('GitHub', 'org') + env.authType = config.get('GitHub', 'authType') env.deviceCode = config.get('GitHub', 'deviceCode') // Setup options with cli settings only @@ -295,68 +296,70 @@ class Environmentals { * @description Verify the access token is valid and if not get a new one * @todo there is a bug in the persistence of the expiresAt value in the config file */ - async verifyAccessToken (fromSetup=false) { - // Get configuration information from the config file - const configFile = this.checkConfigDir() - let env = this.readConfig(configFile) + // async verifyAccessToken (fromSetup=false) { + // // Get configuration information from the config file + // const configFile = this.checkConfigDir() + // let env = this.readConfig(configFile) - // Construct the GitHub authoirzation object - const githubAuth = new GitHubAuth() + // // Construct the GitHub authoirzation object + // const githubAuth = new GitHubAuth({clientId: env.get('GitHub', 'clientId'), authType: env.get('GitHub', 'authType')}) - // Define needed variables to housekeep the accessToken and the config file - let accessToken = env.get('GitHub', 'token') - let updateConfig = false - let deviceCode - let expiresAt + // // Define needed variables to housekeep the accessToken and the config file + // let authType = env.get('GitHub', 'authType') + // let accessToken = env.get('GitHub', 'token') + // let updateConfig = false + // let deviceCode + // let expiresAt - // Check to see if the GitHub section is available - if (env.hasSection('GitHub')) { - // Check to see if the expiration date is available - env.hasKey('GitHub', 'expiresAt') ? expiresAt = env.get('GitHub', 'expiresAt') : expiresAt = 0 + // // Check to see if the GitHub section is available + // if (env.hasSection('GitHub')) { + // // Check to see if the expiration date is available + // env.hasKey('GitHub', 'expiresAt') ? expiresAt = env.get('GitHub', 'expiresAt') : expiresAt = 0 - // Convert the access token expirations into Date objects - let accessExpiry - expiresAt === 'undefined' ? accessExpiry = 0 : accessExpiry = new Date(expiresAt) - const now = new Date() + // // Convert the access token expirations into Date objects + // let accessExpiry + // expiresAt === 'undefined' ? accessExpiry = 0 : accessExpiry = new Date(expiresAt) + // const now = new Date() - // Check to see if the access token is valid - if (accessExpiry < now) { - const myEnv = { - clientId: env.get('GitHub', 'clientId'), - clientType: env.get('GitHub', 'clientType') - } - accessToken = await githubAuth.getAccessToken(myEnv) - env = this.updateConfigSetting(env, 'GitHub', 'token', accessToken.token) - env = this.updateConfigSetting(env[1], 'GitHub', 'expiresAt', accessToken.expiresAt) - env = this.updateConfigSetting(env[1], 'GitHub', 'deviceCode', accessToken.deviceCode) - updateConfig = true - env = env[1] - accessToken = accessToken.token - expiresAt = accessToken.expiresAt - deviceCode = accessToken.deviceCode - } - } else { - // Section GitHub not available perform complete authorization flow - // Get the access token and add a GitHub section to the env - accessToken = await githubAuth.getAccessToken(env) - // Create the GitHub section - env = this.addConfigSection(env, 'GitHub', accessToken) - env = this.removeConfigSetting(env[1], 'GitHub', 'contentType') - env = this.removeConfigSetting(env[1], 'GitHub', 'grantType') - updateConfig = true - env = env[1] - } + // // Check to see if the access token is valid + // if (accessExpiry < now) { + // const myEnv = { + // clientId: env.get('GitHub', 'clientId'), + // clientType: env.get('GitHub', 'clientType') + // } - // Save the config file if needed - if (updateConfig) { - await env.write(configFile) - } + // accessToken = await githubAuth.getAccessTokenDeviceFlow(myEnv) + // env = this.updateConfigSetting(env, 'GitHub', 'token', accessToken.token) + // env = this.updateConfigSetting(env[1], 'GitHub', 'expiresAt', accessToken.expiresAt) + // env = this.updateConfigSetting(env[1], 'GitHub', 'deviceCode', accessToken.deviceCode) + // updateConfig = true + // env = env[1] + // accessToken = accessToken.token + // expiresAt = accessToken.expiresAt + // deviceCode = accessToken.deviceCode + // } + // } else { + // // Section GitHub not available perform complete authorization flow + // // Get the access token and add a GitHub section to the env + // accessToken = await githubAuth.getAccessTokenDeviceFlow(env) + // // Create the GitHub section + // env = this.addConfigSection(env, 'GitHub', accessToken) + // env = this.removeConfigSetting(env[1], 'GitHub', 'contentType') + // env = this.removeConfigSetting(env[1], 'GitHub', 'grantType') + // updateConfig = true + // env = env[1] + // } - if (fromSetup) { - return {token: accessToken, expiry: expiresAt ,device: deviceCode} - } - return accessToken - } + // // Save the config file if needed + // if (updateConfig) { + // await env.write(configFile) + // } + + // if (fromSetup) { + // return {token: accessToken, expiry: expiresAt ,device: deviceCode} + // } + // return accessToken + // } } export default Environmentals \ No newline at end of file diff --git a/src/report/tools.js b/src/report/tools.js new file mode 100644 index 0000000..826835b --- /dev/null +++ b/src/report/tools.js @@ -0,0 +1,74 @@ +/** + * Common functions and tools used for report processing + * @author Michael Hay + * @file tools.js + * @copyright 2023 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 1.0.0 + */ + +import FilesystemOperators from '../cli/filesystem.js' + +/** + * @function getMostSimilarCompany + * @description Find the closest competitor using the Euclidean distance + * @param {Object} similarities - the similarities from a company object + * @returns {Object} An array containing a section description and a table of interaction descriptions + */ +export function getMostSimilarCompany(similarities, companies) { + // x1 = 1 and y1 = 1 because this the equivalent of comparing a company to itself + const x1 = 1 + const y1 = 1 + let distanceToCompany = {} + let companyToDistance = {} + for(const companyName in similarities) { + // Compute the distance using d = sqrt((x2 - x1)^2 + (y2 - y1)^2) + const myDistance = Math.sqrt( + (similarities[companyName].most_similar.score - x1) ** 2 + + (similarities[companyName].least_similar.score - y1) ** 2 + ) + distanceToCompany[myDistance] = companyName + companyToDistance[companyName] = myDistance + } + // Obtain the closest company using max, note min returns the least similar + const mostSimilarId = distanceToCompany[Math.max(...Object.keys(distanceToCompany))] + // Get the id for the most similar company + const mostSimilarCompany = companies.filter(company => { + if (parseInt(company.name) === parseInt(mostSimilarId)) { + return company + } + }) + // Transform the strings into floats prior to return + const allDistances = Object.keys(distanceToCompany).map ( + (distance) => { + return parseFloat(distance) + } + ) + // return both the most similar id and all computed distanceToCompany + return {mostSimilarCompany: mostSimilarCompany[0], distances: allDistances, companyMap: companyToDistance} +} + + +/** + * @function initWorkingDirs + * @description Prepare working directories for report and package creation + * @param {String} baseDir - full path to the directory to initialize with key working directories + */ +export function initWorkingDirs(baseDir) { + const fileSystem = new FilesystemOperators() + const subdirs = ['interactions', 'images'] + for(const myDir in subdirs) { + fileSystem.safeMakedir(baseDir + '/' + subdirs[myDir]) + } +} + +/** + * @function cleanWorkingDirs + * @description Clean up working directories after report and/or package creation + * @param {String} baseDir - full path to the directory to initialize with key working directories + * @returns {Array} containing the status of the rmdir operation, status message and null + */ +export function cleanWorkingDirs(baseDir) { + const fileSystem = new FilesystemOperators() + return fileSystem.rmDir(baseDir) +} \ No newline at end of file From 68f95fe13ebc44a8b5019103f6c7e5f229ec09eb Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 1 Sep 2024 10:29:30 -0700 Subject: [PATCH 18/23] PAT coded in setup, cleanups, ready for install test --- cli/mrcli-actions.js | 14 +++--- cli/mrcli-company.js | 11 +++-- cli/mrcli-interaction.js | 12 +++-- cli/mrcli-setup.js | 103 ++++++++++++++++++++++++++++++--------- cli/mrcli-storage.js | 15 +++--- cli/mrcli-study.js | 14 +++--- cli/mrcli-user.js | 12 +++-- cli/mrcli.js | 10 ++-- package.json | 3 +- src/api/authorize.js | 6 +++ src/api/gitHubServer.js | 10 +++- src/api/github.js | 1 + src/cli/archive.js | 26 +++++++--- src/cli/common.js | 5 -- src/cli/env.js | 97 +++++++++--------------------------- 15 files changed, 191 insertions(+), 148 deletions(-) diff --git a/cli/mrcli-actions.js b/cli/mrcli-actions.js index a60a30b..54e71a1 100644 --- a/cli/mrcli-actions.js +++ b/cli/mrcli-actions.js @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * A CLI utility used for accessing and reporting on mediumroast.io user objects + * @fileoverview A CLI utility to report on and update Mediumroast for GitHub Actions/Workflows + * @license Apache-2.0 + * @version 1.1.1 + * * @author Michael Hay - * @file actions.js + * @file mrcli-actions.js * @copyright 2024 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @verstion 1.0.0 + * */ // Import required modules @@ -22,9 +24,9 @@ const objectType = 'Actions' // Environmentals object const environment = new Environmentals( - '1.1.0', + '1.1.1', `${objectType}`, - `Command line interface to report on and update actions.`, + `A CLI utility to report on an update GitHub Actions/Workflows Mediumroast for GitHub`, objectType ) diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 5711aa2..05158b8 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -1,11 +1,14 @@ #!/usr/bin/env node /** - * A CLI utility used for accessing and reporting on mediumroast.io company objects + * @fileoverview A CLI utility to manage and report on Mediumroast for GitHub Company objects + * @license Apache-2.0 + * @version 3.2.0 + * * @author Michael Hay - * @file company.js + * @file mrcli-company.js * @copyright 2024 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 + * */ // Import required modules @@ -31,7 +34,7 @@ const objectType = 'Companies' const environment = new Environmentals( '3.2.0', `${objectType}`, - `Command line interface for mediumroast.io ${objectType} objects.`, + `A CLI utility to manage and report on Mediumroast for GitHub Company objects`, objectType ) diff --git a/cli/mrcli-interaction.js b/cli/mrcli-interaction.js index 249fe12..3ed6355 100755 --- a/cli/mrcli-interaction.js +++ b/cli/mrcli-interaction.js @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * A CLI utility used for accessing and reporting on mediumroast.io interaction objects + * @fileoverview A CLI utility to manage and report on Mediumroast for GitHub Interaction objects + * @license Apache-2.0 + * @version 3.3.0 + * * @author Michael Hay - * @file interactions.js + * @file mrcli-interaction.js * @copyright 2024 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @version 3.2.0 + * */ @@ -61,7 +63,7 @@ const objectType = 'Interactions' const environment = new Environmentals( '3.3.0', `${objectType}`, - `Command line interface for mediumroast.io ${objectType} objects.`, + `A CLI utility to manage and report on Mediumroast for GitHub Interaction objects`, objectType ) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index 4d6800f..aebcc31 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * A CLI utility to setup the configuration file to talk to the mediumroast.io - * @author Michael Hay - * @file mr_setup.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. + * @fileoverview A CLI utility to perform initial configuration and setup of Mediumroast for GitHub * @license Apache-2.0 - * @version 3.0.0 + * @version 3.1.0 + * + * @author Michael Hay + * @file mrcli-setup.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * */ // Import required modules @@ -255,17 +257,14 @@ async function installActions(actionsManifest) { ----------------------------------------------------------------------- */ // Global variables -const VERSION = '3.0.0' +const VERSION = '3.1.0' const NAME = 'setup' -const DESC = 'Set up the Mediumroast application.' +const DESC = 'A CLI utility to perform initial configuration and setup of Mediumroast for GitHub' const defaultConfigFile = `${process.env.HOME}/.mediumroast/config.ini` // Construct the file system utility object const fsUtils = new FilesystemOperators() -// Construct the authorization object -const githubAuth = new GitHubAuth() - // Parse the commandline arguements const myArgs = parseCLIArgs(NAME, VERSION, DESC) @@ -285,6 +284,9 @@ let myConfig = { myConfig.DEFAULT = myEnv.DEFAULT myConfig.GitHub = myEnv.GitHub +// Construct the authorization object +const githubAuth = new GitHubAuth(myConfig, environment, defaultConfigFile) + // Construct needed classes const cliOutput = new CLIOutput(myEnv) const wizardUtils = new WizardUtils('all') @@ -323,26 +325,60 @@ cliOutput.printLine() /* ----------------------------------------- */ -/* ---- Begin device flow authorization ---- */ +/* ---- Begin authorization ---- */ // If the GitHub section exists in the config file then we can skip the device flow authorization let accessToken +let authType if(configExists[0]) { - accessToken = await environment.verifyAccessToken(true) - myConfig.GitHub.token = accessToken.token - myConfig.GitHub.expiresAt = accessToken.expiry - myConfig.GitHub.deviceCode = accessToken.device + const credential = await githubAuth.verifyAccessToken(false) + if(!credential[0]) { + console.log(chalk.red.bold(`ERROR: ${credential[1].status_msg}`)) + process.exit(-1) + } + accessToken = credential[2].token + authType = credential[2].authType + myConfig.GitHub.token = accessToken + myConfig.GitHub.authType = authType } else { - // Obtain the access token - accessToken = await githubAuth.getAccessToken(myConfig.GitHub) - // Pull in only necessary settings from the access token - myConfig.GitHub.token = accessToken.token - myConfig.GitHub.expiresAt = accessToken.expiresAt - myConfig.GitHub.deviceCode = accessToken.deviceCode - cliOutput.printLine() + const authTypes = { + 'Personal Access Token': 'pat', + 'Device Flow': 'deviceFlow', + } + + // Using map iterate through the keys of types and create an array of objects where each object looks like {name: key} + const authArray = Object.keys(authTypes).map((authType) => { + return { name: authType } + }) + + // Use doList in wizardUtils to prompt the user to select a theme + let authChoice = await wizardUtils.doList( + 'Please select a theme for your Mediumroast reports', + authArray + ) + + // Decode the theme value from the themes object + myConfig.GitHub.authType = authTypes[authChoice] + authType = myConfig.GitHub.authType + + // If the user selects pat we will need to prompt for the token + if(myConfig.GitHub.authType === 'pat') { + // Prompt the user for the PAT + myConfig.GitHub.token = await simplePrompt('Please enter your GitHub Personal Access Token.') + // Set access token to myConfig.GitHub.token + accessToken = myConfig.GitHub.token + } else { + const credential = await githubAuth.verifyAccessToken(false) + if(!credential[0]) { + console.log(chalk.red.bold(`ERROR: ${credential[1].status_msg}`)) + process.exit(-1) + } + accessToken = credential[2].token + } } +cliOutput.printLine() -/* ----- End device flow authorization ----- */ +/* ----- End authorization ----- */ /* ----------------------------------------- */ @@ -379,6 +415,27 @@ if(prevInstallComp[0]) { /* ------- End check for prev install ------ */ /* ----------------------------------------- */ +/* ----------------------------------------- */ +/* --------- Begin prompt for theme -------- */ +const themes = { + 'Electric coffee': 'coffee', + 'Bright espresso': 'espresso', + 'Double shot latte': 'latte', +} + +// Using map iterate through the keys of themes and create an array of objects where each object looks like {name: key} +const themeArray = Object.keys(themes).map((theme) => { + return { name: theme } +}) + +// Use doList in wizardUtils to prompt the user to select a theme +const theme = await wizardUtils.doList( + 'Please select a theme for your Mediumroast reports', + themeArray +) + +// Decode the theme value from the themes object +myConfig.DEFAULT.theme = themes[theme] /* ----------------------------------------- */ /* ----------- Save config file ------------ */ diff --git a/cli/mrcli-storage.js b/cli/mrcli-storage.js index 2d5a62a..b381702 100644 --- a/cli/mrcli-storage.js +++ b/cli/mrcli-storage.js @@ -1,14 +1,17 @@ #!/usr/bin/env node /** - * A CLI utility used for accessing and reporting on mediumroast.io user objects + * @fileoverview A CLI utility to report on Mediumroast for GitHub Storage consumption + * @license Apache-2.0 + * @version 1.1.1 + * * @author Michael Hay - * @file actions.js + * @file mrcli-storage.js * @copyright 2024 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @verstion 1.0.0 + * */ + // Import required modules import { Storage } from '../src/api/gitHubServer.js' import Environmentals from '../src/cli/env.js' @@ -21,9 +24,9 @@ const objectType = 'Storage' // Environmentals object const environment = new Environmentals( - '1.1.0', + '1.1.1', `${objectType}`, - `Command line interface to report on and update actions.`, + `A CLI utility to report on Mediumroast for GitHub Storage consumption`, objectType ) diff --git a/cli/mrcli-study.js b/cli/mrcli-study.js index 2a52008..e0555e0 100755 --- a/cli/mrcli-study.js +++ b/cli/mrcli-study.js @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * A CLI utility used for accessing and reporting on mediumroast.io study objects - * @author Michael Hay - * @file study.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. + * @fileoverview A CLI utility to manage and report on Mediumroast for GitHub Study objects * @license Apache-2.0 - * @verstion 3.0.0 + * @version 3.0.0 + * + * @author Michael Hay + * @file mrcli-study.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * */ @@ -28,7 +30,7 @@ const objectType = 'Studies' const environment = new Environmentals ( '3.0', `${objectType}`, - `Command line interface for mediumroast.io ${objectType} objects.`, + `A CLI utility to manage and report on Mediumroast for GitHub Study objects`, objectType ) diff --git a/cli/mrcli-user.js b/cli/mrcli-user.js index 0a6fde8..4929f28 100755 --- a/cli/mrcli-user.js +++ b/cli/mrcli-user.js @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * A CLI utility used for accessing and reporting on mediumroast.io user objects + * @fileoverview A CLI utility to report on Mediumroast for GitHub Storage authorized users + * @license Apache-2.0 + * @version 2.1.0 + * * @author Michael Hay - * @file user.js + * @file mrcli-user.js * @copyright 2024 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @verstion 2.0.0 + * */ // Import required modules @@ -22,7 +24,7 @@ const objectType = 'Users' const environment = new Environmentals( '2.1.0', `${objectType}`, - `Command line interface for mediumroast.io ${objectType} objects.`, + `A CLI utility to report on Mediumroast for GitHub Storage authorized users`, objectType ) diff --git a/cli/mrcli.js b/cli/mrcli.js index 42d3ca3..672dd4f 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * The wrapping CLI to engage all mediumroast.io CLIs + * @fileoverview The wrapping CLI for Mediumroast for GitHub + * @license Apache-2.0 + * @version 1.2.1 + * * @author Michael Hay - * @file mrcli.js + * @file mrcli-user.js * @copyright 2024 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @version 1.2.0 + * */ // Import required modules diff --git a/package.json b/package.json index 4a332bb..8e02d0e 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "ora": "^6.1.2", "uninstall": "^0.0.0", "wrap-ansi": "^8.1.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "node-geocoder": "^4.3.0" } } diff --git a/src/api/authorize.js b/src/api/authorize.js index 46b06ef..312b73f 100644 --- a/src/api/authorize.js +++ b/src/api/authorize.js @@ -35,6 +35,12 @@ import FilesystemOperators from '../cli/filesystem.js' class GitHubAuth { + /** + * @constructor + * @param {Object} env - The environment object + * @param {Object} environ - The environmentals object + * @param {String} configFile - The configuration file + */ constructor (env, environ, configFile) { this.env = env this.clientType = 'github-app' diff --git a/src/api/gitHubServer.js b/src/api/gitHubServer.js index f49dc9a..eedc7da 100644 --- a/src/api/gitHubServer.js +++ b/src/api/gitHubServer.js @@ -4,12 +4,18 @@ * @file gitHubServer.js * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 1.0.0 + * @version 2.0.0 * * @class baseObjects * @classdesc An implementation for interacting with the GitHub backend. * * @requires GitHubFunctions + * @requires crypto + * @requires fs + * @requires path + * @requires fileURLToPath + * + * @exports {Studies, Companies, Interactions, Users, Storage, Actions} * * @example * import {Companies, Interactions, Users, Billings} from './api/gitHubServer.js' @@ -525,7 +531,7 @@ class Actions extends baseObjects { blobData = fs.readFileSync(action.srcURL, 'base64') status = true } catch (err) { - console.log(`Unable to read file [${action.fileName}] because: ${err}`) + // console.log(`Unable to read file [${action.fileName}] because: ${err}`) return [false,{status_code: 500, status_msg: `Unable to read file [${action.fileName}] because: ${err}`}, installStatus] } if(status) { diff --git a/src/api/github.js b/src/api/github.js index 4322596..1091d98 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -11,6 +11,7 @@ * @classdesc Core functions needed to interact with the GitHub API for mediumroast.io. * * @requires octokit + * @requires axios * * @exports GitHubFunctions * diff --git a/src/cli/archive.js b/src/cli/archive.js index d128d15..32f286f 100644 --- a/src/cli/archive.js +++ b/src/cli/archive.js @@ -1,18 +1,32 @@ /** - * A class used to create or restore from a ZIP based arhive package - * @author Michael Hay - * @file archive.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. + * @fileoverview A class used to create or restore from a ZIP based arhive package * @license Apache-2.0 * @version 2.0.1 + * + * @author Michael Hay + * @file archive.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * + * @class ArchivePackage + * @classdesc A class designed to enable consistent ZIP packaging for mediumroat.io archives/backups + * + * @requires adm-zip + * + * @exports ArchivePackage + * + * @example + * import ArchivePackage from './archive.js' + * const archive = new ArchivePackage('myArchive.zip') + * const createStatus = await archive.createZIPArchive('myDirectory') + * const extractStatus = await archive.extractZIPArchive('myDirectory') + * */ // Import required modules import zip from 'adm-zip' class ArchivePackage { - /** - * A class designed to enable consistent ZIP packaging for mediumroat.io archives/backups + /** * @constructor * @classdesc Apply consistent operations to ZIP packages to enable users to backup and restore * @param {String} packageName - the name of the package to either create or extract depending upoon the operation called diff --git a/src/cli/common.js b/src/cli/common.js index da1e2af..2a0ee22 100644 --- a/src/cli/common.js +++ b/src/cli/common.js @@ -7,11 +7,6 @@ * @version 1.1.0 */ -// Import required modules -import axios from 'axios' -import * as fs from 'fs' -import * as path from 'path' - class CLIUtilities { /** diff --git a/src/cli/env.js b/src/cli/env.js index 5744551..75351ce 100644 --- a/src/cli/env.js +++ b/src/cli/env.js @@ -1,17 +1,34 @@ /** - * A class used by CLIs to capture and set environmental variables + * @fileoverview A class used by CLIs to capture and set environmental variables + * @license Apache-2.0 + * @version 2.5.0 + * * @author Michael Hay * @file env.js - * @copyright 2022 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @version 2.4.0 + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * + * @class Environment + * @classdesc A class to create consistent CLI envrionmentals for mediumroast.io objects + * + * @requires commander + * @requires configparser + * @requires filesystem + * + * @exports Environment + * + * @example + * const env = new Environmentals() + * const program = env.parseCLIArgs() + * const config = env.readConfig(program.conf_file) + * const envSettings = env.getEnv(program, config) + * + * */ // Import required modules import program from 'commander' import ConfigParser from 'configparser' import FilesystemOperators from './filesystem.js' -import { GitHubAuth } from '../api/authorize.js' class Environmentals { @@ -290,76 +307,6 @@ class Environmentals { // Return the environmental settings needed for the CLI to operate return env } - - /** - * @function verifyAccessToken - * @description Verify the access token is valid and if not get a new one - * @todo there is a bug in the persistence of the expiresAt value in the config file - */ - // async verifyAccessToken (fromSetup=false) { - // // Get configuration information from the config file - // const configFile = this.checkConfigDir() - // let env = this.readConfig(configFile) - - // // Construct the GitHub authoirzation object - // const githubAuth = new GitHubAuth({clientId: env.get('GitHub', 'clientId'), authType: env.get('GitHub', 'authType')}) - - // // Define needed variables to housekeep the accessToken and the config file - // let authType = env.get('GitHub', 'authType') - // let accessToken = env.get('GitHub', 'token') - // let updateConfig = false - // let deviceCode - // let expiresAt - - // // Check to see if the GitHub section is available - // if (env.hasSection('GitHub')) { - // // Check to see if the expiration date is available - // env.hasKey('GitHub', 'expiresAt') ? expiresAt = env.get('GitHub', 'expiresAt') : expiresAt = 0 - - // // Convert the access token expirations into Date objects - // let accessExpiry - // expiresAt === 'undefined' ? accessExpiry = 0 : accessExpiry = new Date(expiresAt) - // const now = new Date() - - // // Check to see if the access token is valid - // if (accessExpiry < now) { - // const myEnv = { - // clientId: env.get('GitHub', 'clientId'), - // clientType: env.get('GitHub', 'clientType') - // } - - // accessToken = await githubAuth.getAccessTokenDeviceFlow(myEnv) - // env = this.updateConfigSetting(env, 'GitHub', 'token', accessToken.token) - // env = this.updateConfigSetting(env[1], 'GitHub', 'expiresAt', accessToken.expiresAt) - // env = this.updateConfigSetting(env[1], 'GitHub', 'deviceCode', accessToken.deviceCode) - // updateConfig = true - // env = env[1] - // accessToken = accessToken.token - // expiresAt = accessToken.expiresAt - // deviceCode = accessToken.deviceCode - // } - // } else { - // // Section GitHub not available perform complete authorization flow - // // Get the access token and add a GitHub section to the env - // accessToken = await githubAuth.getAccessTokenDeviceFlow(env) - // // Create the GitHub section - // env = this.addConfigSection(env, 'GitHub', accessToken) - // env = this.removeConfigSetting(env[1], 'GitHub', 'contentType') - // env = this.removeConfigSetting(env[1], 'GitHub', 'grantType') - // updateConfig = true - // env = env[1] - // } - - // // Save the config file if needed - // if (updateConfig) { - // await env.write(configFile) - // } - - // if (fromSetup) { - // return {token: accessToken, expiry: expiresAt ,device: deviceCode} - // } - // return accessToken - // } } export default Environmentals \ No newline at end of file From a3312a7d58b9d37f2f89e66d4aa4c17c07bc2e1e Mon Sep 17 00:00:00 2001 From: mihay42 Date: Tue, 3 Sep 2024 07:40:11 -0700 Subject: [PATCH 19/23] Verified PAT implementation for all CLIs --- cli/mrcli-actions.js | 2 +- cli/mrcli-company.js | 2 +- cli/mrcli-interaction.js | 2 +- cli/mrcli-setup.js | 17 +++++---- cli/mrcli-storage.js | 2 +- cli/mrcli-user.js | 2 +- package.json | 2 +- src/api/authorize.js | 78 ++++++++++++++++++++++++---------------- 8 files changed, 65 insertions(+), 42 deletions(-) diff --git a/cli/mrcli-actions.js b/cli/mrcli-actions.js index 54e71a1..c9f9917 100644 --- a/cli/mrcli-actions.js +++ b/cli/mrcli-actions.js @@ -72,7 +72,7 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) -const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file, true) const verifiedToken = await myAuth.verifyAccessToken() let accessToken = null if (!verifiedToken[0]) { diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 05158b8..b1007c8 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -55,7 +55,7 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) myEnv.company = 'Unknown' -const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file, true) const verifiedToken = await myAuth.verifyAccessToken() let accessToken = null if (!verifiedToken[0]) { diff --git a/cli/mrcli-interaction.js b/cli/mrcli-interaction.js index 3ed6355..3cc9f1e 100755 --- a/cli/mrcli-interaction.js +++ b/cli/mrcli-interaction.js @@ -74,7 +74,7 @@ const fileSystem = new FilesystemOperators() const myArgs = environment.parseCLIArgs() const myConfig = environment.readConfig(myArgs.conf_file) const myEnv = environment.getEnv(myArgs, myConfig) -const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file, true) const verifiedToken = await myAuth.verifyAccessToken() let accessToken = null if (!verifiedToken[0]) { diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index aebcc31..7e2fe0d 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -284,9 +284,6 @@ let myConfig = { myConfig.DEFAULT = myEnv.DEFAULT myConfig.GitHub = myEnv.GitHub -// Construct the authorization object -const githubAuth = new GitHubAuth(myConfig, environment, defaultConfigFile) - // Construct needed classes const cliOutput = new CLIOutput(myEnv) const wizardUtils = new WizardUtils('all') @@ -326,6 +323,8 @@ cliOutput.printLine() /* ----------------------------------------- */ /* ---- Begin authorization ---- */ +// Construct the authorization object +const githubAuth = new GitHubAuth(myConfig, environment, defaultConfigFile, configExists[0]) // If the GitHub section exists in the config file then we can skip the device flow authorization let accessToken @@ -353,7 +352,7 @@ if(configExists[0]) { // Use doList in wizardUtils to prompt the user to select a theme let authChoice = await wizardUtils.doList( - 'Please select a theme for your Mediumroast reports', + 'Please select the authorization type used to access GitHub', authArray ) @@ -367,6 +366,11 @@ if(configExists[0]) { myConfig.GitHub.token = await simplePrompt('Please enter your GitHub Personal Access Token.') // Set access token to myConfig.GitHub.token accessToken = myConfig.GitHub.token + const isTokenValid = await githubAuth.checkTokenExpiration(accessToken) + if(!isTokenValid[0]) { + console.log(chalk.red.bold(`ERROR: Unable to verify the GitHub Personal Access Token with error [${isTokenValid[1].status_msg}].`)) + process.exit(-1) + } } else { const credential = await githubAuth.verifyAccessToken(false) if(!credential[0]) { @@ -374,6 +378,7 @@ if(configExists[0]) { process.exit(-1) } accessToken = credential[2].token + myConfig.GitHub.token = accessToken } } cliOutput.printLine() @@ -440,7 +445,7 @@ myConfig.DEFAULT.theme = themes[theme] /* ----------------------------------------- */ /* ----------- Save config file ------------ */ // Confirm that the configuration directory exists only if we don't already have one -if(!configExists[0]) { +// if(!configExists[0]) { const configFile = environment.checkConfigDir() process.stdout.write(chalk.bold.blue(`Saving configuration to file [${configFile}] ... `)) @@ -458,7 +463,7 @@ if(!configExists[0]) { } cliOutput.printLine() -} +// } // Confirm that Document directory exists and if not create it const docDir = myConfig.DEFAULT.report_output_dir const reportDirExists = fsUtils.safeMakedir(docDir) diff --git a/cli/mrcli-storage.js b/cli/mrcli-storage.js index b381702..03e7298 100644 --- a/cli/mrcli-storage.js +++ b/cli/mrcli-storage.js @@ -71,7 +71,7 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) -const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file, true) const verifiedToken = await myAuth.verifyAccessToken() let accessToken = null if (!verifiedToken[0]) { diff --git a/cli/mrcli-user.js b/cli/mrcli-user.js index 4929f28..29baeb8 100755 --- a/cli/mrcli-user.js +++ b/cli/mrcli-user.js @@ -66,7 +66,7 @@ myArgs = myArgs.opts() const myConfig = environment.readConfig(myArgs.conf_file) let myEnv = environment.getEnv(myArgs, myConfig) -const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file) +const myAuth = new GitHubAuth(myEnv, environment, myArgs.conf_file, true) const verifiedToken = await myAuth.verifyAccessToken() let accessToken = null if (!verifiedToken[0]) { diff --git a/package.json b/package.json index 8e02d0e..79da0be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.7.00.42", + "version": "0.7.00.43", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with Mediumroast for GitHub.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/api/authorize.js b/src/api/authorize.js index 312b73f..7667aef 100644 --- a/src/api/authorize.js +++ b/src/api/authorize.js @@ -41,16 +41,21 @@ class GitHubAuth { * @param {Object} environ - The environmentals object * @param {String} configFile - The configuration file */ - constructor (env, environ, configFile) { + constructor (env, environ, configFile, configExists) { this.env = env this.clientType = 'github-app' this.configFile = configFile - this.environ = environ - this.config = environ.readConfig(configFile) + this.configExists = configExists this.filesystem = new FilesystemOperators() + this.environ = environ + // Use ternary operator to determine if the config file exists and if it does read it else set it to null + this.config = configExists ? environ.readConfig(configFile) : null } verifyGitHubSection () { + if (!this.config) { + return false + } return this.config.hasSection('GitHub') } @@ -96,11 +101,13 @@ class GitHubAuth { * @returns {Object} The access token object */ async getAccessTokenDeviceFlow() { + // Set the clientId depending on if the config file exists + const clientId = this.configExists ? this.env.clientId : this.env.GitHub.clientId // Construct the oAuth device flow object which starts the browser let deviceCode // Provide a place for the device code to be captured const deviceauth = octoDevAuth.createOAuthDeviceAuth({ clientType: this.clientType, - clientId: this.env.clientId, + clientId: clientId, onVerification(verifier) { deviceCode = verifier.device_code // Print the verification artifact to the console @@ -120,12 +127,6 @@ class GitHubAuth { // Call GitHub to obtain the token let accessToken = await deviceauth({type: 'oauth'}) - - // NOTE: The token is not returned with the expires_in and expires_at fields, this is a workaround - // let now = new Date() - // now.setHours(now.getHours() + 8) - // accessToken.expiresAt = now.toUTCString() - // Add the device code to the accessToken object accessToken.deviceCode = deviceCode return accessToken } @@ -137,22 +138,36 @@ class GitHubAuth { * @param {Boolean} saveToConfig - Save to the configuration file, default is true */ async verifyAccessToken (saveToConfig=true) { - // Get key variables from the config file - const hasGitHubSection = this.verifyGitHubSection() - // If the GitHub section is not available, then the token is not available, return false. - // This is only to be used when called from a function that intendes to setup the configuration file, but - // just in case this condition occurs we want to return clearly to the caller. - if (!hasGitHubSection) { - return [false, {status_code: 500, status_msg: 'The GitHub section is not available in the configuration file'}, null] + + if (this.configExists) { + // Get key variables from the config file + const hasGitHubSection = this.verifyGitHubSection() + // If the GitHub section is not available, then the token is not available, return false. + // This is only to be used when called from a function that intendes to setup the configuration file, but + // just in case this condition occurs we want to return clearly to the caller. + if (!hasGitHubSection) { + return [false, {status_code: 500, status_msg: 'The GitHub section is not available in the configuration file'}, null] + } } // Get the access token and authType from the config file since the section is available - let accessToken = this.getAccessTokenFromConfig() - const authType = this.getAuthTypeFromConfig() + let accessToken + // If the configuration exists then we can obtain the token and authType from the config file, but if + // the configuration is not present and the intention is to use PAT this code won't be executed. Therefore, + // prompting the user for the PAT, verifyin the PAT, and saving the PAT to the config file will be done in the + // caller. However, if the intention is to use deviceFlow then we can support that here and return the token to the + // caller which will then save the token and the authType to the config file. + let authType = 'deviceFlow' + if (this.configExists) { + accessToken = this.getAccessTokenFromConfig() + authType = this.getAuthTypeFromConfig() + } - // Check to see if the token is valid - const validToken = await this.checkTokenExpiration(accessToken) - if (validToken[0]) { + // Check to see if the token is valid but if the config isn't present then we can't check the token + const validToken = this.configExists ? + await this.checkTokenExpiration(accessToken) : + [false, {status_code: 500, status_msg: 'The configuration file isn\'t present'}, null] + if (validToken[0] && this.configExists) { return [ true, {status_code: 200, status_msg: validToken[1].status_msg}, @@ -172,15 +187,18 @@ class GitHubAuth { } else if (authType === 'deviceFlow') { // Get the new access token accessToken = await this.getAccessTokenDeviceFlow() - // Update the config - let tmpConfig = this.environ.updateConfigSetting(this.config, 'GitHub', 'token', accessToken.token) - tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'authType', authType) - tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'deviceCode', accessToken.deviceCode) - // Save the config file if needed - this.config = tmpConfig[1] - if (saveToConfig) { - await this.config.write(this.configFile) + // Update the config if the config file exists and if saveToConfig is true + if (this.configExists && this.config && this.saveToConfig) { + let tmpConfig = this.environ.updateConfigSetting(this.config, 'GitHub', 'token', accessToken.token) + tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'authType', authType) + tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'deviceCode', accessToken.deviceCode) + + // Save the config file if needed + this.config = tmpConfig[1] + if (saveToConfig) { + await this.config.write(this.configFile) + } } return [ From 9570c4141a3904d2351b9315533fa781cd0afb20 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Wed, 4 Sep 2024 20:16:05 -0700 Subject: [PATCH 20/23] Documentation, footer cleanup, etc. --- README.md | 8 ++++--- cli/Company.md | 12 +++++++++- cli/Interaction.md | 9 +++++++ cli/README.md | 49 +++++++++++++++++++++++++++++++------- cli/mrcli-company.js | 2 +- cli/mrcli-interaction.js | 2 -- cli/mrcli-setup.js | 2 +- package.json | 2 +- src/report/charts.js | 1 + src/report/widgets/Text.js | 1 + 10 files changed, 70 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f3a3eef..598b501 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ ## Welcome to Open Source Mediumroast for GitHub. Products organizations must build robust product plans from competitive and customer interactions everyone can see, use, and reference. Therefore, Mediumroast for GitHub intends to help Products oranizations construct an active interactions repository close to the action of development and issue management in GitHub. -**Notice:** You can review the [GitHub Page Version](https://mediumroast.github.io/mediumroast_js/) rather than the repository version of this documentation, but the screencasts of several of the CLI tutorials will not display. +### Notices +- A new version of the CLI is available and documentation is in progress. The major focus of this version is to add in Competitive Similarity Analysis, Interaction summarization and Interaction Proto-requirements discovery. +- You can review the [GitHub Page Version](https://mediumroast.github.io/mediumroast_js/) rather than the repository version of this documentation, but the screencasts of several of the CLI tutorials will not display. ## Installation and configuration Mediumroast for GitHub includes a [GitHub Application](https://github.com/apps/mediumroast-for-github), a Command Line Interface, and a Software Development Kit. The following steps show you how to install the App and the CLI with SDK. ### Preinstallation requirements -1. A GitHub organization, please +1. A GitHub organization 2. Permissions in your GitHub organization to install a GitHub application. 3. Access to a command line terminal on Linux or MacOS. 4. [Node.js installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm#using-a-node-installer-to-install-nodejs-and-npm), ideally globally for all users. @@ -36,7 +38,7 @@ Coming soon. Before you can use the Mediumroast for GitHub [CLI](https://github.com/mediumroast/mediumroast_js/blob/main/cli/README.md) the environment must be setup. With the CLI installed please run `mrcli setup` to start the setup process, note there's a video of the setup process in CLI README. ## What's provided -Running `mrcli setup` creates a repository in your oganization called `_discovery` for all interactions and objects, creates two intitial companies, and installs two GitHub Actions to control the number of branches and provide some basic out of the box reporting -- see example screenshot below. +Running `mrcli setup` creates a repository in your oganization called `_discovery` to contain all interactions and companies, creates two intitial companies, and installs two GitHub Actions to control the number of branches and provide some basic out of the box reporting -- see example screenshot below. ### Warning Since Mediumroast for GitHub creates a regular repository you can interact with it as normal, but **doing so is not recommended**. If you interact with the repository, in regular ways, this could result in Mediumroast for GitHub becoming inoperable. There are cases where it may become necessary to directly work with the repository, but that should be rare. diff --git a/cli/Company.md b/cli/Company.md index 9207f21..7d75be6 100644 --- a/cli/Company.md +++ b/cli/Company.md @@ -1,5 +1,5 @@ ## Companies -Company objects are central to Mediumroast for GitHub. Interaction sand in the future Studies rely on Companies to function. After setup is run, via `mrcli setup`, two companies are present to work with. Additional Companies can be added, updated, or removed; essentially, `company` is an `mrcli` sub-command that affords users Create, Read, Update and Delete capabilities. Each of the major functions for `mrcli company` are described in this document. +Company objects are central to Mediumroast for GitHub. Interactions and in the future Studies rely on Companies to function. After setup is run, via `mrcli setup`, two companies are present to work with. Additional Companies can be added, updated, or removed; essentially, `company` is an `mrcli` sub-command that affords users Create, Read, Update and Delete capabilities. Each of the major functions for `mrcli company` are described in this document. ### Notice Some of the command line options and switches may not yet be implemented; therefore, if a switch or option is not yet implemented the CLI will inform the user and then exit. @@ -100,6 +100,16 @@ A command line prompt based wizard steps the user through either a semi-automate companies_add +## Report on a company +Produce a MS Word document report on a company. The report includes a dashboard with a company similarity report, company firmographics, detail on similar companies, and summaries for all interactions associated to the company reported on. The report is stored in the `$HOME/Documents` directory as `.docx`. + +Optionally, if the `--package` switch is used the report is zipped and stored in the $HOME/Documents directory as `.zip` including all of the interactions associated to the company and the set of most and least similar companies. + +### Command(s) run +- `mrcli c --report="Atlassian Corp"` +- `mrcli c --report="Atlassian Corp" --package` + +### Screenshot of the report dashboard diff --git a/cli/Interaction.md b/cli/Interaction.md index d95b29f..fb777da 100644 --- a/cli/Interaction.md +++ b/cli/Interaction.md @@ -110,7 +110,16 @@ A command line prompt based wizard steps the user through a semi-automated proce https://github.com/mediumroast/mediumroast_js/assets/10818650/0b28db90-d6ba-4224-a9ae-4c301c2b9614 +## Report on an interaction +Produce a MS Word document report on an interaction. The report includes a dashboard with interaction metadata, interaction abstract, and proto-requirements. The report is stored in the `$HOME/Documents` directory as `.docx`. +Optionally, if the `--package` switch is used the report is zipped and stored in the $`HOME/Documents` directory as `.zip` including the interaction content. + +### Command(s) run +- `mrcli i --report="Atlassian Corp"` +- `mrcli i --report="Atlassian Corp" --package` + +### Screenshot of the report dashboard diff --git a/cli/README.md b/cli/README.md index 4a4e683..d44a72c 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,9 +1,37 @@ # Mediumroast for GitHub CLI (Command Line Interface) The CLI is divided into two sets one for administrative interactions with the system and another for interacting with **Mediumroast for GitHub** objects like Companies and Interactions. This document covers both the administrative CLI and makes reference to the CLI set for **Mediumroast for GitHub** objects. +# Authentication +**Mediumroast for GitHub** uses either Device Flow or a Personal Access Token for authentication. The setup CLI will prompt you to choose the type of authentication and then store the necessary information in the `${HOME}/.mediumroast/config.ini` file. If you need to change the authentication configuration you can run `mrcli setup` to reset the configuration file. + +## Required permissions for the Personal Access Token (PAT) +If you create a PAT before the **Mediumroast for GitHub** repository exists the required scope is for **all repositories** in your organization. At PAT renewal or if you want to reduce the scope to only the **Mediumroast for GitHub** repository this can be done later. The Personal Access Token must have the following permissions on all repositories to perform the necessary actions including setup. + +- `Actions`: read/write +- `Administration`: read/write +- `Contents`: read/write +- `Metadata`: read +- `Pull requests`: read/write +- `Workflows`: read/write + +For background and directions on creating a PAT see the [GitHub documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). + + # Administrative CLIs To enable setup and operational reporting of **Mediumroast for GitHub** several CLIs are available, each is described below. +## Storage reporting +Reports storage consumed by **Mediumroast for GitHub**. A screenshot showing the usage information and outputs for major functions is included below. +### Command(s) run +- `mrcli t` +- `mrcli storage` + +## Actions +Reports actions minutes consumed by **Mediumroast for GitHub** and enables a user to update their repository as new versions of actions are released. A screenshot showing the usage information and outputs for major functions is included below. +### Command(s) run +- `mrcli actions` +- `mrcli a --update` + ## User reporting Reports on all users who can access the repository that contains the **Mediumroast for GitHub**. A screenshot showing the usage information and outputs for major functions is included below, and notice, user names and other personally identifiable information has been redacted from the screenshot below. ### Command(s) run @@ -12,15 +40,6 @@ Reports on all users who can access the repository that contains the **Mediumroa ### Screenshot with ouput users_util -## Billing reporting -Provides reports for consumed actions and repository storage consumed by the organization that has installed and is using **Mediumroast for GitHub**. A screenshot showing the usage information and outputs for major functions is included below. -### Command(s) run -- `mrcli b` -- `mrcli b --storage` -- `mrcli b --actions` -### Screenshot with ouput -billings_util - ## Setup To help users quickly get on board with **Mediumroast for GitHub** the setup CLI is used. This CLI creates the `${HOME}/.mediumroast/config.ini` file, creates the repository including key directories, and creates two initial companies. A screencast video showing the process for setting up the CLI environment and creating two companies is available below. @@ -31,6 +50,18 @@ To help users quickly get on board with **Mediumroast for GitHub** the setup CLI https://github.com/mediumroast/mediumroast_js/assets/10818650/68c08502-4f59-4981-a001-0d9c9bd1d4d2 +# Deprecated commands +The following commands are deprecated, are disabled and will be removed in a future release of the CLI. + +## Billing reporting +Is replaced by `storage` and `actions` commands in **Mediumroast for GitHub**. If you run this command it will immediately exit stating it is no longer supported. + +### Command(s) run +- `mrcli b` +- `mrcli billing` + + + --- [[Company Subcommand](https://github.com/mediumroast/mediumroast_js/blob/main/cli/Company.md)] | [[Interaction Subcommand](https://github.com/mediumroast/mediumroast_js/blob/main/cli/Interaction.md)] diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index b1007c8..27cf8b1 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -159,7 +159,7 @@ if (myArgs.report) { } // Create the document - const [report_success, sourceData] = await docController.makeDOCX(fileName, myArgs.package) + const [report_success, report_stat, report_results] = await docController.makeDOCX(fileName, myArgs.package) // Create the package and cleanup as needed diff --git a/cli/mrcli-interaction.js b/cli/mrcli-interaction.js index 3cc9f1e..a6ec473 100755 --- a/cli/mrcli-interaction.js +++ b/cli/mrcli-interaction.js @@ -134,8 +134,6 @@ if (myArgs.report) { company, // The company associated to the interaction myEnv, // The environment settings allObjects, // All objects - fileName, // The file name - myArgs.package // The package flag ) if(myArgs.package) { diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index 7e2fe0d..fdf99e0 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -68,7 +68,7 @@ function getEnv () { DEFAULT: { company_dns: "https://company-dns.mediumroast.io", company_logos: "https://icon-server.mediumroast.io/allicons.json?url=", - echarts: "https://chart-server.mediumroast.io:11000", + echarts: "https://echart-server.mediumroast.io:11000", nominatim: 'https://nominatim.openstreetmap.org/search?addressdetails=1&q=', working_directory: "working", report_output_dir: "Documents", diff --git a/package.json b/package.json index 79da0be..452dac9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.7.00.43", + "version": "0.7.00.44", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with Mediumroast for GitHub.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/report/charts.js b/src/report/charts.js index a1d578b..bbffb37 100755 --- a/src/report/charts.js +++ b/src/report/charts.js @@ -250,6 +250,7 @@ class Charting { // Send to the chart server const putResult = await this._postToChartServer(myChart, this.env.echartsServer) + // console.log(putResult) const myFullPath = path.resolve(this.workingImageDir, chartFile) fs.writeFileSync(myFullPath, putResult[2]) return myFullPath diff --git a/src/report/widgets/Text.js b/src/report/widgets/Text.js index a87b6d6..2248b6b 100644 --- a/src/report/widgets/Text.js +++ b/src/report/widgets/Text.js @@ -83,6 +83,7 @@ class TextWidgets extends Widgets { } makeFooter(documentAuthor, datePrepared, options={}) { + console.log(documentAuthor, datePrepared) const { landscape = false, fontColor = this.themeSettings.textFontColor, From 3e30ec4630b3fdb10fa89f9add914ce718be94b5 Mon Sep 17 00:00:00 2001 From: Michael Hay Date: Wed, 4 Sep 2024 20:25:43 -0700 Subject: [PATCH 21/23] Update CLI README.md --- cli/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cli/README.md b/cli/README.md index d44a72c..4e0fac2 100644 --- a/cli/README.md +++ b/cli/README.md @@ -26,12 +26,19 @@ Reports storage consumed by **Mediumroast for GitHub**. A screenshot showing the - `mrcli t` - `mrcli storage` +### Screenshot with ouput +![Screenshot 2024-09-04 at 8 23 17 PM](https://github.com/user-attachments/assets/dba9738f-e093-4415-9ec7-1031f66cf6d1) + ## Actions Reports actions minutes consumed by **Mediumroast for GitHub** and enables a user to update their repository as new versions of actions are released. A screenshot showing the usage information and outputs for major functions is included below. ### Command(s) run - `mrcli actions` - `mrcli a --update` +### Screenshot with ouput +![Screenshot 2024-09-04 at 8 23 55 PM](https://github.com/user-attachments/assets/d9ea51fc-7609-4abb-9cbe-6e010baacb50) + + ## User reporting Reports on all users who can access the repository that contains the **Mediumroast for GitHub**. A screenshot showing the usage information and outputs for major functions is included below, and notice, user names and other personally identifiable information has been redacted from the screenshot below. ### Command(s) run From c6da123e4cd694581d2494e4793452bf15ad65ad Mon Sep 17 00:00:00 2001 From: Michael Hay Date: Wed, 4 Sep 2024 20:27:39 -0700 Subject: [PATCH 22/23] Update Company.md with report screenshot --- cli/Company.md | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/Company.md b/cli/Company.md index 7d75be6..7f1cef3 100644 --- a/cli/Company.md +++ b/cli/Company.md @@ -111,5 +111,6 @@ Optionally, if the `--package` switch is used the report is zipped and stored in ### Screenshot of the report dashboard +![Screenshot 2024-09-03 at 8 23 29 AM](https://github.com/user-attachments/assets/bd1141cc-53c6-4cac-8e56-ed50601dcbb6) From 6b8ea2d0e8562037373abeb1134df4d5c78dec11 Mon Sep 17 00:00:00 2001 From: Michael Hay Date: Wed, 4 Sep 2024 20:31:03 -0700 Subject: [PATCH 23/23] Update Interaction.md with report screenshot --- cli/Interaction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/Interaction.md b/cli/Interaction.md index fb777da..f32b09b 100644 --- a/cli/Interaction.md +++ b/cli/Interaction.md @@ -120,7 +120,7 @@ Optionally, if the `--package` switch is used the report is zipped and stored in - `mrcli i --report="Atlassian Corp" --package` ### Screenshot of the report dashboard - +![Screenshot 2024-09-04 at 8 30 25 PM](https://github.com/user-attachments/assets/19c614a5-ed58-4687-aff8-a619ea693398)