mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: show popular searches
This commit is contained in:
@@ -56,8 +56,8 @@
|
|||||||
"active-users.total": "Total",
|
"active-users.total": "Total",
|
||||||
"active-users.connections": "Connections",
|
"active-users.connections": "Connections",
|
||||||
|
|
||||||
"anonymous-registered-users": "Anonymous vs Registered Users",
|
"guest-registered-users": "Guest vs Registered Users",
|
||||||
"anonymous": "Anonymous",
|
"guest": "Guest",
|
||||||
"registered": "Registered",
|
"registered": "Registered",
|
||||||
|
|
||||||
"user-presence": "User Presence",
|
"user-presence": "User Presence",
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
"unread": "Unread",
|
"unread": "Unread",
|
||||||
|
|
||||||
"high-presence-topics": "High Presence Topics",
|
"high-presence-topics": "High Presence Topics",
|
||||||
|
"popular-searches": "Popular Searches",
|
||||||
|
|
||||||
"graphs.page-views": "Page Views",
|
"graphs.page-views": "Page Views",
|
||||||
"graphs.page-views-registered": "Page Views Registered",
|
"graphs.page-views-registered": "Page Views Registered",
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
"graphs.page-views-bot": "Page Views Bot",
|
"graphs.page-views-bot": "Page Views Bot",
|
||||||
"graphs.unique-visitors": "Unique Visitors",
|
"graphs.unique-visitors": "Unique Visitors",
|
||||||
"graphs.registered-users": "Registered Users",
|
"graphs.registered-users": "Registered Users",
|
||||||
"graphs.anonymous-users": "Anonymous Users",
|
"graphs.guest-users": "Guest Users",
|
||||||
"last-restarted-by": "Last restarted by",
|
"last-restarted-by": "Last restarted by",
|
||||||
"no-users-browsing": "No users browsing",
|
"no-users-browsing": "No users browsing",
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"dashboard/logins": "Logins",
|
"dashboard/logins": "Logins",
|
||||||
"dashboard/users": "Users",
|
"dashboard/users": "Users",
|
||||||
"dashboard/topics": "Topics",
|
"dashboard/topics": "Topics",
|
||||||
|
"dashboard/searches": "Searches",
|
||||||
"section-general": "General",
|
"section-general": "General",
|
||||||
|
|
||||||
"section-manage": "Manage",
|
"section-manage": "Manage",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
border-color: rgba(151,187,205,1);
|
border-color: rgba(151,187,205,1);
|
||||||
background-color: rgba(151,187,205,0.2);
|
background-color: rgba(151,187,205,0.2);
|
||||||
}
|
}
|
||||||
&.anonymous {
|
&.guest {
|
||||||
border-color: #46BFBD;
|
border-color: #46BFBD;
|
||||||
background-color: #5AD3D1;
|
background-color: #5AD3D1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ paths:
|
|||||||
$ref: 'read/admin/dashboard/users.yaml'
|
$ref: 'read/admin/dashboard/users.yaml'
|
||||||
/api/admin/dashboard/topics:
|
/api/admin/dashboard/topics:
|
||||||
$ref: 'read/admin/dashboard/topics.yaml'
|
$ref: 'read/admin/dashboard/topics.yaml'
|
||||||
|
/api/admin/dashboard/searches:
|
||||||
|
$ref: 'read/admin/dashboard/searches.yaml'
|
||||||
"/api/admin/settings/{term}":
|
"/api/admin/settings/{term}":
|
||||||
$ref: 'read/admin/settings/term.yaml'
|
$ref: 'read/admin/settings/term.yaml'
|
||||||
/api/admin/settings/languages:
|
/api/admin/settings/languages:
|
||||||
|
|||||||
25
public/openapi/read/admin/dashboard/searches.yaml
Normal file
25
public/openapi/read/admin/dashboard/searches.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
|
summary: Get detailed user registration analytics
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: A JSON object containing popular searches.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
searches:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
description: The string that was searched
|
||||||
|
score:
|
||||||
|
type: number
|
||||||
|
description: Number of times this string has been searched
|
||||||
|
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||||
@@ -150,7 +150,7 @@ define('admin/dashboard', ['Chart', 'translator', 'benchpress', 'bootbox'], func
|
|||||||
t.translateKey('admin/dashboard:graphs.page-views-bot', []),
|
t.translateKey('admin/dashboard:graphs.page-views-bot', []),
|
||||||
t.translateKey('admin/dashboard:graphs.unique-visitors', []),
|
t.translateKey('admin/dashboard:graphs.unique-visitors', []),
|
||||||
t.translateKey('admin/dashboard:graphs.registered-users', []),
|
t.translateKey('admin/dashboard:graphs.registered-users', []),
|
||||||
t.translateKey('admin/dashboard:graphs.anonymous-users', []),
|
t.translateKey('admin/dashboard:graphs.guest-users', []),
|
||||||
t.translateKey('admin/dashboard:on-categories', []),
|
t.translateKey('admin/dashboard:on-categories', []),
|
||||||
t.translateKey('admin/dashboard:reading-posts', []),
|
t.translateKey('admin/dashboard:reading-posts', []),
|
||||||
t.translateKey('admin/dashboard:browsing-topics', []),
|
t.translateKey('admin/dashboard:browsing-topics', []),
|
||||||
@@ -469,11 +469,11 @@ define('admin/dashboard', ['Chart', 'translator', 'benchpress', 'bootbox'], func
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRegisteredGraph(registered, anonymous) {
|
function updateRegisteredGraph(registered, guest) {
|
||||||
$('#analytics-legend .registered').parent().find('.count').text(registered);
|
$('#analytics-legend .registered').parent().find('.count').text(registered);
|
||||||
$('#analytics-legend .anonymous').parent().find('.count').text(anonymous);
|
$('#analytics-legend .guest').parent().find('.count').text(guest);
|
||||||
graphs.registered.data.datasets[0].data[0] = registered;
|
graphs.registered.data.datasets[0].data[0] = registered;
|
||||||
graphs.registered.data.datasets[0].data[1] = anonymous;
|
graphs.registered.data.datasets[0].data[1] = guest;
|
||||||
graphs.registered.update();
|
graphs.registered.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const nconf = require('nconf');
|
|||||||
const semver = require('semver');
|
const semver = require('semver');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const validator = require('validator');
|
||||||
|
|
||||||
const versions = require('../../admin/versions');
|
const versions = require('../../admin/versions');
|
||||||
const db = require('../../database');
|
const db = require('../../database');
|
||||||
@@ -18,12 +19,13 @@ const emailer = require('../../emailer');
|
|||||||
const dashboardController = module.exports;
|
const dashboardController = module.exports;
|
||||||
|
|
||||||
dashboardController.get = async function (req, res) {
|
dashboardController.get = async function (req, res) {
|
||||||
const [stats, notices, latestVersion, lastrestart, isAdmin] = await Promise.all([
|
const [stats, notices, latestVersion, lastrestart, isAdmin, popularSearches] = await Promise.all([
|
||||||
getStats(),
|
getStats(),
|
||||||
getNotices(),
|
getNotices(),
|
||||||
getLatestVersion(),
|
getLatestVersion(),
|
||||||
getLastRestart(),
|
getLastRestart(),
|
||||||
user.isAdministrator(req.uid),
|
user.isAdministrator(req.uid),
|
||||||
|
getPopularSearches(),
|
||||||
]);
|
]);
|
||||||
const version = nconf.get('version');
|
const version = nconf.get('version');
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ dashboardController.get = async function (req, res) {
|
|||||||
canRestart: !!process.send,
|
canRestart: !!process.send,
|
||||||
lastrestart: lastrestart,
|
lastrestart: lastrestart,
|
||||||
showSystemControls: isAdmin,
|
showSystemControls: isAdmin,
|
||||||
|
popularSearches: popularSearches,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,6 +241,11 @@ async function getLastRestart() {
|
|||||||
return lastrestart;
|
return lastrestart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getPopularSearches() {
|
||||||
|
const searches = await db.getSortedSetRevRangeWithScores('searches:all', 0, 9);
|
||||||
|
return searches.map(s => ({ value: validator.escape(String(s.value)), score: s.score }));
|
||||||
|
}
|
||||||
|
|
||||||
dashboardController.getLogins = async (req, res) => {
|
dashboardController.getLogins = async (req, res) => {
|
||||||
let stats = await getStats();
|
let stats = await getStats();
|
||||||
stats = stats.filter(stat => stat.name === '[[admin/dashboard:logins]]').map(({ ...stat }) => {
|
stats = stats.filter(stat => stat.name === '[[admin/dashboard:logins]]').map(({ ...stat }) => {
|
||||||
@@ -327,3 +335,10 @@ dashboardController.getTopics = async (req, res) => {
|
|||||||
topics: topicData,
|
topics: topicData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dashboardController.getSearches = async (req, res) => {
|
||||||
|
const searches = await db.getSortedSetRevRangeWithScores('searches:all', 0, 99);
|
||||||
|
res.render('admin/dashboard/searches', {
|
||||||
|
searches: searches.map(s => ({ value: validator.escape(String(s.value)), score: s.score })),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -89,8 +89,10 @@ exports.handleErrors = async function handleErrors(err, req, res, next) { // esl
|
|||||||
}
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
winston.error(`${req.originalUrl}\n${_err.stack}`);
|
winston.error(`${req.originalUrl}\n${_err.stack}`);
|
||||||
|
if (!res.headersSent) {
|
||||||
res.status(500).send(_err.message);
|
res.status(500).send(_err.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getErrorHandlers(cases) {
|
async function getErrorHandlers(cases) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module.exports = function (app, name, middleware, controllers) {
|
|||||||
helpers.setupAdminPageRoute(app, `/${name}/dashboard/logins`, middleware, middlewares, controllers.admin.dashboard.getLogins);
|
helpers.setupAdminPageRoute(app, `/${name}/dashboard/logins`, middleware, middlewares, controllers.admin.dashboard.getLogins);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/dashboard/users`, middleware, middlewares, controllers.admin.dashboard.getUsers);
|
helpers.setupAdminPageRoute(app, `/${name}/dashboard/users`, middleware, middlewares, controllers.admin.dashboard.getUsers);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/dashboard/topics`, middleware, middlewares, controllers.admin.dashboard.getTopics);
|
helpers.setupAdminPageRoute(app, `/${name}/dashboard/topics`, middleware, middlewares, controllers.admin.dashboard.getTopics);
|
||||||
|
helpers.setupAdminPageRoute(app, `/${name}/dashboard/searches`, middleware, middlewares, controllers.admin.dashboard.getSearches);
|
||||||
|
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/manage/categories`, middleware, middlewares, controllers.admin.categories.getAll);
|
helpers.setupAdminPageRoute(app, `/${name}/manage/categories`, middleware, middlewares, controllers.admin.categories.getAll);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/manage/categories/:category_id`, middleware, middlewares, controllers.admin.categories.get);
|
helpers.setupAdminPageRoute(app, `/${name}/manage/categories/:category_id`, middleware, middlewares, controllers.admin.categories.get);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ async function searchInContent(data) {
|
|||||||
|
|
||||||
async function doSearch(type, searchIn) {
|
async function doSearch(type, searchIn) {
|
||||||
if (searchIn.includes(data.searchIn)) {
|
if (searchIn.includes(data.searchIn)) {
|
||||||
|
await recordSearch(data.query);
|
||||||
return await plugins.hooks.fire('filter:search.query', {
|
return await plugins.hooks.fire('filter:search.query', {
|
||||||
index: type,
|
index: type,
|
||||||
content: data.query,
|
content: data.query,
|
||||||
@@ -94,6 +95,13 @@ async function searchInContent(data) {
|
|||||||
return Object.assign(returnData, metadata);
|
return Object.assign(returnData, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function recordSearch(query) {
|
||||||
|
const cleanedQuery = String(query).trim().toLowerCase().substr(0, 255);
|
||||||
|
if (cleanedQuery.length > 2) {
|
||||||
|
await db.sortedSetIncrBy('searches:all', 1, cleanedQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function filterAndSort(pids, data) {
|
async function filterAndSort(pids, data) {
|
||||||
if (data.sortBy === 'relevance' && !data.replies && !data.timeRange && !data.hasTags && !plugins.hooks.hasListeners('filter:search.filterAndSort')) {
|
if (data.sortBy === 'relevance' && !data.replies && !data.timeRange && !data.hasTags && !plugins.hooks.hasListeners('filter:search.filterAndSort')) {
|
||||||
return pids;
|
return pids;
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
|
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-3">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/dashboard:anonymous-registered-users]]</div>
|
<div class="panel-heading">[[admin/dashboard:guest-registered-users]]</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="graph-container pie-chart legend-down">
|
<div class="graph-container pie-chart legend-down">
|
||||||
<canvas id="analytics-registered"></canvas>
|
<canvas id="analytics-registered"></canvas>
|
||||||
<ul class="graph-legend" id="analytics-legend">
|
<ul class="graph-legend" id="analytics-legend">
|
||||||
<li><div class="registered"></div><span>(<span class="count"></span>) [[admin/dashboard:registered]]</span></li>
|
<li><div class="registered"></div><span>(<span class="count"></span>) [[admin/dashboard:registered]]</span></li>
|
||||||
<li><div class="anonymous"></div><span>(<span class="count"></span>) [[admin/dashboard:anonymous]]</span></li>
|
<li><div class="guest"></div><span>(<span class="count"></span>) [[admin/dashboard:guest]]</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-3">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/dashboard:user-presence]]</div>
|
<div class="panel-heading">[[admin/dashboard:user-presence]]</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-3">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/dashboard:high-presence-topics]]</div>
|
<div class="panel-heading">[[admin/dashboard:high-presence-topics]]</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -47,6 +47,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">[[admin/dashboard:popular-searches]]</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="graph-container pie-chart legend-down">
|
||||||
|
<ul class="graph-legend" id="popular-searches-legend">
|
||||||
|
{{{ each popularSearches}}}
|
||||||
|
<li>({popularSearches.score}) {popularSearches.value}</li>
|
||||||
|
{{{ end }}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
25
src/views/admin/dashboard/searches.tpl
Normal file
25
src/views/admin/dashboard/searches.tpl
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="row dashboard">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<a class="btn btn-link" href="{config.relative_path}/admin/dashboard">
|
||||||
|
<i class="fa fa-chevron-left"></i>
|
||||||
|
[[admin/dashboard:back-to-dashboard]]
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table table-striped search-list">
|
||||||
|
<tbody>
|
||||||
|
{{{ if !searches.length}}}
|
||||||
|
<tr>
|
||||||
|
<td colspan=4" class="text-center"><em>[[admin/dashboard:details.no-searches]]</em></td>
|
||||||
|
</tr>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ each searches }}}
|
||||||
|
<tr>
|
||||||
|
<td>{searches.value}</a></td>
|
||||||
|
<td class="text-right">{searches.score}</td>
|
||||||
|
</tr>
|
||||||
|
{{{ end }}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -184,6 +184,7 @@
|
|||||||
<li><a href="{relative_path}/admin/dashboard/logins">[[admin/menu:dashboard/logins]]</a></li>
|
<li><a href="{relative_path}/admin/dashboard/logins">[[admin/menu:dashboard/logins]]</a></li>
|
||||||
<li><a href="{relative_path}/admin/dashboard/users">[[admin/menu:dashboard/users]]</a></li>
|
<li><a href="{relative_path}/admin/dashboard/users">[[admin/menu:dashboard/users]]</a></li>
|
||||||
<li><a href="{relative_path}/admin/dashboard/topics">[[admin/menu:dashboard/topics]]</a></li>
|
<li><a href="{relative_path}/admin/dashboard/topics">[[admin/menu:dashboard/topics]]</a></li>
|
||||||
|
<li><a href="{relative_path}/admin/dashboard/searches">[[admin/menu:dashboard/searches]]</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
|
|||||||
Reference in New Issue
Block a user