diff --git a/app/assets/javascripts/admin/components/dashboard-mini-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 similarity index 95% rename from app/assets/javascripts/admin/components/dashboard-mini-table.js.es6 rename to app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 index e416601d851..070625e9e47 100644 --- a/app/assets/javascripts/admin/components/dashboard-mini-table.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 @@ -2,7 +2,7 @@ import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ - classNames: ["dashboard-mini-table"], + classNames: ["dashboard-table", "dashboard-inline-table"], classNameBindings: ["isLoading"], diff --git a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 index 7d1986721b2..eea4bc917cd 100644 --- a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 @@ -11,22 +11,18 @@ export default Ember.Component.extend({ total: null, trend: null, title: null, - chartData: null, oneDataPoint: false, backgroundColor: "rgba(200,220,240,0.3)", borderColor: "#08C", + didInsertElement() { + this._super(); + this._initializeChart(); + }, + didUpdateAttrs() { this._super(); - - loadScript("/javascripts/Chart.min.js").then(() => { - if (this.get("model") && !this.get("chartData")) { - this._setPropertiesFromModel(this.get("model")); - this._drawChart(); - } else if (this.get("dataSource")) { - this._fetchReport(); - } - }); + this._initializeChart(); }, @computed("dataSourceName") @@ -75,13 +71,27 @@ export default Ember.Component.extend({ }); }, + _initializeChart() { + loadScript("/javascripts/Chart.min.js").then(() => { + if (this.get("model") && !this.get("values")) { + this._setPropertiesFromModel(this.get("model")); + this._drawChart(); + } else if (this.get("dataSource")) { + this._fetchReport(); + } + }); + }, + _drawChart() { - const context = this.$(".chart-canvas")[0].getContext("2d"); + const $chartCanvas = this.$(".chart-canvas"); + if (!$chartCanvas.length) return; + + const context = $chartCanvas[0].getContext("2d"); const data = { - labels: this.get("chartData").map(r => r.x), + labels: this.get("labels"), datasets: [{ - data: this.get("chartData").map(r => r.y), + data: this.get("values"), backgroundColor: this.get("backgroundColor"), borderColor: this.get("borderColor") }] @@ -92,17 +102,18 @@ export default Ember.Component.extend({ _setPropertiesFromModel(model) { this.setProperties({ + labels: model.data.map(r => r.x), + values: model.data.map(r => r.y), oneDataPoint: (this.get("startDate") && this.get("endDate")) && this.get("startDate").isSame(this.get("endDate"), 'day'), total: model.total, title: model.title, - trend: this._computeTrend(model.total, model.prev30Days), - chartData: model.data + trend: this._computeTrend(model.total, model.prev30Days) }); }, _buildChartConfig(data) { - const values = this.get("chartData").map(d => d.y); + const values = this.get("values"); const max = Math.max(...values); const min = Math.min(...values); const stepSize = Math.max(...[Math.ceil((max - min)/5), 20]); diff --git a/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 b/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 new file mode 100644 index 00000000000..aac53bb9c0c --- /dev/null +++ b/app/assets/javascripts/admin/components/dashboard-table-trending-search.js.es6 @@ -0,0 +1,17 @@ +import DashboardTable from "admin/components/dashboard-table"; +import { number } from 'discourse/lib/formatter'; + +export default DashboardTable.extend({ + layoutName: "admin/templates/components/dashboard-table", + + classNames: ["dashboard-table", "dashboard-table-trending-search"], + + transformModel(model) { + return { + labels: model.labels, + values: model.data.map(data => { + return [data[0], number(data[1]), number(data[2])]; + }) + }; + }, +}); diff --git a/app/assets/javascripts/admin/components/dashboard-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-table.js.es6 new file mode 100644 index 00000000000..2bf5929443d --- /dev/null +++ b/app/assets/javascripts/admin/components/dashboard-table.js.es6 @@ -0,0 +1,83 @@ +import { ajax } from 'discourse/lib/ajax'; +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNames: ["dashboard-table"], + + classNameBindings: ["isLoading"], + + total: null, + labels: null, + title: null, + chartData: null, + isLoading: false, + help: null, + helpPage: null, + model: null, + + transformModel(model) { + const data = model.data.sort((a, b) => a.x >= b.x); + + return { + labels: model.labels, + values: data + }; + }, + + didInsertElement() { + this._super(); + this._initializeTable(); + }, + + didUpdateAttrs() { + this._super(); + this._initializeTable(); + }, + + @computed("dataSourceName") + dataSource(dataSourceName) { + return `/admin/reports/${dataSourceName}`; + }, + + _initializeTable() { + if (this.get("model") && !this.get("values")) { + this._setPropertiesFromModel(this.get("model")); + } else if (this.get("dataSource")) { + this._fetchReport(); + } + }, + + _fetchReport() { + if (this.get("isLoading")) return; + + this.set("isLoading", true); + + let payload = {data: {}}; + + if (this.get("startDate")) { + payload.data.start_date = this.get("startDate").toISOString(); + } + + if (this.get("endDate")) { + payload.data.end_date = this.get("endDate").toISOString(); + } + + ajax(this.get("dataSource"), payload) + .then((response) => { + this._setPropertiesFromModel(response.report); + }).finally(() => { + this.set("isLoading", false); + }); + }, + + _setPropertiesFromModel(model) { + const { labels, values } = this.transformModel(model); + + this.setProperties({ + labels, + values, + total: model.total, + title: model.title + }); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 index 443597c2fb0..3afb55343dd 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 @@ -34,7 +34,7 @@ export default Ember.Controller.extend({ }).finally(() => { this.set("isLoading", false); }); - }; + } }, @computed("period") diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 index afb2eaaec5f..30ca9b033c0 100644 --- a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 @@ -1,5 +1,5 @@ export default Discourse.Route.extend({ - setupController(controller) { - controller.fetchDashboard(); + activate() { + this.controllerFor('admin-dashboard-next').fetchDashboard(); } }); diff --git a/app/assets/javascripts/admin/templates/components/dashboard-mini-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/components/dashboard-mini-table.hbs rename to app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs diff --git a/app/assets/javascripts/admin/templates/components/dashboard-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-table.hbs new file mode 100644 index 00000000000..cb4070e40ea --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/dashboard-table.hbs @@ -0,0 +1,31 @@ +{{#conditional-loading-spinner condition=isLoading}} +
+

{{title}}

+ + {{#if help}} + {{i18n help}} + {{/if}} +
+ +
+ + + + {{#each labels as |label|}} + + {{/each}} + + + + + {{#each values as |value|}} + + {{#each value as |v|}} + + {{/each}} + + {{/each}} + +
{{label}}
{{v}}
+
+{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/dashboard_next.hbs b/app/assets/javascripts/admin/templates/dashboard_next.hbs index 7ed4c78904c..99f3de35bd1 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next.hbs @@ -1,72 +1,82 @@ {{plugin-outlet name="admin-dashboard-top"}} - - -
-
-

{{i18n "admin.dashboard.community_health"}}

- {{period-chooser period=period action="changePeriod"}} -
- -
-
- {{dashboard-mini-chart - model=global_reports_signups - dataSourceName="signups" - startDate=startDate - endDate=endDate - help="admin.dashboard.charts.signups.help"}} - - {{dashboard-mini-chart - model=global_reports_topics - dataSourceName="topics" - startDate=startDate - endDate=endDate - help="admin.dashboard.charts.topics.help"}} -
-
+{{lastRefreshedAt}} +
+
+

{{i18n "admin.dashboard.community_health"}}

+ {{period-chooser period=period action="changePeriod"}}
-
-
- {{dashboard-mini-table model=user_reports_users_by_type isLoading=isLoading}} - {{dashboard-mini-table model=user_reports_users_by_trust_level isLoading=isLoading}} +
+
+ {{dashboard-mini-chart + model=global_reports_signups + dataSourceName="signups" + startDate=startDate + endDate=endDate + help="admin.dashboard.charts.signups.help"}} - {{#conditional-loading-spinner isLoading=isLoading}} -
-
- {{#if currentUser.admin}} -
-

{{i18n "admin.dashboard.backups"}}

-

- {{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}}) -
- {{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}} -

-
- {{/if}} + {{dashboard-mini-chart + model=global_reports_topics + dataSourceName="topics" + startDate=startDate + endDate=endDate + help="admin.dashboard.charts.topics.help"}} +
+
+
-
-

{{i18n "admin.dashboard.uploads"}}

+
+
+ {{dashboard-inline-table + model=user_reports_users_by_type + lastRefreshedAt=lastRefreshedAt + isLoading=isLoading}} + + {{dashboard-inline-table + model=user_reports_users_by_trust_level + lastRefreshedAt=lastRefreshedAt + isLoading=isLoading}} + + {{#conditional-loading-spinner isLoading=isLoading}} +
+
+ {{#if currentUser.admin}} +
+

{{i18n "admin.dashboard.backups"}}

- {{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}}) + {{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}}) +
+ {{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}

+ {{/if}} + +
+

{{i18n "admin.dashboard.uploads"}}

+

+ {{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}}) +

- -
- -

- {{i18n "admin.dashboard.last_updated"}} {{updatedTimestamp}} -

- - - {{i18n "admin.dashboard.whats_new_in_discourse"}} -
- {{/conditional-loading-spinner}} -
+
-
-
+

+ {{i18n "admin.dashboard.last_updated"}} {{updatedTimestamp}} +

+ + + {{i18n "admin.dashboard.whats_new_in_discourse"}} + +
+ {{/conditional-loading-spinner}}
+ +
+ {{dashboard-table-trending-search + model=global_reports_trending_search + dataSourceName="trending_search" + startDate=startDate + endDate=endDate}} +
+
diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss index 1b769a3db7d..b0ade25fb53 100644 --- a/app/assets/stylesheets/common/admin/dashboard_next.scss +++ b/app/assets/stylesheets/common/admin/dashboard_next.scss @@ -9,8 +9,15 @@ justify-content: space-between; .section-column { - flex: 1; - flex-grow: 1; + min-width: calc(50% - .5em); + } + + .section-column:last-child { + margin-left: .5em; + } + + .section-column:first-child { + margin-right: .5em; } } @@ -33,7 +40,7 @@ } } - .dashboard-mini-table { + .dashboard-table { margin-bottom: 1em; &.is-loading { @@ -81,14 +88,9 @@ flex-wrap: wrap; .dashboard-mini-chart { - flex-grow: 1; width: calc(100% * (1/3)); margin-bottom: 1em; - margin-right: 1em; - - &:last-child { - margin-right: 0; - } + flex-grow: 1; &.is-loading { height: 150px; @@ -104,7 +106,7 @@ display: flex; h3 { - margin: .5em 0; + margin: 1em 0; } } @@ -142,6 +144,7 @@ .chart-container { position: relative; + padding: 0 1em; } .chart-trend { diff --git a/app/controllers/admin/dashboard_next_controller.rb b/app/controllers/admin/dashboard_next_controller.rb index 4896deb2b34..7e6260cbf8f 100644 --- a/app/controllers/admin/dashboard_next_controller.rb +++ b/app/controllers/admin/dashboard_next_controller.rb @@ -1,8 +1,8 @@ require 'disk_space' + class Admin::DashboardNextController < Admin::AdminController def index - dashboard_data = AdminDashboardNextData.fetch_cached_stats || Jobs::DashboardNextStats.new.execute({}) - dashboard_data.merge!(version_check: DiscourseUpdates.check_version.as_json) if SiteSetting.version_checks? + dashboard_data = AdminDashboardNextData.fetch_stats dashboard_data[:disk_space] = DiskSpace.cached_stats render json: dashboard_data end diff --git a/app/jobs/scheduled/dashboard_next_stats.rb b/app/jobs/scheduled/dashboard_next_stats.rb deleted file mode 100644 index cf8a4853dff..00000000000 --- a/app/jobs/scheduled/dashboard_next_stats.rb +++ /dev/null @@ -1,20 +0,0 @@ -require_dependency 'admin_dashboard_data' -require_dependency 'group' -require_dependency 'group_message' - -module Jobs - class DashboardNextStats < Jobs::Scheduled - every 30.minutes - - def execute(args) - problems_started_at = AdminDashboardNextData.problems_started_at - if problems_started_at && problems_started_at < 2.days.ago - # If there have been problems reported on the dashboard for a while, - # send a message to admins no more often than once per week. - GroupMessage.create(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i) - end - - AdminDashboardNextData.refresh_stats - end - end -end diff --git a/app/models/admin_dashboard_next_data.rb b/app/models/admin_dashboard_next_data.rb index ee478a13ebc..ca6ce399cdc 100644 --- a/app/models/admin_dashboard_next_data.rb +++ b/app/models/admin_dashboard_next_data.rb @@ -4,6 +4,7 @@ class AdminDashboardNextData GLOBAL_REPORTS ||= [ 'signups', 'topics', + 'trending_search' ] USER_REPORTS ||= [ diff --git a/app/models/report.rb b/app/models/report.rb index 508256e9863..a95fa7b9ac1 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -2,7 +2,7 @@ require_dependency 'topic_subtype' class Report - attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :category_id, :group_id + attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :category_id, :group_id, :labels def self.default_days 30 @@ -26,7 +26,8 @@ class Report end_date: end_date, category_id: category_id, group_id: group_id, - prev30Days: self.prev30Days + prev30Days: self.prev30Days, + labels: labels }.tap do |json| if type == 'page_view_crawler_reqs' json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json @@ -261,4 +262,27 @@ class Report silenced = User.real.silenced.count report.data << { x: label.call("silenced"), y: silenced } if silenced > 0 end + + def self.report_trending_search(report) + report.data = [] + + trends = SearchLog.select("term, + COUNT(*) AS searches, + SUM(CASE + WHEN search_result_id IS NOT NULL THEN 1 + ELSE 0 + END) AS click_through, + COUNT(DISTINCT ip_address) AS unique") + .where('created_at > ? AND created_at <= ?', report.start_date, report.end_date) + .group(:term) + .order('COUNT(DISTINCT ip_address) DESC, COUNT(*) DESC') + .limit(20).to_a + + label = Proc.new { |key| I18n.t("reports.trending_search.labels.#{key}") } + report.labels = [:term, :searches, :unique].map {|key| label.call(key) } + + trends.each do |trend| + report.data << [trend.term, trend.searches, trend.unique] + end + end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fb4355ddd83..bbc6fe7ac32 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -878,6 +878,12 @@ en: moderator: Moderator suspended: Suspended silenced: Silenced + trending_search: + title: Trending search + labels: + term: Term + searches: Searches + unique: Unique emails: title: "Emails Sent" xaxis: "Day" diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 9bd18999384..4fc1ab63ace 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -279,6 +279,37 @@ describe Report do end end + describe 'trending search report' do + let(:report) { Report.find('trending_search') } + + context "no searches" do + it "returns an empty report" do + expect(report.data).to be_blank + end + end + + context "with different searches" do + before do + SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1') + SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1', user_id: Fabricate(:user).id) + SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.2') + SearchLog.log(term: 'php', search_type: :header, ip_address: '127.0.0.1') + end + + it "returns a report with data" do + expect(report.data).to be_present + + expect(report.data[0][0]).to eq "ruby" + expect(report.data[0][1]).to eq 3 + expect(report.data[0][2]).to eq 2 + + expect(report.data[1][0]).to eq "php" + expect(report.data[1][1]).to eq 1 + expect(report.data[1][2]).to eq 1 + end + end + end + describe 'posts counts' do it "only counts regular posts" do post = Fabricate(:post)