From 6960b89f28b8a12ca4ee3aa8403441b2641a33ea Mon Sep 17 00:00:00 2001 From: rhymes Date: Wed, 12 Jun 2019 15:40:42 +0200 Subject: [PATCH] Dashboard Pro: support InstantClick and show org analytics (#3102) * Fix JS lint issues * Do not rely on globals * Use InstantClick to make transitions work in the pro dashboard * Fix eager loadings and remove unused queries * Refactor drawChart * More consistency in naming * Display correct analytics for organization --- app/controllers/dashboards_controller.rb | 9 +- app/javascript/analytics/dashboard.js | 226 ++++++++++++++++++ app/javascript/packs/analyticsDashboard.js | 16 ++ app/javascript/packs/proCharts.js | 263 --------------------- app/views/dashboards/pro.html.erb | 36 ++- 5 files changed, 270 insertions(+), 280 deletions(-) create mode 100644 app/javascript/analytics/dashboard.js create mode 100644 app/javascript/packs/analyticsDashboard.js delete mode 100644 app/javascript/packs/proCharts.js diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb index 121bd5612..8212d5538 100644 --- a/app/controllers/dashboards_controller.rb +++ b/app/controllers/dashboards_controller.rb @@ -9,15 +9,16 @@ class DashboardsController < ApplicationController target = @user not_authorized if params[:org_id] && !@user.org_admin?(params[:org_id] || @user.any_admin?) - @organizations = @user.admin_organizations.includes(:users) - @member_organizations = @user.member_organizations + @organizations = @user.admin_organizations if params[:which] == "organization" && params[:org_id] && (@user.org_admin?(params[:org_id]) || @user.any_admin?) target = @organizations.find_by(id: params[:org_id]) @organization = target end - @articles = target.articles.sorting(params[:sort]).decorate - # Updates analytics in background if appropriate: + + @articles = target.articles.includes(:organization).sorting(params[:sort]).decorate + + # Updates analytics in background if appropriate ArticleAnalyticsFetcher.new.delay.update_analytics(current_user.id) if @articles && ApplicationConfig["GA_FETCH_RATE"] < 50 # Rate limit concerned, sometimes we throttle down. end diff --git a/app/javascript/analytics/dashboard.js b/app/javascript/analytics/dashboard.js new file mode 100644 index 000000000..dd62475bb --- /dev/null +++ b/app/javascript/analytics/dashboard.js @@ -0,0 +1,226 @@ +import Chart from 'chart.js'; + +function resetActive(activeButton) { + const buttons = document.getElementsByClassName('timerange-button'); + for (let i = 0; i < buttons.length; i += 1) { + const button = buttons[i]; + button.classList.remove('selected'); + } + + activeButton.classList.add('selected'); +} + +function sumAnalytics(data, key) { + return Object.entries(data).reduce((sum, day) => sum + day[1][key].total, 0); +} + +function cardHTML(stat, header) { + return ` +

${header}

+ + `; +} + +function writeCards(data, timeRangeLabel) { + const readers = sumAnalytics(data, 'page_views'); + const reactions = sumAnalytics(data, 'reactions'); + const comments = sumAnalytics(data, 'comments'); + const follows = sumAnalytics(data, 'follows'); + + const reactionCard = document.getElementById('reactions-card'); + const commentCard = document.getElementById('comments-card'); + const followerCard = document.getElementById('followers-card'); + const readerCard = document.getElementById('readers-card'); + + readerCard.innerHTML = cardHTML(readers, `Readers ${timeRangeLabel}`); + commentCard.innerHTML = cardHTML(comments, `Comments ${timeRangeLabel}`); + reactionCard.innerHTML = cardHTML(reactions, `Reactions ${timeRangeLabel}`); + followerCard.innerHTML = cardHTML(follows, `Followers ${timeRangeLabel}`); +} + +function drawChart({ canvas, title, labels, datasets }) { + const options = { + legend: { + position: 'bottom', + }, + responsive: true, + title: { + display: true, + text: title, + }, + scales: { + yAxes: [ + { + ticks: { + suggestedMin: 0, + precision: 0, + }, + }, + ], + }, + }; + + // eslint-disable-next-line no-new + new Chart(canvas, { + type: 'line', + data: { + labels, + datasets, + options, + }, + }); +} + +function drawCharts(data, timeRangeLabel) { + const labels = Object.keys(data); + const parsedData = Object.entries(data).map(date => date[1]); + const comments = parsedData.map(date => date.comments.total); + const reactions = parsedData.map(date => date.reactions.total); + const likes = parsedData.map(date => date.reactions.like); + const readingList = parsedData.map(date => date.reactions.readinglist); + const unicorns = parsedData.map(date => date.reactions.unicorn); + const followers = parsedData.map(date => date.follows.total); + const readers = parsedData.map(date => date.page_views.total); + + drawChart({ + canvas: document.getElementById('reactionsChart'), + title: `Reactions ${timeRangeLabel}`, + labels, + datasets: [ + { + label: 'Total', + data: reactions, + fill: false, + borderColor: 'rgb(75, 192, 192)', + lineTension: 0.1, + }, + { + label: 'Likes', + data: likes, + fill: false, + borderColor: 'rgb(229, 100, 100)', + lineTension: 0.1, + }, + { + label: 'Unicorns', + data: unicorns, + fill: false, + borderColor: 'rgb(157, 57, 233)', + lineTension: 0.1, + }, + { + label: 'Bookmarks', + data: readingList, + fill: false, + borderColor: 'rgb(10, 133, 255)', + lineTension: 0.1, + }, + ], + }); + + drawChart({ + canvas: document.getElementById('commentsChart'), + title: `Comments ${timeRangeLabel}`, + labels, + datasets: [ + { + label: 'Comments', + data: comments, + fill: false, + borderColor: 'rgb(75, 192, 192)', + lineTension: 0.1, + }, + ], + }); + + drawChart({ + canvas: document.getElementById('followersChart'), + title: `New Followers ${timeRangeLabel}`, + labels, + datasets: [ + { + label: 'Followers', + data: followers, + fill: false, + borderColor: 'rgb(10, 133, 255)', + lineTension: 0.1, + }, + ], + }); + + drawChart({ + canvas: document.getElementById('readersChart'), + title: `Reads ${timeRangeLabel}`, + labels, + datasets: [ + { + label: 'Reads', + data: readers, + fill: false, + borderColor: 'rgb(157, 57, 233)', + lineTension: 0.1, + }, + ], + }); +} + +function callAnalyticsApi(date, timeRangeLabel, organizationId) { + let url = `/api/analytics/historical?start=${ + date.toISOString().split('T')[0] + }`; + + if (organizationId) { + url = `${url}&organization_id=${organizationId}`; + } + + fetch(url) + .then(data => data.json()) + .then(data => { + drawCharts(data, timeRangeLabel); + writeCards(data, timeRangeLabel); + }); +} + +function drawWeekCharts(organizationId) { + resetActive(document.getElementById('week-button')); + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + callAnalyticsApi(oneWeekAgo, 'this Week', organizationId); +} + +function drawMonthCharts(organizationId) { + resetActive(document.getElementById('month-button')); + const oneMonthAgo = new Date(); + oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); + callAnalyticsApi(oneMonthAgo, 'this Month', organizationId); +} + +function drawInfinityCharts(organizationId) { + resetActive(document.getElementById('infinity-button')); + // April 1st is when the DEV analytics feature went into place + const beginningOfTime = new Date('2019-4-1'); + callAnalyticsApi(beginningOfTime, '', organizationId); +} + +export default function initCharts({ organizationId }) { + const weekButton = document.getElementById('week-button'); + weekButton.addEventListener( + 'click', + drawWeekCharts.bind(null, organizationId), + ); + + const monthButton = document.getElementById('month-button'); + monthButton.addEventListener( + 'click', + drawMonthCharts.bind(null, organizationId), + ); + + const infinityButton = document.getElementById('infinity-button'); + infinityButton.addEventListener( + 'click', + drawInfinityCharts.bind(null, organizationId), + ); + + // draw week charts by default + drawWeekCharts(organizationId); +} diff --git a/app/javascript/packs/analyticsDashboard.js b/app/javascript/packs/analyticsDashboard.js new file mode 100644 index 000000000..11302d5f8 --- /dev/null +++ b/app/javascript/packs/analyticsDashboard.js @@ -0,0 +1,16 @@ +import initCharts from '../analytics/dashboard'; + +function initDashboard() { + const activeOrg = document.querySelector('.organization.active'); + if (activeOrg) { + initCharts({ organizationId: activeOrg.dataset.organizationId }); + } else { + initCharts({ organizationId: null }); + } +} + +window.InstantClick.on('change', () => { + initDashboard(); +}); + +initDashboard(); diff --git a/app/javascript/packs/proCharts.js b/app/javascript/packs/proCharts.js deleted file mode 100644 index 42ac1fd10..000000000 --- a/app/javascript/packs/proCharts.js +++ /dev/null @@ -1,263 +0,0 @@ -import Chart from 'chart.js'; - -const reactionsCanvas = document.getElementById('reactionsChart'); -const commentsCanvas = document.getElementById('commentsChart'); -const readersCanvas = document.getElementById('readersChart'); -const followersCanvas = document.getElementById('followersChart'); - -const weekButton = document.getElementById('week-button'); -const monthButton = document.getElementById('month-button'); -const infinityButton = document.getElementById('infinity-button'); - -const reactionCard = document.getElementById('reactions-card'); -const commentCard = document.getElementById('comments-card'); -const followerCard = document.getElementById('followers-card'); -const readerCard = document.getElementById('readers-card'); - -function resetActive(activeButton) { - [weekButton, monthButton, infinityButton].forEach(button => { - button.classList.remove('selected'); - }); - - activeButton.classList.add('selected'); -} - -function sumAnalytics(data, key) { - return Object.entries(data).reduce((sum, day) => sum + day[1][key].total, 0); -} - -function writeCard(stat, element, header) { - element.innerHTML = ` -

${header}

- - `; -} - -function writeCards(data, timeFrame) { - const readers = sumAnalytics(data, 'page_views'); - const reactions = sumAnalytics(data, 'reactions'); - const comments = sumAnalytics(data, 'comments'); - const follows = sumAnalytics(data, 'follows'); - - writeCard(readers, readerCard, `Readers ${timeFrame}`); - writeCard(comments, commentCard, `Comments ${timeFrame}`); - writeCard(reactions, reactionCard, `Reactions ${timeFrame}`); - writeCard(follows, followerCard, `Followers ${timeFrame}`); -} - -function callAnalyticsApi(date, timeRange) { - fetch(`/api/analytics/historical?start=${date.toISOString().split('T')[0]}`) - .then(data => data.json()) - .then(data => { - drawCharts(data, timeRange); - writeCards(data, timeRange); - }); -} - -function drawWeekCharts() { - resetActive(weekButton); - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - callAnalyticsApi(oneWeekAgo, 'this Week'); -} - -function drawMonthCharts() { - resetActive(monthButton); - const oneMonthAgo = new Date(); - oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); - callAnalyticsApi(oneMonthAgo, 'this Month'); -} - -function drawInfinityCharts() { - resetActive(infinityButton); - // April 1st is when the DEV analytics feature went into place - const beginningOfTime = new Date('2019-4-1'); - callAnalyticsApi(beginningOfTime, ''); -} - -drawWeekCharts(); -weekButton.addEventListener('click', drawWeekCharts); -monthButton.addEventListener('click', drawMonthCharts); -infinityButton.addEventListener('click', drawInfinityCharts); - -function drawCharts(data, timeRange) { - const labels = Object.keys(data); - const parsedData = Object.entries(data).map(date => date[1]); - const comments = parsedData.map(date => date.comments.total); - const reactions = parsedData.map(date => date.reactions.total); - const likes = parsedData.map(date => date.reactions.like); - const readingList = parsedData.map(date => date.reactions.readinglist); - const unicorns = parsedData.map(date => date.reactions.unicorn); - const followers = parsedData.map(date => date.follows.total); - const readers = parsedData.map(date => date.page_views.total); - - const reactionsChart = new Chart(reactionsCanvas, { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Total', - data: reactions, - // data: [5, 10, 15, 17, 25, 23], - fill: false, - borderColor: 'rgb(75, 192, 192)', - lineTension: 0.1, - }, - { - label: 'Likes', - data: likes, - // data: [2, 5, 10, 10, 15, 13], - fill: false, - borderColor: 'rgb(229, 100, 100)', - lineTension: 0.1, - }, - { - label: 'Unicorns', - data: unicorns, - // data: [1, 2, 2, 4, 5, 3], - fill: false, - borderColor: 'rgb(157, 57, 233)', - lineTension: 0.1, - }, - { - label: 'Bookmarks', - data: readingList, - // data: [2, 3, 3, 3, 5, 7], - fill: false, - borderColor: 'rgb(10, 133, 255)', - lineTension: 0.1, - }, - ], - }, - options: { - legend: { - position: 'bottom', - }, - responsive: true, - title: { - display: true, - text: `Reactions ${timeRange}`, - }, - scales: { - yAxes: [ - { - ticks: { - suggestedMin: 0, - precision: 0, - }, - }, - ], - }, - }, - }); - - const commentsChart = new Chart(commentsCanvas, { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Comments', - data: comments, - fill: false, - borderColor: 'rgb(75, 192, 192)', - lineTension: 0.1, - }, - ], - }, - options: { - legend: { - position: 'bottom', - }, - responsive: true, - title: { - display: true, - text: `Comments ${timeRange}`, - }, - scales: { - yAxes: [ - { - ticks: { - suggestedMin: 0, - precision: 0, - }, - }, - ], - }, - }, - }); - - const followChart = new Chart(followersCanvas, { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Followers', - data: followers, - fill: false, - borderColor: 'rgb(10, 133, 255)', - lineTension: 0.1, - }, - ], - }, - options: { - legend: { - position: 'bottom', - }, - responsive: true, - title: { - display: true, - text: `New Followers ${timeRange}`, - }, - scales: { - yAxes: [ - { - ticks: { - suggestedMin: 0, - precision: 0, - }, - }, - ], - }, - }, - }); - - const readsChart = new Chart(readersCanvas, { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Reads', - data: readers, - fill: false, - borderColor: 'rgb(157, 57, 233)', - lineTension: 0.1, - }, - ], - }, - options: { - legend: { - position: 'bottom', - }, - responsive: true, - title: { - display: true, - text: `Reads ${timeRange}`, - }, - scales: { - yAxes: [ - { - ticks: { - suggestedMin: 0, - precision: 0, - }, - }, - ], - }, - }, - }); - -} diff --git a/app/views/dashboards/pro.html.erb b/app/views/dashboards/pro.html.erb index 52bb3622e..4a6aa5b08 100644 --- a/app/views/dashboards/pro.html.erb +++ b/app/views/dashboards/pro.html.erb @@ -2,30 +2,39 @@
👈 Back to Main Dashboard - " href="/dashboard/pro"> - Your Pro Dashboard + + " href="/dashboard/pro"> + Your Pro Dashboard + + + <% @organizations.each do |org| %> + " + href="/dashboard/pro/org/<%= org.id %>" + data-organization-id="<%= org.id %>"> + <%= org.name %> Pro Dashboard - <% @organizations.each do |org| %> - " href="/dashboard/pro/org/<%= org.id %>"> - <%= org.name %> Pro Dashboard - - <% end %> + <% end %> +
-

Pro Dashboard <%= "for #{@dashboard.user_or_org.name}" %>

-

Welcome to the pro dashboard which shows in-depth user metrics so that authors can make data-driven decisions about the developer ecosystem.

+

Pro Dashboard for <%= @dashboard.user_or_org.name %>

+

Welcome to the Pro Dashboard which shows in-depth user metrics so that authors can make data-driven decisions about the developer ecosystem.

This dashboard will highlight deep insights especially relevant to developer relations authors and serious bloggers.

+
- - - + + +
+
+
@@ -56,5 +65,6 @@
- <%= javascript_pack_tag "proCharts", defer: true %>
+ +<%= javascript_pack_tag "analyticsDashboard", defer: true %>