mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Admin tools: improve Active Projects UI
- do not show the project owner, email and connections number columns - add mailto: all active users of a project when clicking the project title - fix error handling
This commit is contained in:
@@ -1,99 +1,90 @@
|
|||||||
import { expressify } from '@overleaf/promise-utils'
|
import { expressify } from '@overleaf/promise-utils'
|
||||||
import ProjectGetter from '../../../../app/src/Features/Project/ProjectGetter.mjs'
|
import ProjectGetter from '../../../../app/src/Features/Project/ProjectGetter.mjs'
|
||||||
import UserGetter from '../../../../app/src/Features/User/UserGetter.mjs'
|
import { fetchJson } from '@overleaf/fetch-utils'
|
||||||
import request from 'request'
|
|
||||||
import Settings from '@overleaf/settings'
|
import Settings from '@overleaf/settings'
|
||||||
import logger from '@overleaf/logger'
|
import logger from '@overleaf/logger'
|
||||||
|
import OError from '@overleaf/o-error'
|
||||||
|
|
||||||
|
const REQUEST_TIMEOUT_MS = 10000
|
||||||
|
|
||||||
// Helper function to get all active projects from real-time service
|
// Helper function to get all active projects from real-time service
|
||||||
async function getActiveProjectsFromRealTime() {
|
async function getActiveProjectsFromRealTime() {
|
||||||
return new Promise((resolve, reject) => {
|
const realTimeUrl = Settings.apis.realTime?.url || 'http://127.0.0.1:3026'
|
||||||
const realTimeUrl = Settings.apis.realTime?.url || 'http://127.0.0.1:3026'
|
const url = `${realTimeUrl}/clients`
|
||||||
const url = `${realTimeUrl}/clients`
|
const user = Settings.apis.realTime?.user || process.env.WEB_API_USER || 'overleaf'
|
||||||
const user = Settings.apis.realTime?.user || process.env.WEB_API_USER || 'overleaf'
|
const password = Settings.apis.realTime?.pass || process.env.WEB_API_PASSWORD || ''
|
||||||
const pass = Settings.apis.realTime?.pass || process.env.WEB_API_PASSWORD || ''
|
|
||||||
|
|
||||||
logger.info({ url, user }, 'Fetching active clients from real-time service')
|
logger.debug({ url, user }, 'Fetching active clients from real-time service')
|
||||||
|
|
||||||
request.get({
|
let clients
|
||||||
url,
|
try {
|
||||||
auth: { user, pass, sendImmediately: true },
|
clients = await fetchJson(url, {
|
||||||
json: true,
|
basicAuth: {
|
||||||
timeout: 10000
|
user,
|
||||||
}, async (error, response, body) => {
|
password,
|
||||||
if (error) {
|
},
|
||||||
logger.error({ err: error }, 'Error getting active clients from real-time')
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
||||||
return resolve([])
|
})
|
||||||
}
|
} catch (err) {
|
||||||
if (response.statusCode !== 200) {
|
throw new OError('Error getting active clients from real-time', {}, err)
|
||||||
logger.warn({ statusCode: response.statusCode }, 'Unexpected response from real-time service')
|
}
|
||||||
return resolve([])
|
if (!Array.isArray(clients)) {
|
||||||
}
|
throw new OError('Unexpected response body from real-time service', { body: clients })
|
||||||
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()
|
||||||
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')
|
for (const client of clients) {
|
||||||
|
let list = projectMap.get(client.project_id)
|
||||||
|
if (!list) {
|
||||||
|
list = []
|
||||||
|
projectMap.set(client.project_id, list)
|
||||||
|
}
|
||||||
|
list.push(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug({ projectCount: projectMap.size }, 'Got active projects from real-time')
|
||||||
|
|
||||||
|
const enrichedProjects = await Promise.all(
|
||||||
|
[...projectMap.entries()].map(async ([projectId, clients]) => {
|
||||||
try {
|
try {
|
||||||
const enrichedProjects = await Promise.all(
|
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||||
[...projectMap.entries()].map(async ([projectId, clients]) => {
|
_id: 1,
|
||||||
try {
|
name: 1,
|
||||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
})
|
||||||
name: 1,
|
|
||||||
_id: 1,
|
if (!project) {
|
||||||
owner_ref: 1,
|
throw new OError('Project not found in database', { projectId })
|
||||||
})
|
}
|
||||||
if (!project) {
|
|
||||||
logger.warn({ projectId }, 'Project not found in database')
|
return {
|
||||||
return null
|
id: project._id.toString(),
|
||||||
}
|
name: project.name,
|
||||||
const owner = await UserGetter.promises.getUser(project.owner_ref, {
|
activeUsers: clients.map(client => ({
|
||||||
email: 1,
|
name: `${client.first_name || ''} ${client.last_name || ''}`.trim() || client.email || 'Unknown',
|
||||||
first_name: 1,
|
email: client.email,
|
||||||
last_name: 1,
|
})),
|
||||||
})
|
connectionCount: clients.length,
|
||||||
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) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'Error processing active projects')
|
throw new OError('Error enriching project data', { projectId }, err)
|
||||||
resolve([])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
)
|
||||||
|
|
||||||
|
return enrichedProjects.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activeProjects(req, res) {
|
async function activeProjects(req, res) {
|
||||||
const activeProjects = await getActiveProjectsFromRealTime()
|
try {
|
||||||
res.json(activeProjects)
|
const projects = await getActiveProjectsFromRealTime()
|
||||||
|
res.json(projects)
|
||||||
|
} catch (err) {
|
||||||
|
const info = OError.getFullInfo(err)
|
||||||
|
logger.error(OError.getFullStack(err))
|
||||||
|
logger.error({ info }, 'Error processing active projects')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -55,28 +55,49 @@ function renderTable(data) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = data.map(project => {
|
const rows = data.map((project, projectIndex) => {
|
||||||
const owner = project.owner || {}
|
|
||||||
const users = project.activeUsers || []
|
const users = project.activeUsers || []
|
||||||
|
|
||||||
|
const mailto = users
|
||||||
|
.map(u => u.email)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',')
|
||||||
|
|
||||||
|
const projectName = mailto
|
||||||
|
? `<a href="mailto:${mailto}" style="text-decoration:none;">${project.name}</a>`
|
||||||
|
: project.name
|
||||||
|
|
||||||
|
const stripedStyle = projectIndex % 2 === 1
|
||||||
|
? 'background-color: var(--bs-table-striped-bg);'
|
||||||
|
: ''
|
||||||
|
|
||||||
const usersHtml = users.length
|
const usersHtml = users.length
|
||||||
? `<ul class="list-unstyled mb-0">${users.map(u =>
|
? users.map(user => `
|
||||||
`<li><strong>${u.name}</strong>${u.email ? ` (${u.email})` : ''}</li>`
|
<div>
|
||||||
).join('')}</ul>`
|
${
|
||||||
|
user.email
|
||||||
|
? `<a href="mailto:${user.email}" style="text-decoration:none;">${user.name}</a>`
|
||||||
|
: user.name
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
: '<em>None detected</em>'
|
: '<em>None detected</em>'
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<div style="
|
||||||
<td><a href="/project/${project.id}" target="_blank">${project.name}</a></td>
|
display:grid;
|
||||||
<td>${owner.name || 'Unknown'}</td>
|
grid-template-columns:70% 30%;
|
||||||
<td>${
|
column-gap:1rem;
|
||||||
owner.email
|
${stripedStyle}
|
||||||
? `<a href="mailto:${owner.email}">${owner.email}</a>`
|
padding:0.5rem 0;
|
||||||
: 'N/A'
|
">
|
||||||
}</td>
|
<div style="background:inherit;">
|
||||||
<td>${usersHtml}</td>
|
${projectName}
|
||||||
<td>${project.connectionCount}</td>
|
</div>
|
||||||
</tr>
|
<div style="background:inherit;">
|
||||||
|
${usersHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
@@ -85,17 +106,18 @@ function renderTable(data) {
|
|||||||
<strong>${data.length}</strong> project(s) currently being edited
|
<strong>${data.length}</strong> project(s) currently being edited
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="table table-striped table-hover">
|
<div style="
|
||||||
<thead>
|
display:grid;
|
||||||
<tr>
|
grid-template-columns:70% 30%;
|
||||||
<th>Project Name</th>
|
column-gap:1rem;
|
||||||
<th>Owner</th>
|
font-weight:bold;
|
||||||
<th>Owner Email</th>
|
padding-bottom:0.5rem;
|
||||||
<th>Active Users</th>
|
border-bottom:1px solid var(--bs-border-color);
|
||||||
<th>Connections</th>
|
">
|
||||||
</tr>
|
<div>Project Name</div>
|
||||||
</thead>
|
<div>Active User</div>
|
||||||
<tbody>${rows}</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
|
${rows}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user