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
This commit is contained in:
parent
18d1300341
commit
6960b89f28
5 changed files with 270 additions and 280 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
226
app/javascript/analytics/dashboard.js
Normal file
226
app/javascript/analytics/dashboard.js
Normal file
|
|
@ -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 `
|
||||
<h4>${header}</h4>
|
||||
<div class="featured-stat">${stat}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
16
app/javascript/packs/analyticsDashboard.js
Normal file
16
app/javascript/packs/analyticsDashboard.js
Normal file
|
|
@ -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();
|
||||
|
|
@ -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 = `
|
||||
<h4>${header}</h4>
|
||||
<div class="featured-stat">${stat}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -2,30 +2,39 @@
|
|||
|
||||
<div class="dashboard-container pro-container" id="user-dashboard">
|
||||
<a href="/dashboard" class="rounded-btn rounded-btn--transparent">👈 Back to Main Dashboard</a>
|
||||
<a class="rounded-btn <%= "active" if params[:org_id].blank? %>" href="/dashboard/pro">
|
||||
Your Pro Dashboard
|
||||
|
||||
<a class="rounded-btn <%= "active" unless params[:org_id] %>" href="/dashboard/pro">
|
||||
Your Pro Dashboard
|
||||
</a>
|
||||
|
||||
<% @organizations.each do |org| %>
|
||||
<a
|
||||
class="rounded-btn organization <%= "active" if params[:org_id].to_i == org.id %>"
|
||||
href="/dashboard/pro/org/<%= org.id %>"
|
||||
data-organization-id="<%= org.id %>">
|
||||
<%= org.name %> Pro Dashboard
|
||||
</a>
|
||||
<% @organizations.each do |org| %>
|
||||
<a class="rounded-btn <%= "active" if params[:org_id].to_i == org.id %>" href="/dashboard/pro/org/<%= org.id %>">
|
||||
<%= org.name %> Pro Dashboard
|
||||
</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<section class="header-card card">
|
||||
<h1 class="pro-header">Pro Dashboard <%= "for #{@dashboard.user_or_org.name}" %></h1>
|
||||
<p>Welcome to the pro dashboard which shows in-depth user metrics so that authors can make data-driven decisions about the developer ecosystem.</p>
|
||||
<h1 class="pro-header">Pro Dashboard for <%= @dashboard.user_or_org.name %></h1>
|
||||
<p>Welcome to the Pro Dashboard which shows in-depth user metrics so that authors can make data-driven decisions about the developer ecosystem.</p>
|
||||
<p>This dashboard will highlight deep insights especially relevant to developer relations authors and serious bloggers.</p>
|
||||
</section>
|
||||
|
||||
<div class="toggles">
|
||||
<button class="selected" id="week-button">Week</button>
|
||||
<button id="month-button">Month</button>
|
||||
<button id="infinity-button">Infinity</button>
|
||||
<button class="selected timerange-button" id="week-button">Week</button>
|
||||
<button class="timerange-button" id="month-button">Month</button>
|
||||
<button class="timerange-button" id="infinity-button">Infinity</button>
|
||||
</div>
|
||||
|
||||
<div class="summary-stats">
|
||||
<div class="card" id="readers-card"></div>
|
||||
<div class="card" id="reactions-card"></div>
|
||||
<div class="card" id="comments-card"></div>
|
||||
<div class="card" id="followers-card"></div>
|
||||
</div>
|
||||
|
||||
<div class="graphs">
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
|
|
@ -56,5 +65,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= javascript_pack_tag "proCharts", defer: true %>
|
||||
</div>
|
||||
|
||||
<%= javascript_pack_tag "analyticsDashboard", defer: true %>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue