From 216a4a91e44c4d737ea598b58e1b6cec52557d08 Mon Sep 17 00:00:00 2001 From: masnn Date: Sun, 31 May 2020 20:07:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=8A=B6=E6=80=81=EF=BC=88/s?= =?UTF-8?q?tatus=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 + hydro/handler/home.js | 28 +++---- hydro/handler/manage.js | 4 +- hydro/handler/status.js | 34 +++++++++ hydro/handler/system.js | 91 +++++++++++++++++++++++ hydro/lib/misc.js | 2 +- hydro/lib/sysinfo.js | 60 +++++++++++++++ hydro/loader.js | 5 +- hydro/model/document.js | 11 +-- hydro/model/user.js | 11 +-- hydro/service/monitor.js | 25 +++++++ module/contest-rule-homework/homework.js | 95 ------------------------ module/contest-rule-homework/hydro.json | 4 - module/contest-rule-homework/model.js | 3 - module/migrate-vijos/script.js | 18 ++++- package.json | 9 +-- templates/status.html | 43 +++++++++++ ui/pages/status.page.styl | 9 +++ yarn.lock | 5 ++ 19 files changed, 322 insertions(+), 136 deletions(-) create mode 100644 hydro/handler/status.js create mode 100644 hydro/handler/system.js create mode 100644 hydro/lib/sysinfo.js create mode 100644 hydro/service/monitor.js delete mode 100644 module/contest-rule-homework/homework.js delete mode 100644 module/contest-rule-homework/hydro.json delete mode 100644 module/contest-rule-homework/model.js create mode 100644 templates/status.html create mode 100644 ui/pages/status.page.styl diff --git a/.eslintrc.js b/.eslintrc.js index 154bb052b..4c1ee370c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { "no-plusplus": "off", "no-console": "off", "no-extend-native": "off", + "no-underscore-dangle": "off", "no-restricted-syntax": "off", "max-classes-per-file": "off", "radix": "off", diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 9a0a16441..0dc2482d1 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -21,6 +21,8 @@ const { PERM_LOGGEDIN, } = require('../permission'); +const { geoip, useragent } = global.Hydro.lib; + class HomeHandler extends Handler { async contest(domainId) { if (this.user.hasPerm(PERM_VIEW_CONTEST)) { @@ -81,13 +83,13 @@ class HomeSecurityHandler extends Handler { // TODO(iceboy): pagination? or limit session count for uid? // TODO(masnn): UA & IP const sessions = await token.getSessionListByUid(this.user._id); - const parsed = sessions.map((session) => ({ - ...session, - // updateUa: useragent.parse(session.updateUa || session.createUa || ''), - // updateGeoip: geoip.ip2geo(session.updateIp || session.createIp, this.user.viewLang), - _id: md5(session._id), - isCurrent: session._id === this.session._id, - })); + const parsed = []; + for (const session of sessions) { + session.isCurrent = session._id === this.session._id; + session._id = md5(session._id); + if (useragent) session.updateUa = useragent.parse(session.updateUa || session.createUa || ''); + if (geoip) session.updateGeoip = geoip.lookup(session.updateIp || session.createIp); + } this.response.template = 'home_security.html'; this.response.body = { sessions: parsed }; } @@ -153,7 +155,7 @@ class HomeSettingsHandler extends Handler { async post(args) { // FIXME validation - await user.setById(domainId, this.user._id, args); + await user.setById(args.domainId, this.user._id, args); this.back(); } } @@ -166,9 +168,10 @@ class UserChangemailWithCodeHandler extends Handler { } const udoc = await user.getByEmail(tdoc.mail); if (udoc) throw new UserAlreadyExistError(tdoc.mail); - // TODO(twd2): Ensure mail is unique. - await user.setEmail(this.user._id, tdoc.mail); - await token.delete(code, token.TYPE_CHANGEMAIL); + await Promise.all([ + user.setEmail(this.user._id, tdoc.mail), + token.delete(code, token.TYPE_CHANGEMAIL), + ]); this.response.redirect = '/home/security'; } } @@ -211,8 +214,7 @@ class HomeMessagesHandler extends Handler { if (type === 'single') { mdoc = mdoc.reply[mdoc.reply.length - 1]; } - // TODO(twd2): improve here: - // projection + // TODO(twd2): improve here: projection mdoc.from_udoc = this.user; mdoc.to_udoc = udoc; this.udoc(mdoc, 'from'); diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js index ac24bbc46..da60ce92c 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.js @@ -1,6 +1,7 @@ const user = require('../model/user'); const domain = require('../model/domain'); const setting = require('../model/setting'); +const system = require('../model/system'); const { Route, Handler } = require('../service/server'); const { PERM_MANAGE } = require('../permission'); const hpm = require('../lib/hpm'); @@ -53,6 +54,7 @@ class ManageEditHandler extends ManageHandler { class ManageUserHandler extends ManageHandler { async get({ domainId }) { + // FIXME(masnn) UPDATE CODE const uids = []; const rudocs = {}; const [udocs, roles] = await Promise.all([ @@ -73,7 +75,7 @@ class ManageUserHandler extends ManageHandler { ]; this.response.template = 'manage_user.html'; this.response.body = { - roles, rolesSelect, rudocs, udict, system: sys, path, + roles, rolesSelect, rudocs, udict, path, }; } diff --git a/hydro/handler/status.js b/hydro/handler/status.js new file mode 100644 index 000000000..a2f64a9bb --- /dev/null +++ b/hydro/handler/status.js @@ -0,0 +1,34 @@ +const { PERM_JUDGE } = require('../permission'); +const { Route, Handler } = require('../service/server'); +const db = require('../service/db'); + +const coll = db.collection('status'); + +class StatusHandler extends Handler { + async get() { + const stats = await coll.find().sort({ type: 1, updateAt: -1 }).toArray(); + for (const i in stats) { + let desc = ''; + const online = new Date(stats[i].updateAt).getTime() > new Date().getTime() - 300000; + if (!online) desc = 'Offline'; + desc = desc || 'Online'; + stats[i].status = desc; + } + this.response.body = { stats }; + this.response.template = 'status.html'; + } +} + +class StatusUpdateHandler extends Handler { + async post(args) { + this.checkPerm(PERM_JUDGE); + await coll.updateOne({ _id: args._id }, { $set: args }, { upsert: true }); + } +} + +async function apply() { + Route('/status', StatusHandler); + Route('/status/update', StatusUpdateHandler); +} + +global.Hydro.handler.status = module.exports = { StatusHandler, StatusUpdateHandler, apply }; diff --git a/hydro/handler/system.js b/hydro/handler/system.js new file mode 100644 index 000000000..d6d35a1bc --- /dev/null +++ b/hydro/handler/system.js @@ -0,0 +1,91 @@ +const domain = require('../model/domain'); +const system = require('../model/system'); +const setting = require('../model/setting'); +const { Route, Handler } = require('../service/server'); +const { PERM_MANAGE } = require('../permission'); +const hpm = require('../lib/hpm'); +const loader = require('../loader'); + +class SystemHandler extends Handler { + async prepare({ domainId }) { + this.checkPerm(PERM_MANAGE); + this.domain = await domain.get(domainId); + } +} + +class SystemDashboardHandler extends SystemHandler { + async get() { + const path = [ + ['Hydro', '/'], + ['manage', '/manage'], + ['manage_dashboard', null], + ]; + this.response.template = 'manage_dashboard.html'; + this.response.body = { domain: this.domain, path }; + } +} + +class SystemModuleHandler extends SystemHandler { + async get() { + const installed = await hpm.getInstalled(); + const path = [ + ['Hydro', '/'], + ['manage', '/manage'], + ['manage_module', null], + ]; + this.response.body = { installed, active: loader.active, path }; + this.response.template = 'manage_module.html'; + } + + async postInstall({ url, id }) { + await hpm.install(url, id); + this.back(); + } + + async postDelete({ id }) { + await hpm.del(id); + this.back(); + } +} + +class SystemSettingHandler extends SystemHandler { + async get() { + this.response.template = 'manage_settings.html'; + const current = {}; + const settings = setting.SYSTEM_SETTINGS; + for (const s of settings) { + current[s.key] = await system.get(s.key); + } + this.response.body = { + current, settings, + }; + } + + async post(args) { + const tasks = []; + for (const key in args) { + if (typeof args[key] === 'object') { + const subtasks = []; + for (const sub in args[key]) { + subtasks.push(system.set(`${key}.${sub}`, args[key][sub])); + } + tasks.push(Promise.all(subtasks)); + } else tasks.push(system.set(key, args[key])); + } + await Promise.all(tasks); + this.back(); + } +} + +async function apply() { + Route('/system', module.exports.SystemDashboardHandler); + Route('/system/module', module.exports.SystemModuleHandler); + Route('/system/setting', module.exports.SystemSettingHandler); +} + +global.Hydro.handler.manage = module.exports = { + SystemDashboardHandler, + SystemModuleHandler, + SystemSettingHandler, + apply, +}; diff --git a/hydro/lib/misc.js b/hydro/lib/misc.js index 820eb1a28..93fc0c043 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.js @@ -43,7 +43,7 @@ function size(s, base = 1) { const unit = 1024; const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; for (const unitName of unitNames) { - if (s < unit) return '{0} {1}'.format(Math.round(s), unitName); + if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName); s /= unit; } return '{0} {1}'.format(Math.round(s * unit), unitNames[unitNames.length - 1]); diff --git a/hydro/lib/sysinfo.js b/hydro/lib/sysinfo.js new file mode 100644 index 000000000..ef6cef685 --- /dev/null +++ b/hydro/lib/sysinfo.js @@ -0,0 +1,60 @@ +const systeminformation = require('systeminformation'); +const { size } = require('./misc'); + +const cache = {}; + +async function get() { + const [Cpu, Memory, OsInfo, CurrentLoad, CpuFlags, CpuTemp, Battery] = await Promise.all([ + systeminformation.cpu(), + systeminformation.mem(), + systeminformation.osInfo(), + systeminformation.currentLoad(), + systeminformation.cpuFlags(), + systeminformation.cpuTemperature(), + systeminformation.battery(), + ]); + const cpu = `${Cpu.manufacturer} ${Cpu.brand}`; + const memory = `${size(Memory.active)}/${size(Memory.total)}`; + const osinfo = `${OsInfo.distro} ${OsInfo.release} ${OsInfo.codename} ${OsInfo.kernel} ${OsInfo.arch}`; + const load = `${CurrentLoad.avgload}`; + const flags = CpuFlags; + let battery; + if (!Battery.hasbattery) battery = 'No battery'; + else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`; + const _id = OsInfo.serial; + cache.cpu = cpu; + cache.osinfo = osinfo; + cache.flags = flags; + cache._id = _id; + return { + _id, cpu, memory, osinfo, load, flags, CpuTemp, battery, + }; +} + +async function update() { + const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([ + systeminformation.mem(), + systeminformation.currentLoad(), + systeminformation.cpuTemperature(), + systeminformation.battery(), + ]); + const { + _id, cpu, osinfo, flags, + } = cache; + const memory = `${size(Memory.active)}/${size(Memory.total)}`; + const load = `${CurrentLoad.avgload}`; + let battery; + if (!Battery.hasbattery) battery = 'No battery'; + else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`; + return [ + _id, + { + memory, load, battery, CpuTemp, + }, + { + _id, cpu, memory, osinfo, load, flags, battery, CpuTemp, + }, + ]; +} + +global.Hydro.lib.sysinfo = module.exports = { get, update }; diff --git a/hydro/loader.js b/hydro/loader.js index 2027209f4..6f787aa1b 100644 --- a/hydro/loader.js +++ b/hydro/loader.js @@ -229,11 +229,12 @@ async function load() { const builtinLib = [ 'axios', 'download', 'i18n', 'mail', 'markdown', 'md5', 'misc', 'paginate', 'hash.hydro', 'rank', - 'template', 'validator', 'nav', + 'template', 'validator', 'nav', 'sysinfo', ]; for (const i of builtinLib) require(`./lib/${i}`); await lib(); require('./service/gridfs'); + require('./service/monitor'); const server = require('./service/server'); await server.prepare(); await service(); @@ -256,7 +257,7 @@ async function load() { const builtinHandler = [ 'home', 'problem', 'record', 'judge', 'user', 'contest', 'training', 'discussion', 'manage', 'import', - 'fs', + 'fs', 'status', ]; for (const i of builtinHandler) require(`./handler/${i}`); await model(); diff --git a/hydro/model/document.js b/hydro/model/document.js index 1d5a38d1b..c66f7bd29 100644 --- a/hydro/model/document.js +++ b/hydro/model/document.js @@ -23,6 +23,7 @@ async function add( ) { const _id = new ObjectID(); const doc = { + _id, content, owner, domainId, @@ -53,8 +54,10 @@ async function set(domainId, docType, docId, args) { } function deleteOne(domainId, docType, docId) { - // TODO(twd2): delete status? - return coll.deleteOne({ domainId, docType, docId }); + return Promise.all([ + coll.deleteMany({ domainId, docType, docId }), + coll.deleteOne({ domainId, docType, docId }), + ]); } function deleteMulti(domainId, docType, args = {}) { @@ -134,9 +137,7 @@ async function getSub(domainId, docType, docId, key, subId) { }); if (!doc) return [null, null]; for (const sdoc of doc[key] || []) { - if (sdoc._id === subId) { - return [doc, sdoc]; - } + if (sdoc._id === subId) return [doc, sdoc]; } return [doc, null]; } diff --git a/hydro/model/user.js b/hydro/model/user.js index fc4ecb360..36f3dd6ae 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -1,3 +1,4 @@ +const builtin = require('./builtin'); const document = require('./document'); const system = require('./system'); const { UserNotFoundError, UserAlreadyExistError } = require('../error'); @@ -40,15 +41,15 @@ class USER { async function getPerm(domainId, udoc) { if (udoc._id === 1) { // Is Guest const p = await document.get(domainId, document.TYPE_DOMAIN_ROLE, 'guest'); - return p.content; + return p ? p.content : builtin.BUILTIN_ROLES.guest.perm; } - if (udoc.priv === 1) { + if (udoc.priv === 1) { // Is superadmin const p = await document.get(domainId, document.TYPE_DOMAIN_ROLE, 'admin'); - return p.content; + return p ? p.content : builtin.BUILTIN_ROLES.admin.perm; } const role = await document.getStatus(domainId, document.TYPE_DOMAIN_ROLE, 0, udoc._id); - const p = await document.get(domainId, document.TYPE_DOMAIN_ROLE, role.role || 'default'); - return p.content; + const p = await document.get(domainId, document.TYPE_DOMAIN_ROLE, (role || {}).role || 'default'); + return p ? p.content : builtin.BUILTIN_ROLES.default.perm; } async function getById(domainId, _id) { diff --git a/hydro/service/monitor.js b/hydro/service/monitor.js new file mode 100644 index 000000000..8378289b7 --- /dev/null +++ b/hydro/service/monitor.js @@ -0,0 +1,25 @@ +const db = require('./db'); +const sysinfo = require('../lib/sysinfo'); + +const coll = db.collection('status'); + +async function update() { + const [_id, $set] = await sysinfo.update(); + await coll.updateOne( + { _id }, + { $set: { ...$set, updateAt: new Date() } }, + { upsert: true }, + ); +} + +async function postInit() { + const info = await sysinfo.get(); + await coll.updateOne( + { _id: info._id }, + { $set: { ...info, updateAt: new Date() } }, + { upsert: true }, + ); + setInterval(update, 3 * 60 * 1000); +} + +global.Hydro.service.monitor = module.exports = { postInit }; diff --git a/module/contest-rule-homework/homework.js b/module/contest-rule-homework/homework.js deleted file mode 100644 index 7e24c5a26..000000000 --- a/module/contest-rule-homework/homework.js +++ /dev/null @@ -1,95 +0,0 @@ -module.exports = { - TEXT: 'Assignment', - check() {}, - stat(tdoc, journal) { - const effective = {}; - const detail = []; - let score = 0; - let time = 0; - for (const j in journal) { - if (tdoc.pids.includes(j.pid) - && !(effective.includes(j.pid) && effective[j.pid].accept)) { - effective[j.pid] = j; - } - } - const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000); - for (const j in effective) { - detail.push({ - ...effective[j], - time: _time(effective[j]), - }); - } - for (const d of detail) { - score += d.score; - time += d.time; - } - return { - score, time, detail, - }; - }, - scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ - { type: 'rank', value: _('Rank') }, - { type: 'user', value: _('User') }, - { type: 'display_name', value: _('Display Name') }, - { type: 'total_score', value: _('Score') }, - ]; - if (isExport) { - columns.push( - { type: 'total_original_score', value: _('Original Score') }, - { type: 'total_time', value: _('Total Time (Seconds)') }, - ); - } - columns.push({ type: 'total_time_str', value: _('Total Time') }); - for (const i in tdoc.pids) { - if (isExport) { - columns.push( - { type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) }, - { type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) }, - { type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) }, - { type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) }, - ); - } else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] }); - } - const rows = [columns]; - for (const [rank, tsdoc] in rankedTsdocs) { - const tsddict = {}; - if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } - const row = [ - { type: 'string', value: rank }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, - { type: 'string', value: tsdoc.penalty_score || 0 }, - ]; - if (isExport) { - row.push( - { type: 'string', value: tsdoc.score || 0 }, - { type: 'string', value: tsdoc.time || 0.0 }, - ); - } - row.push({ type: 'string', value: tsdoc.time || 0 }); - for (const pid of tdoc.pids) { - const rdoc = (tsddict[pid] || {}).rid || null; - const colScore = (tsddict[pid] || {}).penalty_score || '-'; - const colOriginalScore = (tsddict[pid] || {}).score || '-'; - const colTime = (tsddict[pid] || {}).time || '-'; - const colTimeStr = colTime !== '-' ? colTime : '-'; - if (isExport) { - row.push( - { type: 'string', value: colScore }, - { type: 'string', value: colOriginalScore }, - { type: 'string', value: colTime }, - { type: 'string', value: colTimeStr }, - ); - } else { - row.push({ - type: 'record', - value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), - raw: rdoc, - }); - } - } - rows.push(row); - } - return rows; - }, -}; diff --git a/module/contest-rule-homework/hydro.json b/module/contest-rule-homework/hydro.json deleted file mode 100644 index 7a73a2d71..000000000 --- a/module/contest-rule-homework/hydro.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": "contest-rule-homework", - "version": "1.0.0" -} \ No newline at end of file diff --git a/module/contest-rule-homework/model.js b/module/contest-rule-homework/model.js deleted file mode 100644 index 55963a3a2..000000000 --- a/module/contest-rule-homework/model.js +++ /dev/null @@ -1,3 +0,0 @@ -const homework = require('./homework'); - -global.Hydro.model.contest.RULES.homework = homework; diff --git a/module/migrate-vijos/script.js b/module/migrate-vijos/script.js index 6791e47b8..32a61976d 100644 --- a/module/migrate-vijos/script.js +++ b/module/migrate-vijos/script.js @@ -56,6 +56,7 @@ const tasks = { reply: 'reply', parent_doc_id: 'parentId', parent_doc_type: 'parentType', + num_replies: 'nReply', views: 'views', highlight: 'highlight', ip: 'ip', @@ -244,8 +245,21 @@ async function task(name, src, report) { for (let i = 0; i <= total; i++) { const docs = await cursor[name](src).skip(i * 50).limit(50).toArray(); const res = []; - for (const doc of docs) res.push(tasks[name](doc)); - if (res.length) await dst.collection(name).insertMany(await Promise.all(res)); + for (const doc of docs) { + const d = await tasks[name](doc); + if (d) { + if (d.docId && d.docType) { + res.push(dst.collection(name).updateOne({ + docId: d.docId, docType: d.docType, + }, { $set: d }, { upsert: true })); + } else if (d._id) { + res.push(dst.collection(name).updateOne({ + _id: d._id, + }, { $set: d }, { upsert: true })); + } else res.push(dst.collection(name).insertOne(d)); + } + } + await Promise.all(res); await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) }); } await fixPid(report); diff --git a/package.json b/package.json index 94a834d56..2f7ef5fa6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "nodemailer": "^6.3.1", "nunjucks": "^3.2.1", "prismjs": "^1.20.0", - "sockjs": "^0.3.20" + "sockjs": "^0.3.20", + "systeminformation": "^4.26.5" }, "devDependencies": { "eslint": "^6.8.0", @@ -39,12 +40,10 @@ }, "pkg": { "scripts": [ - "hydro/**.js" + ".build/development.js" ], "assets": [ - ".uibuild/*", - "locales/*", - "templates/*" + ".build/*" ] } } \ No newline at end of file diff --git a/templates/status.html b/templates/status.html new file mode 100644 index 000000000..f6f05d343 --- /dev/null +++ b/templates/status.html @@ -0,0 +1,43 @@ +{% set page_name = "status" %} +{% extends "layout/basic.html" %} +{% block content %} +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + {% for doc in stats %} + + + + + + + + {% endfor %} + +
{{ _('ID') }}{{ _('Current Status') }}{{ _('OS Info') }}{{ _('CPU Info') }}{{ _('Memory Info') }}
{{ doc._id|truncate(6, True, '') }}{{ doc.status }}{{ doc.osinfo }}{{ doc.cpu }}{{ doc.memory }}
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/ui/pages/status.page.styl b/ui/pages/status.page.styl new file mode 100644 index 000000000..63122dc22 --- /dev/null +++ b/ui/pages/status.page.styl @@ -0,0 +1,9 @@ +.page--status + .col--id + width: 110px + + .col--status + width: 100px + + .col--memory + width: 150px diff --git a/yarn.lock b/yarn.lock index 29079df19..2d04ce96d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3786,6 +3786,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +systeminformation@^4.26.5: + version "4.26.5" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.26.5.tgz#d2dc77d4cc54b38345d552fcfd00344f49a964a4" + integrity sha512-hTVhCYNIsoDtL8brW6wg/UTHmyRfMRItd/+6f6JZtXjmx06FHKBGCyBe7WN6J+FiDIiDfTK7mvMq7hljM6r5OA== + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"