feat: admin instances

pull/6078/head
Nicolas Giard 2 years ago
parent 7aa4a0e9ee
commit 055fcc6b72
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -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()

@ -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

@ -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!')

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="k2yiUqNWn2Slk9gpK~j2Da" x1="14.104" x2="33.763" y1="42.408" y2="5.435" gradientTransform="matrix(1 0 0 -1 0 47.89)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c8d3de"/><stop offset="1" stop-color="#c8d3de"/></linearGradient><path fill="url(#k2yiUqNWn2Slk9gpK~j2Da)" d="M35.7,40H12.3C9.9,40,8,38.1,8,35.7V12.3C8,9.9,9.9,8,12.3,8h23.3c2.4,0,4.3,1.9,4.3,4.3v23.3 C40,38.1,38.1,40,35.7,40z M12.3,10C11,10,10,11,10,12.3v23.3c0,1.3,1,2.3,2.3,2.3h23.3c1.3,0,2.3-1,2.3-2.3V12.3 c0-1.3-1-2.3-2.3-2.3H12.3z"/><linearGradient id="k2yiUqNWn2Slk9gpK~j2Db" x1="5.936" x2="12.063" y1="29.652" y2="18.128" gradientTransform="matrix(1 0 0 -1 0 47.89)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2aa4f4"/><stop offset="1" stop-color="#007ad9"/></linearGradient><path fill="url(#k2yiUqNWn2Slk9gpK~j2Db)" d="M15,27c0,1.1-0.9,2-2,2H5c-1.1,0-2-0.9-2-2v-6c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2V27z"/><linearGradient id="k2yiUqNWn2Slk9gpK~j2Dc" x1="20.936" x2="27.064" y1="44.652" y2="33.129" gradientTransform="matrix(1 0 0 -1 0 47.89)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2aa4f4"/><stop offset="1" stop-color="#007ad9"/></linearGradient><path fill="url(#k2yiUqNWn2Slk9gpK~j2Dc)" d="M30,12c0,1.1-0.9,2-2,2h-8c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2V12z"/><linearGradient id="k2yiUqNWn2Slk9gpK~j2Dd" x1="20.936" x2="27.064" y1="14.652" y2="3.128" gradientTransform="matrix(1 0 0 -1 0 47.89)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2aa4f4"/><stop offset="1" stop-color="#007ad9"/></linearGradient><path fill="url(#k2yiUqNWn2Slk9gpK~j2Dd)" d="M30,42c0,1.1-0.9,2-2,2h-8c-1.1,0-2-0.9-2-2v-6c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2V42z"/><linearGradient id="k2yiUqNWn2Slk9gpK~j2De" x1="35.937" x2="42.063" y1="29.652" y2="18.128" gradientTransform="matrix(1 0 0 -1 0 47.89)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2aa4f4"/><stop offset="1" stop-color="#007ad9"/></linearGradient><path fill="url(#k2yiUqNWn2Slk9gpK~j2De)" d="M45,27c0,1.1-0.9,2-2,2h-8c-1.1,0-2-0.9-2-2v-6c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2V27z"/><rect width="8" height="6" x="5" y="21" fill="#50e6ff"/><rect width="8" height="6" x="20" y="6" fill="#50e6ff"/><rect width="8" height="6" x="20" y="36" fill="#50e6ff"/><rect width="8" height="6" x="35" y="21" fill="#50e6ff"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -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"
}

@ -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')

@ -0,0 +1,208 @@
<template lang='pug'>
q-page.admin-terminal
.row.q-pa-md.items-center
.col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-network.svg')
.col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ t('admin.instances.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.instances.subtitle') }}
.col-auto.flex
q-btn.q-mr-sm.acrylic-btn(
icon='las la-question-circle'
flat
color='grey'
:href='siteStore.docsBase + `/admin/instances`'
target='_blank'
type='a'
)
q-btn.q-mr-sm.acrylic-btn(
icon='las la-redo-alt'
flat
color='secondary'
:loading='state.loading > 0'
@click='load'
)
q-separator(inset)
.q-pa-md.q-gutter-md
q-card.shadow-1
q-table(
:rows='state.instances'
:columns='instancesHeaders'
row-key='name'
flat
hide-bottom
:rows-per-page-options='[0]'
:loading='state.loading > 0'
)
template(v-slot:body-cell-icon='props')
q-td(:props='props')
q-icon(name='las la-server', color='positive', size='sm')
template(v-slot:body-cell-id='props')
q-td(:props='props')
strong {{props.value}}
div: small.text-grey: strong {{props.row.ip}}
div: small.text-grey {{props.row.dbUser}}
template(v-slot:body-cell-cons='props')
q-td(:props='props')
q-chip(
icon='las la-plug'
square
size='md'
color='blue'
text-color='white'
)
span.font-robotomono {{ props.value }}
template(v-slot:body-cell-subs='props')
q-td(:props='props')
q-chip(
icon='las la-broadcast-tower'
square
size='md'
color='green'
text-color='white'
)
small.text-uppercase {{ props.value }}
template(v-slot:body-cell-firstseen='props')
q-td(:props='props')
span {{props.value}}
div: small.text-grey {{humanizeDate(props.row.dbFirstSeen)}}
template(v-slot:body-cell-lastseen='props')
q-td(:props='props')
span {{props.value}}
div: small.text-grey {{humanizeDate(props.row.dbLastSeen)}}
</template>
<script setup>
import { onMounted, reactive } from 'vue'
import { useMeta, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import gql from 'graphql-tag'
import { DateTime, Duration, Interval } from 'luxon'
import { useSiteStore } from 'src/stores/site'
// QUASAR
const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('admin.instances.title')
})
// DATA
const state = reactive({
instances: [],
loading: 0
})
const instancesHeaders = [
{
align: 'center',
field: 'id',
name: 'icon',
sortable: false,
style: 'width: 15px; padding-right: 0;'
},
{
label: t('common.field.id'),
align: 'left',
field: 'id',
name: 'id',
sortable: true
},
{
label: t('admin.instances.activeConnections'),
align: 'left',
field: 'activeConnections',
name: 'cons',
sortable: true
},
{
label: t('admin.instances.activeListeners'),
align: 'left',
field: 'activeListeners',
name: 'subs',
sortable: true
},
{
label: t('admin.instances.firstSeen'),
align: 'left',
field: 'dbFirstSeen',
name: 'firstseen',
sortable: true,
format: v => DateTime.fromISO(v).toRelative()
},
{
label: t('admin.instances.lastSeen'),
align: 'left',
field: 'dbLastSeen',
name: 'lastseen',
sortable: true,
format: v => DateTime.fromISO(v).toRelative()
}
]
// METHODS
function humanizeDate (val) {
return DateTime.fromISO(val).toFormat('fff')
}
function humanizeDuration (start, end) {
const dur = Interval.fromDateTimes(DateTime.fromISO(start), DateTime.fromISO(end))
.toDuration(['hours', 'minutes', 'seconds', 'milliseconds'])
return Duration.fromObject({
...dur.hours > 0 && { hours: dur.hours },
...dur.minutes > 0 && { minutes: dur.minutes },
...dur.seconds > 0 && { seconds: dur.seconds },
...dur.milliseconds > 0 && { milliseconds: dur.milliseconds }
}).toHuman({ unitDisplay: 'narrow', listStyle: 'short' })
}
async function load () {
state.loading++
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSystemInstances {
systemInstances {
id
activeConnections
activeListeners
dbUser
dbFirstSeen
dbLastSeen
ip
}
}
`,
fetchPolicy: 'network-only'
})
state.instances = resp?.data?.systemInstances
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to load list of instances.',
caption: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(() => {
load()
})
</script>

@ -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') },

Loading…
Cancel
Save