From 055fcc6b7256bdf427f160d9d82788a2e02bdd17 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Sat, 15 Oct 2022 03:09:14 +0000 Subject: [PATCH] feat: admin instances --- server/graph/resolvers/system.js | 37 +++- server/graph/schemas/system.graphql | 11 ++ server/index.js | 2 +- ux/public/_assets/icons/fluent-network.svg | 1 + ux/src/i18n/locales/en.json | 8 +- ux/src/layouts/AdminLayout.vue | 4 + ux/src/pages/AdminInstances.vue | 208 +++++++++++++++++++++ ux/src/router/routes.js | 1 + 8 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 ux/public/_assets/icons/fluent-network.svg create mode 100644 ux/src/pages/AdminInstances.vue diff --git a/server/graph/resolvers/system.js b/server/graph/resolvers/system.js index c4642c6c..6b5fbed8 100644 --- a/server/graph/resolvers/system.js +++ b/server/graph/resolvers/system.js @@ -23,13 +23,46 @@ module.exports = { } return exts }, + async systemInstances () { + const instRaw = await WIKI.db.knex('pg_stat_activity') + .select([ + 'usename', + 'client_addr', + 'application_name', + 'backend_start', + 'state_change' + ]) + .where('datname', WIKI.db.knex.client.connectionSettings.database) + .andWhereLike('application_name', 'Wiki.js%') + const insts = {} + for (const inst of instRaw) { + const instId = inst.application_name.substring(10, 20) + const conType = inst.application_name.includes(':MAIN') ? 'main' : 'sub' + const curInst = insts[instId] ?? { + activeConnections: 0, + activeListeners: 0, + dbFirstSeen: inst.backend_start, + dbLastSeen: inst.state_change + } + insts[instId] = { + id: instId, + activeConnections: conType === 'main' ? curInst.activeConnections + 1 : curInst.activeConnections, + activeListeners: conType === 'sub' ? curInst.activeListeners + 1 : curInst.activeListeners, + dbUser: inst.usename, + dbFirstSeen: curInst.dbFirstSeen > inst.backend_start ? inst.backend_start : curInst.dbFirstSeen, + dbLastSeen: curInst.dbLastSeen < inst.state_change ? inst.state_change : curInst.dbLastSeen, + ip: inst.client_addr + } + } + return _.values(insts) + }, systemSecurity () { return WIKI.config.security }, async systemJobs (obj, args) { const results = args.states?.length > 0 ? - await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt') : - await WIKI.db.knex('jobHistory').orderBy('startedAt') + await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt', 'desc') : + await WIKI.db.knex('jobHistory').orderBy('startedAt', 'desc') return results.map(r => ({ ...r, state: r.state.toUpperCase() diff --git a/server/graph/schemas/system.graphql b/server/graph/schemas/system.graphql index a531fd63..8344d41c 100644 --- a/server/graph/schemas/system.graphql +++ b/server/graph/schemas/system.graphql @@ -6,6 +6,7 @@ extend type Query { systemExtensions: [SystemExtension] systemFlags: [SystemFlag] systemInfo: SystemInfo + systemInstances: [SystemInstance] systemSecurity: SystemSecurity systemJobs( states: [SystemJobState] @@ -93,6 +94,16 @@ type SystemInfo { workingDirectory: String } +type SystemInstance { + id: String + activeConnections: Int + activeListeners: Int + dbUser: String + dbFirstSeen: Date + dbLastSeen: Date + ip: String +} + enum SystemImportUsersGroupMode { MULTI SINGLE diff --git a/server/index.js b/server/index.js index 392ba05d..f3542cc0 100644 --- a/server/index.js +++ b/server/index.js @@ -4,9 +4,9 @@ // =========================================== const path = require('path') -const { nanoid } = require('nanoid') const { DateTime } = require('luxon') const semver = require('semver') +const nanoid = require('nanoid').customAlphabet('1234567890abcdef', 10) if (!semver.satisfies(process.version, '>=18')) { console.error('ERROR: Node.js 18.x or later required!') diff --git a/ux/public/_assets/icons/fluent-network.svg b/ux/public/_assets/icons/fluent-network.svg new file mode 100644 index 00000000..e6c3227c --- /dev/null +++ b/ux/public/_assets/icons/fluent-network.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/src/i18n/locales/en.json b/ux/src/i18n/locales/en.json index 607ac088..fbbe1fa6 100644 --- a/ux/src/i18n/locales/en.json +++ b/ux/src/i18n/locales/en.json @@ -1534,5 +1534,11 @@ "admin.scheduler.completedIn": "Completed in {duration}", "admin.scheduler.pending": "Pending", "admin.scheduler.error": "Error", - "admin.scheduler.interrupted": "Interrupted" + "admin.scheduler.interrupted": "Interrupted", + "admin.instances.title": "Instances", + "admin.instances.subtitle": "View a list of active instances", + "admin.instances.lastSeen": "Last Seen", + "admin.instances.firstSeen": "First Seen", + "admin.instances.activeListeners": "Active Listeners", + "admin.instances.activeConnections": "Active Connections" } diff --git a/ux/src/layouts/AdminLayout.vue b/ux/src/layouts/AdminLayout.vue index deed18db..2129e9ff 100644 --- a/ux/src/layouts/AdminLayout.vue +++ b/ux/src/layouts/AdminLayout.vue @@ -146,6 +146,10 @@ q-layout.admin(view='hHh Lpr lff') q-item-section(avatar) q-icon(name='img:/_assets/icons/fluent-module.svg') q-item-section {{ t('admin.extensions.title') }} + q-item(to='/_admin/instances', v-ripple, active-class='bg-primary text-white') + q-item-section(avatar) + q-icon(name='img:/_assets/icons/fluent-network.svg') + q-item-section {{ t('admin.instances.title') }} q-item(to='/_admin/mail', v-ripple, active-class='bg-primary text-white') q-item-section(avatar) q-icon(name='img:/_assets/icons/fluent-message-settings.svg') diff --git a/ux/src/pages/AdminInstances.vue b/ux/src/pages/AdminInstances.vue new file mode 100644 index 00000000..92bb7e33 --- /dev/null +++ b/ux/src/pages/AdminInstances.vue @@ -0,0 +1,208 @@ + + + diff --git a/ux/src/router/routes.js b/ux/src/router/routes.js index 2e372e55..ef86f276 100644 --- a/ux/src/router/routes.js +++ b/ux/src/router/routes.js @@ -46,6 +46,7 @@ const routes = [ // -> System { path: 'api', component: () => import('pages/AdminApi.vue') }, { path: 'extensions', component: () => import('pages/AdminExtensions.vue') }, + { path: 'instances', component: () => import('pages/AdminInstances.vue') }, { path: 'mail', component: () => import('pages/AdminMail.vue') }, { path: 'scheduler', component: () => import('pages/AdminScheduler.vue') }, { path: 'security', component: () => import('pages/AdminSecurity.vue') },