Commit dbbaf90c authored by Jonne Haß's avatar Jonne Haß

Merge pull request #6216 from svbergerem/admin-page-update-notifications

Add dashboard to admin page
parents 3d6ae08e 484e70a6
......@@ -48,6 +48,7 @@ With the port to Bootstrap 3, app/views/terms/default.haml has a new structure.
* Support color themes [#6033](https://github.com/diaspora/diaspora/pull/6033)
* Add mobile services and privacy settings pages [#6086](https://github.com/diaspora/diaspora/pull/6086)
* Optionally make your extended profile details public [#6162](https://github.com/diaspora/diaspora/pull/6162)
* Add admin dashboard showing latest diaspora\* version [#6216](https://github.com/diaspora/diaspora/pull/6216)
# 0.5.3.0
......
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.pages.AdminDashboard = Backbone.View.extend({
initialize: function() {
this.updatePodStatus();
},
updatePodStatus: function() {
var self = this,
tagName = "";
$.get("https://api.github.com/repos/diaspora/diaspora/releases/latest")
.done(function(data) {
// the response might be malformed
try {
/* jshint camelcase: false */
tagName = data.tag_name;
/* jshint camelcase: true */
if(tagName.charAt(0) !== "v") {
self.updatePodStatusFail();
return;
}
} catch(e) {
self.updatePodStatusFail();
return;
}
// split version into components
self.latestVersion = tagName.slice(1).split(".").map(Number);
if(self.podUpToDate() === null) {
self.updatePodStatusFail();
} else {
self.updatePodStatusSuccess();
}
})
.fail(function() {
self.updatePodStatusFail();
});
},
updatePodStatusSuccess: function() {
$("#pod-status .alert").removeClass("alert-info");
var podStatusMessage = Diaspora.I18n.t("admins.dashboard.up_to_date");
if(this.podUpToDate()) {
$("#pod-status .alert").addClass("alert-success");
} else {
podStatusMessage = Diaspora.I18n.t("admins.dashboard.outdated");
$("#pod-status .alert").addClass("alert-danger");
}
$("#pod-status .alert")
.html("<strong>" + podStatusMessage + "</strong>")
.append(" ")
.append(Diaspora.I18n.t("admins.dashboard.compare_versions", {
latestVersion: "v" + this.latestVersion.join("."),
podVersion: "v" + gon.podVersion
}));
},
updatePodStatusFail: function() {
$("#pod-status .alert")
.removeClass("alert-info")
.addClass("alert-warning")
.text(Diaspora.I18n.t("admins.dashboard.error"));
},
podUpToDate: function() {
var podVersion = gon.podVersion.split(/\.|\-/).map(Number);
if(this.latestVersion.length < 4 || podVersion.length < 4) { return null; }
for(var i = 0; i < 4; i++) {
if(this.latestVersion[i] < podVersion[i]) { return true; }
if(this.latestVersion[i] > podVersion[i]) { return false; }
}
return true;
}
});
// @license-end
......@@ -10,6 +10,7 @@ app.Router = Backbone.Router.extend({
"user/edit": "settings",
"users/sign_up": "registration",
"profile/edit": "settings",
"admins/dashboard": "adminDashboard",
//new hotness
"posts/:id": "singlePost",
......@@ -47,6 +48,10 @@ app.Router = Backbone.Router.extend({
app.help.render(section);
},
adminDashboard: function() {
app.page = new app.pages.AdminDashboard();
},
contacts: function() {
app.aspect = new app.models.Aspect(gon.preloads.aspect);
app.contacts = new app.collections.Contacts(app.parsePreload("contacts"));
......
@import 'colors';
/** ADMIN STYlES **/
body > div.container {
margin-top: 40px;
padding-top: 1em;
}
#admin_nav {
font-size: 1em;
border-bottom: 2px solid #777;
margin-bottom: 20px;
ul {
display: inline;
}
li {
font-size: 0.8em;
display: inline;
margin-right: 0.5em;
a { color: $blue; }
}
}
/** user search **/
.users {
......
......@@ -92,7 +92,7 @@
<li><a href="/user/edit">{{t "header.settings"}}</a></li>
<li><a href="/help">{{t "header.help"}}</a></li>
{{#if current_user.admin}}
<li><a href="/admins/user_search">{{t "header.admin"}}</a></li>
<li><a href="/admins/dashboard">{{t "header.admin"}}</a></li>
{{/if}}
<li><a href="/users/sign_out" data-method="delete">{{t "header.log_out"}}</a></li>
</ul>
......
class AdminsController < Admin::AdminController
include ApplicationHelper
def dashboard
gon.push(pod_version: pod_version)
end
def user_search
if params[:admins_controller_user_search]
......
- content_for :head do
= stylesheet_link_tag :admin
#admin_nav
%h2
= t('.pages')
%ul
%li= link_to t('.user_search'), user_search_path
%li= link_to t('.weekly_user_stats'), weekly_user_stats_path
%li= link_to t('.pod_stats'), pod_stats_path
%li= link_to t('.report'), report_index_path
%li= link_to t('.correlations'), correlations_path
%li= link_to t('.sidekiq_monitor'), sidekiq_path
%h2= t(".pages")
%ul#admin_nav.nav.nav-pills.nav-stacked
%li{role: "presentation", class: current_page?(admin_dashboard_path) && "active"}
= link_to t('.dashboard'), admin_dashboard_path
%li{role: "presentation", class: current_page?(user_search_path) && "active"}
= link_to t('.user_search'), user_search_path
%li{role: "presentation", class: current_page?(weekly_user_stats_path) && "active"}
= link_to t('.weekly_user_stats'), weekly_user_stats_path
%li{role: "presentation", class: current_page?(pod_stats_path) && "active"}
= link_to t('.pod_stats'), pod_stats_path
%li{role: "presentation", class: current_page?(report_index_path) && "active"}
= link_to t('.report'), report_index_path
%li{role: "presentation", class: current_page?(correlations_path) && "active"}
= link_to t('.correlations'), correlations_path
%li{role: "presentation", class: current_page?(sidekiq_path) && "active"}
= link_to t('.sidekiq_monitor'), sidekiq_path
.container
%div
= render :partial => 'admins/admin_bar'
%div.row
%div.col-md-12
.row
.col-md-3
= render partial: "admins/admin_bar"
.col-md-9
%h1
= t('.correlations_count')
%ul
......
.container
.row
.col-md-3
= render partial: "admins/admin_bar"
.col-md-9
#pod-status
%h2
= t(".pod_status")
.alert.alert-info{role: "alert"}
= t(".fetching_diaspora_version")
.container
%div
= render :partial => 'admins/admin_bar'
%h1
= t('.usage_statistic')
%div.pull-right
= form_tag('/admins/stats', :method => 'get', class: 'form-inline') do
%select{:name => 'range'}
%option{:value => 'daily', :selected => ('selected' if params[:range] == 'daily')}
= t('.daily')
%option{:value => 'week', :selected => ('selected' if params[:range] == 'week')}
= t('.week')
%option{:value => '2weeks', :selected => ('selected' if params[:range] == '2weeks')}
= t('.2weeks')
%option{:value => 'month', :selected => ('selected' if params[:range] == 'month')}
= t('.month')
= submit_tag t('.go'), class: 'btn btn-primary'
%h3
!= t('.display_results', :segment => @segment)
%div.row
- [:posts, :comments, :aspect_memberships, :users].each do |name|
- model = eval("@#{name.to_s}")
- if name == :aspect_memberships
- name = t('.shares', :count => model[:yesterday])
- if name == :posts
- name = t('.posts', :count => model[:yesterday])
- if name == :comments
- name = t('.comments', :count => model[:yesterday])
- if name == :users
- name = t('.users', :count => model[:yesterday])
.col-md-3
%h2{:style => 'font-weight:bold;'}
= name.to_s
%h4
= model[:day_before]
%span.percent_change{:class => (model[:change] > 0 ? "green" : "red")}
= "(#{model[:change]}%)"
%div.row
%div.col-md-12
%p.alert.alert-info.text-center
!= t('.current_segment', :post_yest => @posts[:yesterday]/@user_count.to_f, :post_day => @posts[:day_before]/@user_count.to_f)
%div.row
%div.col-md-12
%h3= t('.50_most')
%ul
- @popular_tags.each do |name,count|
%li
!= t('.tag_name', :name_tag => name, :count_tag => count)
.row
.col-md-3
= render partial: "admins/admin_bar"
.col-md-9
%h1= t('.usage_statistic')
.pull-right
= form_tag('/admins/stats', :method => 'get', class: 'form-inline') do
%select{:name => 'range'}
%option{:value => 'daily', :selected => ('selected' if params[:range] == 'daily')}
= t('.daily')
%option{:value => 'week', :selected => ('selected' if params[:range] == 'week')}
= t('.week')
%option{:value => '2weeks', :selected => ('selected' if params[:range] == '2weeks')}
= t('.2weeks')
%option{:value => 'month', :selected => ('selected' if params[:range] == 'month')}
= t('.month')
= submit_tag t('.go'), class: 'btn btn-primary'
%h3
!= t('.display_results', :segment => @segment)
.row
- [:posts, :comments, :aspect_memberships, :users].each do |name|
- model = eval("@#{name.to_s}")
- if name == :aspect_memberships
- name = t('.shares', :count => model[:yesterday])
- if name == :posts
- name = t('.posts', :count => model[:yesterday])
- if name == :comments
- name = t('.comments', :count => model[:yesterday])
- if name == :users
- name = t('.users', :count => model[:yesterday])
.col-md-3
%h2{:style => 'font-weight:bold;'}
= name.to_s
%h4
= model[:day_before]
%span.percent_change{:class => (model[:change] > 0 ? "green" : "red")}
= "(#{model[:change]}%)"
.row
.col-md-12
%p.alert.alert-info.text-center
!= t('.current_segment', :post_yest => @posts[:yesterday]/@user_count.to_f, :post_day => @posts[:day_before]/@user_count.to_f)
.row
.col-md-12
%h3= t('.50_most')
%ul
- @popular_tags.each do |name,count|
%li
!= t('.tag_name', :name_tag => name, :count_tag => count)
.container
%div
= render :partial => 'admins/admin_bar'
%div.row
%div.user_search.col-md-8
%h3= t('admins.admin_bar.user_search')
= form_for @search, url: {action: 'user_search'}, html: {method: :get, class: 'form-horizontal'} do |f|
%div.form-group
= f.label :username, t('username'), class: 'col-sm-2 control-label'
%div.col-sm-10
= f.text_field :username, class: "form-control"
%div.form-group
= f.label :email, t('email'), class: 'col-sm-2 control-label'
%div.col-sm-10
= f.text_field :email, class: "form-control"
%div.form-group
= f.label :guid, t('admins.user_entry.guid'), class: 'col-sm-2 control-label'
%div.col-sm-10
= f.text_field :guid, class: "form-control"
%div.form-group
%div.col-sm-offset-2.col-sm-10
= f.label :under13 do
= f.check_box :under13
= t(".under_13")
%div.form-group
%div.clearfix.col-sm-12
= submit_tag t("admins.stats.go"), class: "btn btn-primary pull-right"
%div.more_invites.col-md-4
%h3= t("shared.invitations.invites")
#add-invites-section.clearfix
!= t(".you_currently", count: current_user.invitation_code.count,
link: link_to(t(".add_invites"), add_invites_path(current_user.invitation_code),
class: "btn btn-link pull-right"))
= form_tag "admin_inviter", method: :get, class: "form-horizontal" do
.form-group
%label.col-sm-4.control-label
= t(".email_to")
.col-sm-8
= text_field_tag "identifier", nil, class: "form-control"
.form-group
.clearfix.col-md-12
= submit_tag t("services.remote_friend.invite"), class: "btn btn-default pull-right"
%div.row
%div.col-md-12
%div.alert.alert-info.text-center= t('.users', :count => @users.count)
%div.row
%div.users.col-md-12
%ul.media-list
- @users.each do |user|
= render partial: 'user_entry', locals: { user: user }
.row
.col-md-3
= render partial: "admins/admin_bar"
.col-md-9
.row
.user_search.col-md-8
%h3= t('admins.admin_bar.user_search')
= form_for @search, url: {action: 'user_search'}, html: {method: :get, class: 'form-horizontal'} do |f|
.form-group
= f.label :username, t('username'), class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :username, class: "form-control"
.form-group
= f.label :email, t('email'), class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :email, class: "form-control"
.form-group
= f.label :guid, t('admins.user_entry.guid'), class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :guid, class: "form-control"
.form-group
.col-sm-offset-2.col-sm-10
= f.label :under13 do
= f.check_box :under13
= t(".under_13")
.form-group
.clearfix.col-sm-12
= submit_tag t("admins.stats.go"), class: "btn btn-primary pull-right"
.more_invites.col-md-4
%h3= t("shared.invitations.invites")
#add-invites-section.clearfix
!= t(".you_currently", count: current_user.invitation_code.count,
link: link_to(t(".add_invites"), add_invites_path(current_user.invitation_code),
class: "btn btn-link pull-right"))
= form_tag "admin_inviter", method: :get, class: "form-horizontal" do
.form-group
%label.col-sm-4.control-label
= t(".email_to")
.col-sm-8
= text_field_tag "identifier", nil, class: "form-control"
.form-group
.clearfix.col-md-12
= submit_tag t("services.remote_friend.invite"), class: "btn btn-default pull-right"
.row
.col-md-12
.alert.alert-info.text-center= t('.users', :count => @users.count)
.row
.users.col-md-12
%ul.media-list
- @users.each do |user|
= render partial: 'user_entry', locals: { user: user }
.container
%div
= render :partial => 'admins/admin_bar'
.row
.col-md-3
= render partial: "admins/admin_bar"
.col-md-9
%h2
= t('.current_server', date: Time.now.to_date)
%h2
= t('.current_server', date: Time.now.to_date)
.pull-right
= form_tag('/admins/weekly_user_stats', method: 'get', class: 'form-inline') do
= select_tag(:week, options_for_select(@created_users_by_week.keys.reverse, @selected_week))
= submit_tag t('admins.stats.go'), class: 'btn btn-primary'
%div.pull-right
= form_tag('/admins/weekly_user_stats', method: 'get', class: 'form-inline') do
= select_tag(:week, options_for_select(@created_users_by_week.keys.reverse, @selected_week))
= submit_tag t('admins.stats.go'), class: 'btn btn-primary'
= t('.amount_of', count: @counter)
%br
- @created_users_by_week[@selected_week].each do |m|
= link_to m, "/u/#{m}"
%br
= t('.amount_of', count: @counter)
%br
- @created_users_by_week[@selected_week].each do |m|
= link_to m, "/u/#{m}"
%br
.container
%div
= render :partial => 'admins/admin_bar'
%div.row
%div.col-md-12
.row
.col-md-3
= render partial: "admins/admin_bar"
.col-md-9
%h1
= t('report.title')
%div#reports
#reports
- @reports.each do |r|
- username = User.find_by_id(r.user_id).username
%div.content
.content
%span.text
= report_content(r.item_id, r.item_type)
%span
= raw t('report.reported_label', person: link_to(username, user_profile_path(username)))
%span
= t('report.reason_label', text: r.text)
%div.options.text-right
.options.text-right
%span
= button_to t('report.review_link'), report_path(r.id, :type => r.item_type),
:class => "btn btn-info btn-small",
......@@ -26,4 +25,4 @@
:data => { :confirm => t('report.confirm_deletion') },
:class => "btn btn-danger btn-small",
method: :delete
%div.clear
.clear
......@@ -98,12 +98,16 @@ en:
admins:
admin_bar:
pages: "Pages"
dashboard: "Dashboard"
user_search: "User search"
weekly_user_stats: "Weekly user stats"
pod_stats: "Pod stats"
report: "Reports"
correlations: "Correlations"
sidekiq_monitor: "Sidekiq monitor"
dashboard:
pod_status: "Pod status"
fetching_diaspora_version: "Fetching current diaspora* version..."
correlations:
correlations_count: "Correlations with sign-in count:"
user_search:
......
......@@ -29,6 +29,13 @@ en:
edit: "Edit"
no_results: "No results found"
admins:
dashboard:
up_to_date: "Your pod is up to date!"
outdated: "Your pod is outdated."
compare_versions: "The latest diaspora* release is <%= latestVersion %>, your pod is running <%= podVersion %>."
error: "Error fetching the latest diaspora* version."
aspects:
make_aspect_list_visible: "Make contacts in this aspect visible to each other?"
name: "Name"
......
......@@ -141,13 +141,14 @@ Diaspora::Application.routes.draw do
# Admin backend routes
scope 'admins', :controller => :admins do
scope "admins", controller: :admins do
match :user_search, via: [:get, :post]
get :admin_inviter
get :weekly_user_stats
get :correlations
get :stats, :as => 'pod_stats'
get "add_invites/:invite_code_id" => 'admins#add_invites', :as => 'add_invites'
get :admin_inviter
get :weekly_user_stats
get :correlations
get :stats, as: "pod_stats"
get :dashboard, as: "admin_dashboard"
get "add_invites/:invite_code_id" => "admins#add_invites", :as => "add_invites"
end
namespace :admin do
......
require "spec_helper"
describe AdminsController, type: :controller do
describe "#dashboard" do
before do
@user = FactoryGirl.create :user
Role.add_admin(@user.person)
sign_in :user, @user
end
context "jasmine fixtures" do
it "generates a jasmine fixture", fixture: true do
get :dashboard
save_fixture(html_for("body"), "admin_dashboard")
end
end
end
end
describe("app.pages.AdminDashboard", function(){
beforeEach(function() {
spec.loadFixture("admin_dashboard");
this.view = new app.pages.AdminDashboard();
gon.podVersion = "0.5.1.2";
// disable jshint camelcase for i18n
/* jshint camelcase: false */
Diaspora.I18n.load({
admins: {
dashboard: {
up_to_date: "Your pod is up to date!",
outdated: "Your pod is outdated.",
compare_versions: "Latest d* release is <%= latestVersion%>, your pod is running <%= podVersion %>.",
error: "Error fetching the latest diaspora* version."
}
}
});
/* jshint camelcase: true */
});
describe("initialize" , function() {
it("calls updatePodStatus", function() {
spyOn(this.view, "updatePodStatus");
this.view.initialize();
expect(this.view.updatePodStatus).toHaveBeenCalled();
});
});
describe("updatePodStatus" , function() {
it("sends an ajax request to the github API", function() {
this.view.updatePodStatus();
expect(jasmine.Ajax.requests.mostRecent().url).toBe(
"https://api.github.com/repos/diaspora/diaspora/releases/latest"
);
});
it("calls updatePodStatusFail on a failed request", function() {
spyOn(this.view, "updatePodStatusFail");
this.view.updatePodStatus();
jasmine.Ajax.requests.mostRecent().respondWith({status: 400});
expect(this.view.updatePodStatusFail).toHaveBeenCalled();
});
it("calls updatePodStatusFail on a malformed response", function() {
spyOn(this.view, "updatePodStatusFail");
spyOn(this.view, "podUpToDate").and.returnValue(true);
var responses = [
// no object
"text",
// object without tag_name
"{\"tag\": 0}",
// tag_name not a string
"{\"tag_name\": 0}",
"{\"tag_name\": {\"id\": 0}}",
// tag_name doesn't start with "v"
"{\"tag_name\": \"0.5.1.2\"}"
];
for(var i = 0; i < responses.length; i++) {
this.view.updatePodStatus();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: responses[i]
});
expect(this.view.updatePodStatusFail.calls.count()).toEqual(i+1);
}
});
it("sets latestVersion on a correct response", function() {
this.view.updatePodStatus();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: "{\"tag_name\": \"v0.5.1.2\"}"
});
expect(this.view.latestVersion).toEqual([0,5,1,2]);
});
it("calls podUpToDate on a correct response", function() {
spyOn(this.view, "podUpToDate");
this.view.updatePodStatus();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: "{\"tag_name\": \"v0.5.1.2\"}"
});
expect(this.view.podUpToDate).toHaveBeenCalled();
});
it("calls updatePodStatusFail if podUpToDate returns null", function() {
spyOn(this.view, "updatePodStatusFail");
spyOn(this.view, "podUpToDate").and.returnValue(null);
this.view.updatePodStatus();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: "{\"tag_name\": \"v0.5.1.2\"}"
});
expect(this.view.updatePodStatusFail).toHaveBeenCalled();
});
it("calls updatePodStatusSuccess if podUpToDate returns a Boolean", function() {
spyOn(this.view, "updatePodStatusSuccess");
spyOn(this.view, "podUpToDate").and.returnValue(false);
this.view.updatePodStatus();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: "{\"tag_name\": \"v0.5.1.2\"}"
});
expect(this.view.updatePodStatusSuccess).toHaveBeenCalled();
});
});
describe("podUpToDate" , function() {