mirror of
https://github.com/discourse/discourse.git
synced 2025-01-20 19:08:47 +08:00
DEV: removes old dashboard (#7295)
This commit is contained in:
parent
d81f3ee2c2
commit
e986e96227
|
@ -1,6 +1,6 @@
|
||||||
import { setting } from "discourse/lib/computed";
|
import { setting } from "discourse/lib/computed";
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
import AdminDashboard from "admin/models/admin-dashboard";
|
||||||
import Report from "admin/models/report";
|
import Report from "admin/models/report";
|
||||||
import PeriodComputationMixin from "admin/mixins/period-computation";
|
import PeriodComputationMixin from "admin/mixins/period-computation";
|
||||||
|
|
||||||
|
@ -88,12 +88,12 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
|
||||||
) {
|
) {
|
||||||
this.set("isLoading", true);
|
this.set("isLoading", true);
|
||||||
|
|
||||||
AdminDashboardNext.fetchGeneral()
|
AdminDashboard.fetchGeneral()
|
||||||
.then(adminDashboardNextModel => {
|
.then(adminDashboardModel => {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
dashboardFetchedAt: new Date(),
|
dashboardFetchedAt: new Date(),
|
||||||
model: adminDashboardNextModel,
|
model: adminDashboardModel,
|
||||||
reports: Ember.makeArray(adminDashboardNextModel.reports).map(x =>
|
reports: Ember.makeArray(adminDashboardModel.reports).map(x =>
|
||||||
Report.create(x)
|
Report.create(x)
|
||||||
)
|
)
|
||||||
});
|
});
|
|
@ -1,90 +0,0 @@
|
||||||
import { setting } from "discourse/lib/computed";
|
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
|
||||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
|
||||||
import VersionCheck from "admin/models/version-check";
|
|
||||||
|
|
||||||
const PROBLEMS_CHECK_MINUTES = 1;
|
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
|
||||||
isLoading: false,
|
|
||||||
dashboardFetchedAt: null,
|
|
||||||
exceptionController: Ember.inject.controller("exception"),
|
|
||||||
showVersionChecks: setting("version_checks"),
|
|
||||||
|
|
||||||
@computed("problems.length")
|
|
||||||
foundProblems(problemsLength) {
|
|
||||||
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchProblems() {
|
|
||||||
if (this.get("isLoadingProblems")) return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.get("problemsFetchedAt") ||
|
|
||||||
moment()
|
|
||||||
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
|
||||||
.toDate() > this.get("problemsFetchedAt")
|
|
||||||
) {
|
|
||||||
this._loadProblems();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchDashboard() {
|
|
||||||
const versionChecks = this.siteSettings.version_checks;
|
|
||||||
|
|
||||||
if (this.get("isLoading") || !versionChecks) return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.get("dashboardFetchedAt") ||
|
|
||||||
moment()
|
|
||||||
.subtract(30, "minutes")
|
|
||||||
.toDate() > this.get("dashboardFetchedAt")
|
|
||||||
) {
|
|
||||||
this.set("isLoading", true);
|
|
||||||
|
|
||||||
AdminDashboardNext.fetch()
|
|
||||||
.then(model => {
|
|
||||||
let properties = {
|
|
||||||
dashboardFetchedAt: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (versionChecks) {
|
|
||||||
properties.versionCheck = VersionCheck.create(model.version_check);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProperties(properties);
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
|
||||||
this.replaceRoute("exception");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.set("isLoading", false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_loadProblems() {
|
|
||||||
this.setProperties({
|
|
||||||
loadingProblems: true,
|
|
||||||
problemsFetchedAt: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
AdminDashboardNext.fetchProblems()
|
|
||||||
.then(model => this.set("problems", model.problems))
|
|
||||||
.finally(() => this.set("loadingProblems", false));
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("problemsFetchedAt")
|
|
||||||
problemsTimestamp(problemsFetchedAt) {
|
|
||||||
return moment(problemsFetchedAt)
|
|
||||||
.locale("en")
|
|
||||||
.format("LLL");
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
refreshProblems() {
|
|
||||||
this._loadProblems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,78 +1,90 @@
|
||||||
import AdminDashboard from "admin/models/admin-dashboard";
|
import { setting } from "discourse/lib/computed";
|
||||||
import Report from "admin/models/report";
|
|
||||||
import AdminUser from "admin/models/admin-user";
|
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import AdminDashboard from "admin/models/admin-dashboard";
|
||||||
|
import VersionCheck from "admin/models/version-check";
|
||||||
|
|
||||||
const ATTRIBUTES = [
|
const PROBLEMS_CHECK_MINUTES = 1;
|
||||||
"admins",
|
|
||||||
"moderators",
|
|
||||||
"silenced",
|
|
||||||
"suspended",
|
|
||||||
"top_traffic_sources",
|
|
||||||
"top_referred_topics",
|
|
||||||
"updated_at"
|
|
||||||
];
|
|
||||||
|
|
||||||
const REPORTS = [
|
|
||||||
"global_reports",
|
|
||||||
"page_view_reports",
|
|
||||||
"private_message_reports",
|
|
||||||
"http_reports",
|
|
||||||
"user_reports",
|
|
||||||
"mobile_reports"
|
|
||||||
];
|
|
||||||
|
|
||||||
// This controller supports the default interface when you enter the admin section.
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
loading: null,
|
isLoading: false,
|
||||||
versionCheck: null,
|
|
||||||
dashboardFetchedAt: null,
|
dashboardFetchedAt: null,
|
||||||
exceptionController: Ember.inject.controller("exception"),
|
exceptionController: Ember.inject.controller("exception"),
|
||||||
|
showVersionChecks: setting("version_checks"),
|
||||||
|
|
||||||
|
@computed("problems.length")
|
||||||
|
foundProblems(problemsLength) {
|
||||||
|
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchProblems() {
|
||||||
|
if (this.get("isLoadingProblems")) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.get("problemsFetchedAt") ||
|
||||||
|
moment()
|
||||||
|
.subtract(PROBLEMS_CHECK_MINUTES, "minutes")
|
||||||
|
.toDate() > this.get("problemsFetchedAt")
|
||||||
|
) {
|
||||||
|
this._loadProblems();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fetchDashboard() {
|
fetchDashboard() {
|
||||||
|
const versionChecks = this.siteSettings.version_checks;
|
||||||
|
|
||||||
|
if (this.get("isLoading") || !versionChecks) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.get("dashboardFetchedAt") ||
|
!this.get("dashboardFetchedAt") ||
|
||||||
moment()
|
moment()
|
||||||
.subtract(30, "minutes")
|
.subtract(30, "minutes")
|
||||||
.toDate() > this.get("dashboardFetchedAt")
|
.toDate() > this.get("dashboardFetchedAt")
|
||||||
) {
|
) {
|
||||||
this.set("loading", true);
|
this.set("isLoading", true);
|
||||||
AdminDashboard.find()
|
|
||||||
.then(d => {
|
|
||||||
this.set("dashboardFetchedAt", new Date());
|
|
||||||
|
|
||||||
REPORTS.forEach(name =>
|
AdminDashboard.fetch()
|
||||||
this.set(name, d[name].map(r => Report.create(r)))
|
.then(model => {
|
||||||
);
|
let properties = {
|
||||||
|
dashboardFetchedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
const topReferrers = d.top_referrers;
|
if (versionChecks) {
|
||||||
if (topReferrers && topReferrers.data) {
|
properties.versionCheck = VersionCheck.create(model.version_check);
|
||||||
d.top_referrers.data = topReferrers.data.map(user =>
|
|
||||||
AdminUser.create(user)
|
|
||||||
);
|
|
||||||
this.set("top_referrers", topReferrers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ATTRIBUTES.forEach(a => this.set(a, d[a]));
|
this.setProperties(properties);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||||
this.replaceRoute("exception");
|
this.replaceRoute("exception");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.set("loading", false);
|
this.set("isLoading", false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("updated_at")
|
_loadProblems() {
|
||||||
updatedTimestamp(updatedAt) {
|
this.setProperties({
|
||||||
return moment(updatedAt).format("LLL");
|
loadingProblems: true,
|
||||||
|
problemsFetchedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
AdminDashboard.fetchProblems()
|
||||||
|
.then(model => this.set("problems", model.problems))
|
||||||
|
.finally(() => this.set("loadingProblems", false));
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("problemsFetchedAt")
|
||||||
|
problemsTimestamp(problemsFetchedAt) {
|
||||||
|
return moment(problemsFetchedAt)
|
||||||
|
.locale("en")
|
||||||
|
.format("LLL");
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
showTrafficReport() {
|
refreshProblems() {
|
||||||
this.set("showTrafficReport", true);
|
this._loadProblems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
@computed("application.currentPath")
|
@computed("application.currentPath")
|
||||||
adminContentsClassName(currentPath) {
|
adminContentsClassName(currentPath) {
|
||||||
return currentPath
|
let cssClasses = currentPath
|
||||||
.split(".")
|
.split(".")
|
||||||
.filter(segment => {
|
.filter(segment => {
|
||||||
return (
|
return (
|
||||||
|
@ -27,5 +27,12 @@ export default Ember.Controller.extend({
|
||||||
})
|
})
|
||||||
.map(Ember.String.dasherize)
|
.map(Ember.String.dasherize)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
// this is done to avoid breaking css customizations
|
||||||
|
if (cssClasses.includes("dashboard")) {
|
||||||
|
cssClasses = `${cssClasses} dashboard-next`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cssClasses;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
|
|
||||||
const GENERAL_ATTRIBUTES = ["updated_at"];
|
|
||||||
|
|
||||||
const AdminDashboardNext = Discourse.Model.extend({});
|
|
||||||
|
|
||||||
AdminDashboardNext.reopenClass({
|
|
||||||
fetch() {
|
|
||||||
return ajax("/admin/dashboard.json").then(json => {
|
|
||||||
const model = AdminDashboardNext.create();
|
|
||||||
model.set("version_check", json.version_check);
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchGeneral() {
|
|
||||||
return ajax("/admin/dashboard/general.json").then(json => {
|
|
||||||
const model = AdminDashboardNext.create();
|
|
||||||
|
|
||||||
const attributes = {};
|
|
||||||
GENERAL_ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
|
||||||
|
|
||||||
model.setProperties({
|
|
||||||
reports: json.reports,
|
|
||||||
attributes,
|
|
||||||
loaded: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Only fetch the list of problems that should be rendered on the dashboard.
|
|
||||||
The model will only have its "problems" attribute set.
|
|
||||||
|
|
||||||
@method fetchProblems
|
|
||||||
@return {jqXHR} a jQuery Promise object
|
|
||||||
**/
|
|
||||||
fetchProblems: function() {
|
|
||||||
return ajax("/admin/dashboard/problems.json", {
|
|
||||||
type: "GET",
|
|
||||||
dataType: "json"
|
|
||||||
}).then(function(json) {
|
|
||||||
var model = AdminDashboardNext.create(json);
|
|
||||||
model.set("loaded", true);
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default AdminDashboardNext;
|
|
|
@ -1,18 +1,38 @@
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const GENERAL_ATTRIBUTES = ["updated_at"];
|
||||||
|
|
||||||
const AdminDashboard = Discourse.Model.extend({});
|
const AdminDashboard = Discourse.Model.extend({});
|
||||||
|
|
||||||
AdminDashboard.reopenClass({
|
AdminDashboard.reopenClass({
|
||||||
/**
|
fetch() {
|
||||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
return ajax("/admin/dashboard.json").then(json => {
|
||||||
has expired and the server must collect the data again.
|
const model = AdminDashboard.create();
|
||||||
|
model.set("version_check", json.version_check);
|
||||||
|
return model;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
@method find
|
fetchGeneral() {
|
||||||
@return {jqXHR} a jQuery Promise object
|
return ajax("/admin/dashboard/general.json").then(json => {
|
||||||
**/
|
const model = AdminDashboard.create();
|
||||||
find: function() {
|
|
||||||
return ajax("/admin/dashboard-old.json").then(function(json) {
|
const attributes = {};
|
||||||
var model = AdminDashboard.create(json);
|
GENERAL_ATTRIBUTES.forEach(a => (attributes[a] = json[a]));
|
||||||
|
|
||||||
|
model.setProperties({
|
||||||
|
reports: json.reports,
|
||||||
|
attributes,
|
||||||
|
loaded: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return model;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchProblems() {
|
||||||
|
return ajax("/admin/dashboard/problems.json").then(json => {
|
||||||
|
const model = AdminDashboard.create(json);
|
||||||
model.set("loaded", true);
|
model.set("loaded", true);
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
activate() {
|
||||||
|
this.controllerFor("admin-dashboard-general").fetchDashboard();
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +0,0 @@
|
||||||
export default Discourse.Route.extend({
|
|
||||||
activate() {
|
|
||||||
this.controllerFor("admin-dashboard-next-general").fetchDashboard();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
|
||||||
|
|
||||||
export default Discourse.Route.extend({
|
|
||||||
activate() {
|
|
||||||
this.controllerFor("admin-dashboard-next").fetchProblems();
|
|
||||||
this.controllerFor("admin-dashboard-next").fetchDashboard();
|
|
||||||
scrollTop();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
import { scrollTop } from "discourse/mixins/scroll-top";
|
||||||
|
|
||||||
export default Discourse.Route.extend({
|
export default Discourse.Route.extend({
|
||||||
setupController(controller) {
|
activate() {
|
||||||
controller.fetchDashboard();
|
this.controllerFor("admin-dashboard").fetchProblems();
|
||||||
|
this.controllerFor("admin-dashboard").fetchDashboard();
|
||||||
|
scrollTop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default Discourse.Route.extend({
|
export default Discourse.Route.extend({
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
this.transitionTo("admin.dashboardNextReports");
|
this.transitionTo("admin.dashboardReports");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
export default function() {
|
export default function() {
|
||||||
this.route("admin", { resetNamespace: true }, function() {
|
this.route("admin", { resetNamespace: true }, function() {
|
||||||
this.route("dashboard", { path: "/dashboard-old" });
|
this.route("dashboard", { path: "/" }, function() {
|
||||||
|
|
||||||
this.route("dashboardNext", { path: "/" }, function() {
|
|
||||||
this.route("general", { path: "/" });
|
this.route("general", { path: "/" });
|
||||||
this.route("admin.dashboardNextModeration", {
|
this.route("admin.dashboardModeration", {
|
||||||
path: "/dashboard/moderation",
|
path: "/dashboard/moderation",
|
||||||
resetNamespace: true
|
resetNamespace: true
|
||||||
});
|
});
|
||||||
this.route("admin.dashboardNextSecurity", {
|
this.route("admin.dashboardSecurity", {
|
||||||
path: "/dashboard/security",
|
path: "/dashboard/security",
|
||||||
resetNamespace: true
|
resetNamespace: true
|
||||||
});
|
});
|
||||||
this.route("admin.dashboardNextReports", {
|
this.route("admin.dashboardReports", {
|
||||||
path: "/dashboard/reports",
|
path: "/dashboard/reports",
|
||||||
resetNamespace: true
|
resetNamespace: true
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{{#admin-wrapper class="container"}}
|
{{#admin-wrapper class="container"}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<div class="admin-main-nav">
|
<div class="admin-main-nav">
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
|
|
||||||
{{nav-item route='admin.dashboardNext' label='admin.dashboard.title'}}
|
{{nav-item route='admin.dashboard' label='admin.dashboard.title'}}
|
||||||
{{#if currentUser.admin}}
|
{{#if currentUser.admin}}
|
||||||
{{nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
|
{{nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
{{#if showAllReportsLink}}
|
{{#if showAllReportsLink}}
|
||||||
<li class="item all-reports">
|
<li class="item all-reports">
|
||||||
{{#link-to "admin.dashboardNextReports" class="report-url"}}
|
{{#link-to "admin.dashboardReports" class="report-url"}}
|
||||||
{{i18n "admin.dashboard.all_reports"}}
|
{{i18n "admin.dashboard.all_reports"}}
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,227 +1,38 @@
|
||||||
{{#conditional-loading-spinner condition=loading}}
|
{{plugin-outlet name="admin-dashboard-top"}}
|
||||||
<div class="alert alert-info">
|
|
||||||
The old dashboard is going to be removed in Discourse 2.2
|
|
||||||
</div>
|
|
||||||
<div class="dashboard-left">
|
|
||||||
<div class="dashboard-stats trust-levels">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th> </th>
|
|
||||||
<th>0</th>
|
|
||||||
<th>1</th>
|
|
||||||
<th>2</th>
|
|
||||||
<th>3</th>
|
|
||||||
<th>4</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each user_reports as |r|}}
|
|
||||||
{{admin-report-trust-level-counts report=r}}
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats totals">
|
{{#if showVersionChecks}}
|
||||||
<table>
|
<div class="section-top">
|
||||||
<tr>
|
<div class="version-checks">
|
||||||
<td class="title">{{d-icon "shield-alt"}} {{i18n 'admin.dashboard.admins'}}</td>
|
{{partial "admin/templates/version-checks"}}
|
||||||
<td class="value">{{#link-to 'adminUsersList.show' 'admins'}}{{admins}}{{/link-to}}</td>
|
|
||||||
<td class="title">{{d-icon "ban"}} {{i18n 'admin.dashboard.suspended'}}</td>
|
|
||||||
<td class="value">{{#link-to 'adminUsersList.show' 'suspended'}}{{suspended}}{{/link-to}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="title">{{d-icon "shield-alt"}} {{i18n 'admin.dashboard.moderators'}}</td>
|
|
||||||
<td class="value">{{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}}</td>
|
|
||||||
<td class="title">{{d-icon "ban"}} {{i18n 'admin.dashboard.silenced'}}</td>
|
|
||||||
<td class="value">{{#link-to 'adminUsersList.show' 'silenced'}}{{silenced}}{{/link-to}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th> </th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each global_reports as |r|}}
|
|
||||||
{{admin-report-counts report=r}}
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title" title="{{i18n 'admin.dashboard.page_views'}}">{{i18n 'admin.dashboard.page_views_short'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each page_view_reports as |r|}}
|
|
||||||
{{admin-report-counts report=r}}
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title" title="{{i18n 'admin.dashboard.private_messages_title'}}">{{d-icon "envelope"}} {{i18n 'admin.dashboard.private_messages_short'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each private_message_reports as |r|}}
|
|
||||||
{{admin-report-counts report=r}}
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title" title="{{i18n 'admin.dashboard.mobile_title'}}">{{i18n 'admin.dashboard.mobile_title'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each mobile_reports as |r|}}
|
|
||||||
{{admin-report-counts report=r}}
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if showTrafficReport}}
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title" title="{{i18n 'admin.dashboard.traffic'}}">{{i18n 'admin.dashboard.traffic_short'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
|
|
||||||
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#unless loading}}
|
|
||||||
{{#each http_reports as |r|}}
|
|
||||||
{{admin-report-counts report=r}}
|
|
||||||
{{/each}}
|
|
||||||
{{/unless}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<a href {{action 'showTrafficReport'}}>{{i18n 'admin.dashboard.show_traffic_report'}}</a>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-right">
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title">{{top_referred_topics.title}} ({{i18n 'admin.dashboard.reports.last_30_days'}})</th>
|
|
||||||
<th>{{top_referred_topics.ytitles.num_clicks}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{{#each top_referred_topics.data as |data|}}
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="title">
|
|
||||||
<div class="referred-topic-title">
|
|
||||||
<div class="overflow-ellipsis">
|
|
||||||
<a href="{{unbound data.topic_url}}">{{data.topic_title}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="value">{{number data.num_clicks}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title">{{top_traffic_sources.title}} ({{i18n 'admin.dashboard.reports.last_30_days'}})</th>
|
|
||||||
<th>{{top_traffic_sources.ytitles.num_clicks}}</th>
|
|
||||||
<th>{{top_traffic_sources.ytitles.num_topics}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{{#each top_traffic_sources.data as |s|}}
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="title">{{s.domain}}</td>
|
|
||||||
<td class="value">{{number s.num_clicks}}</td>
|
|
||||||
<td class="value">{{number s.num_topics}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="title">{{top_referrers.title}} ({{i18n 'admin.dashboard.reports.last_30_days'}})</th>
|
|
||||||
<th>{{top_referrers.ytitles.num_clicks}}</th>
|
|
||||||
<th>{{top_referrers.ytitles.num_topics}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{{#each top_referrers.data as |r|}}
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="title">{{#link-to 'adminUser' r.user_id r.username}}{{unbound r.username}}{{/link-to}}</td>
|
|
||||||
<td class="value">{{number r.num_clicks}}</td>
|
|
||||||
<td class="value">{{number r.num_topics}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='clearfix'></div>
|
{{/if}}
|
||||||
|
|
||||||
<div class="dashboard-stats pull-right">
|
{{partial "admin/templates/dashboard-problems"}}
|
||||||
<div class="pull-right">{{i18n 'admin.dashboard.last_updated'}} {{updatedTimestamp}}</div>
|
|
||||||
<div class='clearfix'></div>
|
<ul class="navigation">
|
||||||
</div>
|
<li class="navigation-item general">
|
||||||
<div class='clearfix'></div>
|
{{#link-to "admin.dashboard.general" class="navigation-link"}}
|
||||||
{{/conditional-loading-spinner}}
|
{{i18n "admin.dashboard.general_tab"}}
|
||||||
|
{{/link-to}}
|
||||||
|
</li>
|
||||||
|
<li class="navigation-item moderation">
|
||||||
|
{{#link-to "admin.dashboardModeration" class="navigation-link"}}
|
||||||
|
{{i18n "admin.dashboard.moderation_tab"}}
|
||||||
|
{{/link-to}}
|
||||||
|
</li>
|
||||||
|
<li class="navigation-item security">
|
||||||
|
{{#link-to "admin.dashboardSecurity" class="navigation-link"}}
|
||||||
|
{{i18n "admin.dashboard.security_tab"}}
|
||||||
|
{{/link-to}}
|
||||||
|
</li>
|
||||||
|
<li class="navigation-item reports">
|
||||||
|
{{#link-to "admin.dashboardReports" class="navigation-link"}}
|
||||||
|
{{i18n "admin.dashboard.reports_tab"}}
|
||||||
|
{{/link-to}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{outlet}}
|
||||||
|
|
||||||
|
{{plugin-outlet name="admin-dashboard-bottom"}}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
{{plugin-outlet name="admin-dashboard-top"}}
|
|
||||||
|
|
||||||
{{#if showVersionChecks}}
|
|
||||||
<div class="section-top">
|
|
||||||
<div class="version-checks">
|
|
||||||
{{partial "admin/templates/version-checks"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{partial "admin/templates/dashboard-problems"}}
|
|
||||||
|
|
||||||
<ul class="navigation">
|
|
||||||
<li class="navigation-item general">
|
|
||||||
{{#link-to "admin.dashboardNext.general" class="navigation-link"}}
|
|
||||||
{{i18n "admin.dashboard.general_tab"}}
|
|
||||||
{{/link-to}}
|
|
||||||
</li>
|
|
||||||
<li class="navigation-item moderation">
|
|
||||||
{{#link-to "admin.dashboardNextModeration" class="navigation-link"}}
|
|
||||||
{{i18n "admin.dashboard.moderation_tab"}}
|
|
||||||
{{/link-to}}
|
|
||||||
</li>
|
|
||||||
<li class="navigation-item security">
|
|
||||||
{{#link-to "admin.dashboardNextSecurity" class="navigation-link"}}
|
|
||||||
{{i18n "admin.dashboard.security_tab"}}
|
|
||||||
{{/link-to}}
|
|
||||||
</li>
|
|
||||||
<li class="navigation-item reports">
|
|
||||||
{{#link-to "admin.dashboardNextReports" class="navigation-link"}}
|
|
||||||
{{i18n "admin.dashboard.reports_tab"}}
|
|
||||||
{{/link-to}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{{outlet}}
|
|
||||||
|
|
||||||
{{plugin-outlet name="admin-dashboard-bottom"}}
|
|
|
@ -961,7 +961,7 @@ table#user-badges {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles for subtabs in admin
|
// Styles for subtabs in admin
|
||||||
@import "common/admin/dashboard_next";
|
@import "common/admin/dashboard";
|
||||||
@import "common/admin/settings";
|
@import "common/admin/settings";
|
||||||
@import "common/admin/users";
|
@import "common/admin/users";
|
||||||
@import "common/admin/suspend";
|
@import "common/admin/suspend";
|
||||||
|
@ -980,4 +980,3 @@ table#user-badges {
|
||||||
@import "common/admin/admin_report_stacked_chart";
|
@import "common/admin/admin_report_stacked_chart";
|
||||||
@import "common/admin/admin_report_table";
|
@import "common/admin/admin_report_table";
|
||||||
@import "common/admin/admin_report_inline_table";
|
@import "common/admin/admin_report_inline_table";
|
||||||
@import "common/admin/dashboard_previous";
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
.admin-reports,
|
.admin-reports,
|
||||||
|
.dashboard,
|
||||||
.dashboard-next {
|
.dashboard-next {
|
||||||
&.admin-contents {
|
&.admin-contents {
|
||||||
margin: 10px 0 0 0;
|
margin: 10px 0 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard,
|
||||||
.dashboard-next {
|
.dashboard-next {
|
||||||
.section-top {
|
.section-top {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
@ -35,15 +37,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dashboard-next-moderation .navigation-item.moderation {
|
&.dashboard-moderation .navigation-item.moderation {
|
||||||
@include active-navigation-item;
|
@include active-navigation-item;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dashboard-next-security .navigation-item.security {
|
&.dashboard-security .navigation-item.security {
|
||||||
@include active-navigation-item;
|
@include active-navigation-item;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dashboard-next-reports .navigation-item.reports {
|
&.dashboard-reports .navigation-item.reports {
|
||||||
@include active-navigation-item;
|
@include active-navigation-item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,8 +510,8 @@
|
||||||
margin-bottom: 1.5em;
|
margin-bottom: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-next-moderation,
|
.dashboard-moderation,
|
||||||
.dashboard-next-security {
|
.dashboard-security {
|
||||||
.section-body {
|
.section-body {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
@ -538,7 +540,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-next-moderation {
|
.dashboard-moderation {
|
||||||
.admin-dashboard-moderation-top {
|
.admin-dashboard-moderation-top {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
@ -547,7 +549,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-next-reports {
|
.dashboard-reports {
|
||||||
.reports-list {
|
.reports-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -576,3 +578,96 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-checks {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.section-title {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
border-bottom: 1px solid $primary-low;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-check {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 50%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-self: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0 10px 0;
|
||||||
|
.upgrade-header {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
@media screen and (max-width: 650px) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
.version-number {
|
||||||
|
font-size: $font-up-2;
|
||||||
|
line-height: $line-height-medium;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
padding-right: 20px;
|
||||||
|
flex: 1 1 27%;
|
||||||
|
h3 {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: $font-down-2;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.version-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
flex: 1 1 24%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-right: 20px;
|
||||||
|
min-width: 250px;
|
||||||
|
@include breakpoint(medium) {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
.face {
|
||||||
|
margin: 0 0.75em 0 0;
|
||||||
|
font-size: $font-up-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.critical .version-notes .normal-note {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&.normal .version-notes .critical-note {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.fa {
|
||||||
|
font-size: $font-up-4;
|
||||||
|
}
|
||||||
|
.up-to-date {
|
||||||
|
color: $success;
|
||||||
|
}
|
||||||
|
.updates-available {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
.critical-updates-available {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-nag {
|
||||||
|
.d-icon {
|
||||||
|
font-size: $font-up-3;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,268 +0,0 @@
|
||||||
// Styles for admin/dashboard-old
|
|
||||||
|
|
||||||
.dashboard-left {
|
|
||||||
float: left;
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-right {
|
|
||||||
float: right;
|
|
||||||
width: 40%;
|
|
||||||
.dashboard-stats {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-checks {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
.section-title {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
border-bottom: 1px solid $primary-low;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-check {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 50%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-self: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 0 10px 0;
|
|
||||||
.upgrade-header {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
}
|
|
||||||
.version-number {
|
|
||||||
font-size: $font-up-2;
|
|
||||||
line-height: $line-height-medium;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
padding-right: 20px;
|
|
||||||
flex: 1 1 27%;
|
|
||||||
h3 {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
font-size: $font-down-2;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.version-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
flex: 1 1 24%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-right: 20px;
|
|
||||||
min-width: 250px;
|
|
||||||
@include breakpoint(medium) {
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
.face {
|
|
||||||
margin: 0 0.75em 0 0;
|
|
||||||
font-size: $font-up-3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.critical .version-notes .normal-note {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.normal .version-notes .critical-note {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.fa {
|
|
||||||
font-size: $font-up-4;
|
|
||||||
}
|
|
||||||
.up-to-date {
|
|
||||||
color: $success;
|
|
||||||
}
|
|
||||||
.updates-available {
|
|
||||||
color: $danger;
|
|
||||||
}
|
|
||||||
.critical-updates-available {
|
|
||||||
color: $danger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-nag {
|
|
||||||
.d-icon {
|
|
||||||
font-size: $font-up-3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-stats {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
flex: 1 1 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
&.version-check {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
&.detected-problems {
|
|
||||||
border-left: 1px solid $primary-low;
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
font-weight: normal;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
.title {
|
|
||||||
.d-icon {
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
.d-icon-heart {
|
|
||||||
color: $love;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
text-align: center;
|
|
||||||
background: $primary-low;
|
|
||||||
}
|
|
||||||
th.title {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
thead {
|
|
||||||
tr:hover > td {
|
|
||||||
background-color: $secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
td.value {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
.d-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.high-trending-up,
|
|
||||||
&.trending-up {
|
|
||||||
.up {
|
|
||||||
color: $success;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.high-trending-down,
|
|
||||||
&.trending-down {
|
|
||||||
.down {
|
|
||||||
color: $danger;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.no-change {
|
|
||||||
.down {
|
|
||||||
display: inline;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tr.reverse-colors {
|
|
||||||
td.value.high-trending-down .down,
|
|
||||||
td.value.trending-down .down {
|
|
||||||
color: $success;
|
|
||||||
}
|
|
||||||
td.value.high-trending-up .up,
|
|
||||||
td.value.trending-up .up {
|
|
||||||
color: $danger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.detected-problems {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
.look-here {
|
|
||||||
margin: 10px 20px;
|
|
||||||
.fa {
|
|
||||||
font-size: $font-up-5;
|
|
||||||
color: $danger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
border-left: none;
|
|
||||||
border-top: 1px solid $primary-low;
|
|
||||||
padding: 20px 0 0 0;
|
|
||||||
.look-here {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.problem-messages {
|
|
||||||
display: flex;
|
|
||||||
a {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
background: $primary-low;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 90px;
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.actions {
|
|
||||||
padding-left: 75px;
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.totals {
|
|
||||||
table {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-left: 5px;
|
|
||||||
.value {
|
|
||||||
text-align: left;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.trust-levels {
|
|
||||||
margin-bottom: 0;
|
|
||||||
table {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
td.value {
|
|
||||||
width: 45px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.referred-topic-title {
|
|
||||||
width: 355px;
|
|
||||||
@media all and (min-width: 1000px) and (max-width: 1139px) {
|
|
||||||
width: 305px;
|
|
||||||
}
|
|
||||||
@include breakpoint(medium) {
|
|
||||||
width: 265px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,7 +22,7 @@
|
||||||
@import "mobile/emoji";
|
@import "mobile/emoji";
|
||||||
@import "mobile/ring";
|
@import "mobile/ring";
|
||||||
@import "mobile/group";
|
@import "mobile/group";
|
||||||
@import "mobile/dashboard_next";
|
@import "mobile/dashboard";
|
||||||
@import "mobile/admin_customize";
|
@import "mobile/admin_customize";
|
||||||
@import "mobile/admin_reports";
|
@import "mobile/admin_reports";
|
||||||
@import "mobile/admin_report";
|
@import "mobile/admin_report";
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
.dashboard,
|
||||||
.dashboard-next {
|
.dashboard-next {
|
||||||
.activity-metrics .counters-list {
|
.activity-metrics .counters-list {
|
||||||
font-size: $font-down-1;
|
font-size: $font-down-1;
|
|
@ -1,8 +1,20 @@
|
||||||
class Admin::DashboardController < Admin::AdminController
|
class Admin::DashboardController < Admin::AdminController
|
||||||
def index
|
def index
|
||||||
dashboard_data = AdminDashboardData.fetch_cached_stats || Jobs::DashboardStats.new.execute({})
|
data = AdminDashboardIndexData.fetch_cached_stats
|
||||||
dashboard_data.merge!(version_check: DiscourseUpdates.check_version.as_json) if SiteSetting.version_checks?
|
|
||||||
render json: dashboard_data
|
if SiteSetting.version_checks?
|
||||||
|
data.merge!(version_check: DiscourseUpdates.check_version.as_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: data
|
||||||
|
end
|
||||||
|
|
||||||
|
def moderation; end
|
||||||
|
def security; end
|
||||||
|
def reports; end
|
||||||
|
|
||||||
|
def general
|
||||||
|
render json: AdminDashboardGeneralData.fetch_cached_stats
|
||||||
end
|
end
|
||||||
|
|
||||||
def problems
|
def problems
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
class Admin::DashboardNextController < Admin::AdminController
|
|
||||||
def index
|
|
||||||
data = AdminDashboardNextIndexData.fetch_cached_stats
|
|
||||||
|
|
||||||
if SiteSetting.version_checks?
|
|
||||||
data.merge!(version_check: DiscourseUpdates.check_version.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: data
|
|
||||||
end
|
|
||||||
|
|
||||||
def moderation; end
|
|
||||||
def security; end
|
|
||||||
def reports; end
|
|
||||||
|
|
||||||
def general
|
|
||||||
render json: AdminDashboardNextGeneralData.fetch_cached_stats
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,36 +3,29 @@ require_dependency 'mem_info'
|
||||||
class AdminDashboardData
|
class AdminDashboardData
|
||||||
include StatsCacheable
|
include StatsCacheable
|
||||||
|
|
||||||
GLOBAL_REPORTS ||= [
|
def initialize(opts = {})
|
||||||
'visits',
|
@opts = opts
|
||||||
'signups',
|
end
|
||||||
'profile_views',
|
|
||||||
'topics',
|
|
||||||
'posts',
|
|
||||||
'time_to_first_response',
|
|
||||||
'topics_with_no_response',
|
|
||||||
'likes',
|
|
||||||
'flags',
|
|
||||||
'bookmarks',
|
|
||||||
'emails',
|
|
||||||
]
|
|
||||||
|
|
||||||
PAGE_VIEW_REPORTS ||= ['page_view_total_reqs'] + ApplicationRequest.req_types.keys.select { |r| r =~ /^page_view_/ && r !~ /mobile/ }.map { |r| r + "_reqs" }
|
def self.fetch_stats
|
||||||
|
new.as_json
|
||||||
|
end
|
||||||
|
|
||||||
PRIVATE_MESSAGE_REPORTS ||= [
|
def get_json
|
||||||
'user_to_user_private_messages',
|
{}
|
||||||
'user_to_user_private_messages_with_replies',
|
end
|
||||||
'system_private_messages',
|
|
||||||
'notify_moderators_private_messages',
|
|
||||||
'notify_user_private_messages',
|
|
||||||
'moderator_warning_private_messages',
|
|
||||||
]
|
|
||||||
|
|
||||||
HTTP_REPORTS ||= ApplicationRequest.req_types.keys.select { |r| r =~ /^http_/ }.map { |r| r + "_reqs" }.sort
|
def as_json(_options = nil)
|
||||||
|
@json ||= get_json
|
||||||
|
end
|
||||||
|
|
||||||
USER_REPORTS ||= ['users_by_trust_level']
|
def self.reports(source)
|
||||||
|
source.map { |type| Report.find(type).as_json }
|
||||||
|
end
|
||||||
|
|
||||||
MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select { |r| r =~ /mobile/ }.map { |r| r + "_reqs" }
|
def self.stats_cache_key
|
||||||
|
"dashboard-data-#{Report::SCHEMA_VERSION}"
|
||||||
|
end
|
||||||
|
|
||||||
def self.add_problem_check(*syms, &blk)
|
def self.add_problem_check(*syms, &blk)
|
||||||
@problem_syms.push(*syms) if syms
|
@problem_syms.push(*syms) if syms
|
||||||
|
@ -40,10 +33,6 @@ class AdminDashboardData
|
||||||
end
|
end
|
||||||
class << self; attr_reader :problem_syms, :problem_blocks, :problem_messages; end
|
class << self; attr_reader :problem_syms, :problem_blocks, :problem_messages; end
|
||||||
|
|
||||||
def initialize(opts = {})
|
|
||||||
@opts = opts
|
|
||||||
end
|
|
||||||
|
|
||||||
def problems
|
def problems
|
||||||
problems = []
|
problems = []
|
||||||
AdminDashboardData.problem_syms.each do |sym|
|
AdminDashboardData.problem_syms.each do |sym|
|
||||||
|
@ -67,7 +56,7 @@ class AdminDashboardData
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.problems_started_key
|
def self.problems_started_key
|
||||||
"dash-problems-started-at"
|
'dash-problems-started-at'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.set_problems_started
|
def self.set_problems_started
|
||||||
|
@ -110,14 +99,6 @@ class AdminDashboardData
|
||||||
end
|
end
|
||||||
reset_problem_checks
|
reset_problem_checks
|
||||||
|
|
||||||
def self.fetch_stats
|
|
||||||
AdminDashboardData.new.as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.stats_cache_key
|
|
||||||
'dash-stats'
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.fetch_problems(opts = {})
|
def self.fetch_problems(opts = {})
|
||||||
AdminDashboardData.new(opts).problems
|
AdminDashboardData.new(opts).problems
|
||||||
end
|
end
|
||||||
|
@ -142,29 +123,6 @@ class AdminDashboardData
|
||||||
"admin-problem:#{i18n_key}"
|
"admin-problem:#{i18n_key}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_json(_options = nil)
|
|
||||||
@json ||= {
|
|
||||||
global_reports: AdminDashboardData.reports(GLOBAL_REPORTS),
|
|
||||||
page_view_reports: AdminDashboardData.reports(PAGE_VIEW_REPORTS),
|
|
||||||
private_message_reports: AdminDashboardData.reports(PRIVATE_MESSAGE_REPORTS),
|
|
||||||
http_reports: AdminDashboardData.reports(HTTP_REPORTS),
|
|
||||||
user_reports: AdminDashboardData.reports(USER_REPORTS),
|
|
||||||
mobile_reports: AdminDashboardData.reports(MOBILE_REPORTS),
|
|
||||||
admins: User.admins.count,
|
|
||||||
moderators: User.moderators.count,
|
|
||||||
suspended: User.suspended.count,
|
|
||||||
silenced: User.silenced.count,
|
|
||||||
top_referrers: IncomingLinksReport.find('top_referrers').as_json,
|
|
||||||
top_traffic_sources: IncomingLinksReport.find('top_traffic_sources').as_json,
|
|
||||||
top_referred_topics: IncomingLinksReport.find('top_referred_topics').as_json,
|
|
||||||
updated_at: Time.zone.now.as_json
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.reports(source)
|
|
||||||
source.map { |type| Report.find(type).as_json }
|
|
||||||
end
|
|
||||||
|
|
||||||
def rails_env_check
|
def rails_env_check
|
||||||
I18n.t("dashboard.rails_env_warning", env: Rails.env) unless Rails.env.production?
|
I18n.t("dashboard.rails_env_warning", env: Rails.env) unless Rails.env.production?
|
||||||
end
|
end
|
||||||
|
@ -260,7 +218,7 @@ class AdminDashboardData
|
||||||
|
|
||||||
def missing_mailgun_api_key
|
def missing_mailgun_api_key
|
||||||
return unless SiteSetting.reply_by_email_enabled
|
return unless SiteSetting.reply_by_email_enabled
|
||||||
return unless ActionMailer::Base.smtp_settings[:address]["smtp.mailgun.org"]
|
return unless ActionMailer::Base.smtp_settings[:address]['smtp.mailgun.org']
|
||||||
return unless SiteSetting.mailgun_api_key.blank?
|
return unless SiteSetting.mailgun_api_key.blank?
|
||||||
I18n.t('dashboard.missing_mailgun_api_key')
|
I18n.t('dashboard.missing_mailgun_api_key')
|
||||||
end
|
end
|
||||||
|
@ -274,14 +232,14 @@ class AdminDashboardData
|
||||||
old_themes = RemoteTheme.out_of_date_themes
|
old_themes = RemoteTheme.out_of_date_themes
|
||||||
return unless old_themes.present?
|
return unless old_themes.present?
|
||||||
|
|
||||||
themes_html_format(old_themes, "dashboard.out_of_date_themes")
|
themes_html_format(old_themes, 'dashboard.out_of_date_themes')
|
||||||
end
|
end
|
||||||
|
|
||||||
def unreachable_themes
|
def unreachable_themes
|
||||||
themes = RemoteTheme.unreachable_themes
|
themes = RemoteTheme.unreachable_themes
|
||||||
return unless themes.present?
|
return unless themes.present?
|
||||||
|
|
||||||
themes_html_format(themes, "dashboard.unreachable_themes")
|
themes_html_format(themes, 'dashboard.unreachable_themes')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -291,8 +249,6 @@ class AdminDashboardData
|
||||||
"<li><a href=\"/admin/customize/themes/#{id}\">#{CGI.escapeHTML(name)}</a></li>"
|
"<li><a href=\"/admin/customize/themes/#{id}\">#{CGI.escapeHTML(name)}</a></li>"
|
||||||
end.join("\n")
|
end.join("\n")
|
||||||
|
|
||||||
message = I18n.t(i18n_key)
|
"#{I18n.t(i18n_key)}<ul>#{html}</ul>"
|
||||||
message += "<ul>#{html}</ul>"
|
|
||||||
message
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
class AdminDashboardNextGeneralData < AdminDashboardNextData
|
class AdminDashboardGeneralData < AdminDashboardData
|
||||||
def get_json
|
def get_json
|
||||||
{
|
{
|
||||||
updated_at: Time.zone.now.as_json
|
updated_at: Time.zone.now.as_json
|
|
@ -1,4 +1,4 @@
|
||||||
class AdminDashboardNextIndexData < AdminDashboardNextData
|
class AdminDashboardIndexData < AdminDashboardData
|
||||||
def get_json
|
def get_json
|
||||||
{
|
{
|
||||||
updated_at: Time.zone.now.as_json
|
updated_at: Time.zone.now.as_json
|
|
@ -1,27 +0,0 @@
|
||||||
class AdminDashboardNextData
|
|
||||||
include StatsCacheable
|
|
||||||
|
|
||||||
def initialize(opts = {})
|
|
||||||
@opts = opts
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.fetch_stats
|
|
||||||
new.as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_json
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json(_options = nil)
|
|
||||||
@json ||= get_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.reports(source)
|
|
||||||
source.map { |type| Report.find(type).as_json }
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.stats_cache_key
|
|
||||||
"dashboard-next-data-#{Report::SCHEMA_VERSION}"
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -240,13 +240,11 @@ Discourse::Application.routes.draw do
|
||||||
|
|
||||||
get "version_check" => "versions#show"
|
get "version_check" => "versions#show"
|
||||||
|
|
||||||
get "dashboard" => "dashboard_next#index"
|
get "dashboard" => "dashboard#index"
|
||||||
get "dashboard/general" => "dashboard_next#general"
|
get "dashboard/general" => "dashboard#general"
|
||||||
get "dashboard/moderation" => "dashboard_next#moderation"
|
get "dashboard/moderation" => "dashboard#moderation"
|
||||||
get "dashboard/security" => "dashboard_next#security"
|
get "dashboard/security" => "dashboard#security"
|
||||||
get "dashboard/reports" => "dashboard_next#reports"
|
get "dashboard/reports" => "dashboard#reports"
|
||||||
|
|
||||||
get "dashboard-old" => "dashboard#index"
|
|
||||||
|
|
||||||
resources :dashboard, only: [:index] do
|
resources :dashboard, only: [:index] do
|
||||||
collection do
|
collection do
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { acceptance } from "helpers/qunit-helpers";
|
import { acceptance } from "helpers/qunit-helpers";
|
||||||
|
|
||||||
acceptance("Dashboard Next", {
|
acceptance("Dashboard", {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
settings: {
|
settings: {
|
||||||
dashboard_general_tab_activity_metrics: "page_view_total_reqs"
|
dashboard_general_tab_activity_metrics: "page_view_total_reqs"
|
||||||
|
@ -9,22 +9,16 @@ acceptance("Dashboard Next", {
|
||||||
|
|
||||||
QUnit.test("Dashboard", async assert => {
|
QUnit.test("Dashboard", async assert => {
|
||||||
await visit("/admin");
|
await visit("/admin");
|
||||||
assert.ok(exists(".dashboard-next"), "has dashboard-next class");
|
assert.ok(exists(".dashboard"), "has dashboard-next class");
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("tabs", async assert => {
|
QUnit.test("tabs", async assert => {
|
||||||
await visit("/admin");
|
await visit("/admin");
|
||||||
|
|
||||||
assert.ok(exists(".dashboard-next .navigation-item.general"), "general tab");
|
assert.ok(exists(".dashboard .navigation-item.general"), "general tab");
|
||||||
assert.ok(
|
assert.ok(exists(".dashboard .navigation-item.moderation"), "moderation tab");
|
||||||
exists(".dashboard-next .navigation-item.moderation"),
|
assert.ok(exists(".dashboard .navigation-item.security"), "security tab");
|
||||||
"moderation tab"
|
assert.ok(exists(".dashboard .navigation-item.reports"), "reports tab");
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
exists(".dashboard-next .navigation-item.security"),
|
|
||||||
"security tab"
|
|
||||||
);
|
|
||||||
assert.ok(exists(".dashboard-next .navigation-item.reports"), "reports tab");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("general tab", async assert => {
|
QUnit.test("general tab", async assert => {
|
||||||
|
@ -61,33 +55,33 @@ QUnit.test("general tab - activity metrics", async assert => {
|
||||||
|
|
||||||
QUnit.test("reports tab", async assert => {
|
QUnit.test("reports tab", async assert => {
|
||||||
await visit("/admin");
|
await visit("/admin");
|
||||||
await click(".dashboard-next .navigation-item.reports .navigation-link");
|
await click(".dashboard .navigation-item.reports .navigation-link");
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".dashboard-next .reports-index.section .reports-list .report").length,
|
find(".dashboard .reports-index.section .reports-list .report").length,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
await fillIn(".dashboard-next .filter-reports-input", "flags");
|
await fillIn(".dashboard .filter-reports-input", "flags");
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".dashboard-next .reports-index.section .reports-list .report").length,
|
find(".dashboard .reports-index.section .reports-list .report").length,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
await click(".dashboard-next .navigation-item.security .navigation-link");
|
await click(".dashboard .navigation-item.security .navigation-link");
|
||||||
await click(".dashboard-next .navigation-item.reports .navigation-link");
|
await click(".dashboard .navigation-item.reports .navigation-link");
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".dashboard-next .reports-index.section .reports-list .report").length,
|
find(".dashboard .reports-index.section .reports-list .report").length,
|
||||||
1,
|
1,
|
||||||
"navigating back and forth resets filter"
|
"navigating back and forth resets filter"
|
||||||
);
|
);
|
||||||
|
|
||||||
await fillIn(".dashboard-next .filter-reports-input", "activities");
|
await fillIn(".dashboard .filter-reports-input", "activities");
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
find(".dashboard-next .reports-index.section .reports-list .report").length,
|
find(".dashboard .reports-index.section .reports-list .report").length,
|
||||||
1,
|
1,
|
||||||
"filter is case insensitive"
|
"filter is case insensitive"
|
||||||
);
|
);
|
Loading…
Reference in New Issue
Block a user