mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Admin tools: introduce Activ Projects tab on the Manage Site page
- code moved to modules/admin-tools (yu-i-i)
This commit is contained in:
@@ -14,6 +14,7 @@ block content
|
|||||||
.nav-tabs-container
|
.nav-tabs-container
|
||||||
ul.nav.nav-tabs.align-left(role='tablist')
|
ul.nav.nav-tabs.align-left(role='tablist')
|
||||||
+bookmarkable-tabset-header('system-messages', 'System Messages', true)
|
+bookmarkable-tabset-header('system-messages', 'System Messages', true)
|
||||||
|
+bookmarkable-tabset-header('active-projects', 'Active Projects')
|
||||||
+bookmarkable-tabset-header('open-sockets', 'Open Sockets')
|
+bookmarkable-tabset-header('open-sockets', 'Open Sockets')
|
||||||
+bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
|
+bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
|
||||||
+bookmarkable-tabset-header('privileges-matrix', 'Privileges Matrix')
|
+bookmarkable-tabset-header('privileges-matrix', 'Privileges Matrix')
|
||||||
@@ -42,6 +43,10 @@ block content
|
|||||||
input(name='_csrf' type='hidden' value=csrfToken)
|
input(name='_csrf' type='hidden' value=csrfToken)
|
||||||
button.btn.btn-danger(type='submit') Clear all messages
|
button.btn.btn-danger(type='submit') Clear all messages
|
||||||
|
|
||||||
|
.tab-pane(role='tabpanel' id='active-projects')
|
||||||
|
.row-spaced
|
||||||
|
include ../../../modules/admin-tools/app/views/active-projects.pug
|
||||||
|
|
||||||
.tab-pane(role='tabpanel' id='open-sockets')
|
.tab-pane(role='tabpanel' id='open-sockets')
|
||||||
.row-spaced
|
.row-spaced
|
||||||
ul
|
ul
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { expressify } from '@overleaf/promise-utils'
|
||||||
|
import ProjectGetter from '../../../../app/src/Features/Project/ProjectGetter.mjs'
|
||||||
|
import UserGetter from '../../../../app/src/Features/User/UserGetter.mjs'
|
||||||
|
import request from 'request'
|
||||||
|
import Settings from '@overleaf/settings'
|
||||||
|
import logger from '@overleaf/logger'
|
||||||
|
|
||||||
|
// Helper function to get all active projects from real-time service
|
||||||
|
async function getActiveProjectsFromRealTime() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const realTimeUrl = Settings.apis.realTime?.url || 'http://127.0.0.1:3026'
|
||||||
|
const url = `${realTimeUrl}/clients`
|
||||||
|
const user = Settings.apis.realTime?.user || process.env.WEB_API_USER || 'overleaf'
|
||||||
|
const pass = Settings.apis.realTime?.pass || process.env.WEB_API_PASSWORD || ''
|
||||||
|
|
||||||
|
logger.info({ url, user }, 'Fetching active clients from real-time service')
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
url,
|
||||||
|
auth: { user, pass, sendImmediately: true },
|
||||||
|
json: true,
|
||||||
|
timeout: 10000
|
||||||
|
}, async (error, response, body) => {
|
||||||
|
if (error) {
|
||||||
|
logger.error({ err: error }, 'Error getting active clients from real-time')
|
||||||
|
return resolve([])
|
||||||
|
}
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
logger.warn({ statusCode: response.statusCode }, 'Unexpected response from real-time service')
|
||||||
|
return resolve([])
|
||||||
|
}
|
||||||
|
if (!Array.isArray(body)) {
|
||||||
|
logger.warn({ body }, 'Unexpected response body from real-time service')
|
||||||
|
return resolve([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group flat client list by project_id
|
||||||
|
const projectMap = new Map()
|
||||||
|
for (const client of body) {
|
||||||
|
if (!projectMap.has(client.project_id)) {
|
||||||
|
projectMap.set(client.project_id, [])
|
||||||
|
}
|
||||||
|
projectMap.get(client.project_id).push(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ projectCount: projectMap.size }, 'Got active projects from real-time')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const enrichedProjects = await Promise.all(
|
||||||
|
[...projectMap.entries()].map(async ([projectId, clients]) => {
|
||||||
|
try {
|
||||||
|
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||||
|
name: 1,
|
||||||
|
_id: 1,
|
||||||
|
owner_ref: 1,
|
||||||
|
})
|
||||||
|
if (!project) {
|
||||||
|
logger.warn({ projectId }, 'Project not found in database')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const owner = await UserGetter.promises.getUser(project.owner_ref, {
|
||||||
|
email: 1,
|
||||||
|
first_name: 1,
|
||||||
|
last_name: 1,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
id: project._id.toString(),
|
||||||
|
name: project.name,
|
||||||
|
owner: owner ? {
|
||||||
|
email: owner.email,
|
||||||
|
name: `${owner.first_name || ''} ${owner.last_name || ''}`.trim() || owner.email,
|
||||||
|
} : null,
|
||||||
|
activeUsers: clients.map(c => ({
|
||||||
|
name: `${c.first_name} ${c.last_name}`.trim() || c.email || 'Unknown',
|
||||||
|
email: c.email,
|
||||||
|
})),
|
||||||
|
connectionCount: clients.length,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err, projectId }, 'Error enriching project data')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
resolve(enrichedProjects.filter(p => p !== null))
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, 'Error processing active projects')
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activeProjects(req, res) {
|
||||||
|
const activeProjects = await getActiveProjectsFromRealTime()
|
||||||
|
res.json(activeProjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
activeProjects: expressify(activeProjects),
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import logger from '@overleaf/logger'
|
import logger from '@overleaf/logger'
|
||||||
import UserListController from './UserListController.mjs'
|
import UserListController from './UserListController.mjs'
|
||||||
import ProjectListController from './ProjectListController.mjs'
|
import ProjectListController from './ProjectListController.mjs'
|
||||||
|
import AdminToolsController from './AdminToolsController.mjs'
|
||||||
import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
|
import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
|
||||||
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.mjs'
|
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.mjs'
|
||||||
|
|
||||||
@@ -77,5 +78,10 @@ export default {
|
|||||||
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
ProjectListController.undeleteProject
|
ProjectListController.undeleteProject
|
||||||
)
|
)
|
||||||
|
|
||||||
|
webRouter.get('/admin/active-projects',
|
||||||
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
|
AdminToolsController.activeProjects,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
meta(name="ol-dummy", content="placeholder")
|
||||||
|
|
||||||
|
h3 Active Projects (Currently Being Edited)
|
||||||
|
p.text-muted Real-time information from the editor service showing who is actively working on which projects.
|
||||||
|
#active-projects-container
|
||||||
|
|
||||||
|
each file in entrypointScripts('modules/admin-tools/pages/active-projects')
|
||||||
|
script(type='text/javascript' nonce=scriptNonce src=file)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const container = document.getElementById('active-projects-container')
|
||||||
|
const tabLink = document.querySelector('a[href="#active-projects"]')
|
||||||
|
const tabPane = document.getElementById('active-projects')
|
||||||
|
|
||||||
|
if (!container || !tabPane) return
|
||||||
|
|
||||||
|
let isLoaded = false
|
||||||
|
let isLoading = false
|
||||||
|
|
||||||
|
function loadActiveProjects() {
|
||||||
|
if (isLoaded || isLoading) return
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
// show loading only if tab is visible
|
||||||
|
if (tabPane.classList.contains('active')) {
|
||||||
|
container.innerHTML = '<p class="text-muted">Loading...</p>'
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/admin/active-projects')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
container.innerHTML = renderTable(data)
|
||||||
|
isLoaded = true
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
container.innerHTML =
|
||||||
|
'<p class="text-danger">Failed to load active projects</p>'
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// preload immediately when /admin page opens
|
||||||
|
loadActiveProjects()
|
||||||
|
|
||||||
|
// ensure data is loaded when tab is opened
|
||||||
|
if (tabLink) {
|
||||||
|
tabLink.addEventListener('shown.bs.tab', () => {
|
||||||
|
if (!isLoaded) loadActiveProjects()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
return `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span style="color: green;">✔</span>
|
||||||
|
<strong> Great news!</strong>
|
||||||
|
No projects are currently being edited.
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = data.map(project => {
|
||||||
|
const owner = project.owner || {}
|
||||||
|
const users = project.activeUsers || []
|
||||||
|
|
||||||
|
const usersHtml = users.length
|
||||||
|
? `<ul class="list-unstyled mb-0">${users.map(u =>
|
||||||
|
`<li><strong>${u.name}</strong>${u.email ? ` (${u.email})` : ''}</li>`
|
||||||
|
).join('')}</ul>`
|
||||||
|
: '<em>None detected</em>'
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td><a href="/project/${project.id}" target="_blank">${project.name}</a></td>
|
||||||
|
<td>${owner.name || 'Unknown'}</td>
|
||||||
|
<td>${
|
||||||
|
owner.email
|
||||||
|
? `<a href="mailto:${owner.email}">${owner.email}</a>`
|
||||||
|
: 'N/A'
|
||||||
|
}</td>
|
||||||
|
<td>${usersHtml}</td>
|
||||||
|
<td>${project.connectionCount}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
return `
|
||||||
|
<p class="small">
|
||||||
|
<strong>${data.length}</strong> project(s) currently being edited
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Project Name</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Owner Email</th>
|
||||||
|
<th>Active Users</th>
|
||||||
|
<th>Connections</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user