Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
055fcc6b
Unverified
Commit
055fcc6b
authored
Oct 15, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: admin instances
parent
7aa4a0e9
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
269 additions
and
4 deletions
+269
-4
system.js
server/graph/resolvers/system.js
+35
-2
system.graphql
server/graph/schemas/system.graphql
+11
-0
index.js
server/index.js
+1
-1
fluent-network.svg
ux/public/_assets/icons/fluent-network.svg
+2
-0
en.json
ux/src/i18n/locales/en.json
+7
-1
AdminLayout.vue
ux/src/layouts/AdminLayout.vue
+4
-0
AdminInstances.vue
ux/src/pages/AdminInstances.vue
+208
-0
routes.js
ux/src/router/routes.js
+1
-0
No files found.
server/graph/resolvers/system.js
View file @
055fcc6b
...
...
@@ -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
()
...
...
server/graph/schemas/system.graphql
View file @
055fcc6b
...
...
@@ -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
...
...
server/index.js
View file @
055fcc6b
...
...
@@ -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!'
)
...
...
ux/public/_assets/icons/fluent-network.svg
0 → 100644
View file @
055fcc6b
<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>
\ No newline at end of file
ux/src/i18n/locales/en.json
View file @
055fcc6b
...
...
@@ -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"
}
ux/src/layouts/AdminLayout.vue
View file @
055fcc6b
...
...
@@ -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')
...
...
ux/src/pages/AdminInstances.vue
0 → 100644
View file @
055fcc6b
<
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
>
ux/src/router/routes.js
View file @
055fcc6b
...
...
@@ -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'
)
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment