Commit 6003e710 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'feature/geo-node-status-ajax' into 'master'

Get Geo secondaries nodes statuses over AJAX

See merge request !1438
parents d83414e6 84e5f5a1
...@@ -43,6 +43,8 @@ import GroupsList from './groups_list'; ...@@ -43,6 +43,8 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import GeoNodes from './geo_nodes';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout'); const UserCallout = require('./user_callout');
...@@ -348,6 +350,9 @@ const UserCallout = require('./user_callout'); ...@@ -348,6 +350,9 @@ const UserCallout = require('./user_callout');
case 'abuse_reports': case 'abuse_reports':
new gl.AbuseReports(); new gl.AbuseReports();
break; break;
case 'geo_nodes':
new GeoNodes($('.geo-nodes'));
break;
} }
break; break;
case 'dashboard': case 'dashboard':
......
/* eslint-disable no-new*/
import './smart_interval';
const healthyClass = 'geo-node-icon-healthy';
const unhealthyClass = 'geo-node-icon-unhealthy';
class GeoNodeStatus {
constructor(el) {
this.$el = $(el);
this.$icon = $('.js-geo-node-icon', this.$el);
this.$status = $('.js-geo-node-status', this.$el);
this.$repositoriesSynced = $('.js-repositories-synced', this.$status);
this.$repositoriesFailed = $('.js-repositories-failed', this.$status);
this.$lfsObjectsSynced = $('.js-lfs-objects-synced', this.$status);
this.$health = $('.js-health', this.$status);
this.endpoint = this.$el.data('status-url');
this.statusInterval = new gl.SmartInterval({
callback: this.getStatus.bind(this),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
}
getStatus() {
$.getJSON(this.endpoint, (status) => {
this.setStatusIcon(status.healthy);
this.$repositoriesSynced.html(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`);
this.$repositoriesFailed.html(status.repositories_failed_count);
this.$lfsObjectsSynced.html(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`);
this.$health.html(status.health);
this.$status.show();
});
}
setStatusIcon(healthy) {
if (healthy) {
this.$icon.removeClass(unhealthyClass)
.addClass(healthyClass)
.attr('title', 'Healthy');
} else {
this.$icon.removeClass(healthyClass)
.addClass(unhealthyClass)
.attr('title', 'Unhealthy');
}
this.$icon.tooltip('fixTitle');
}
}
class GeoNodes {
constructor(container) {
this.$container = $(container);
this.pollForSecondaryNodeStatus();
}
pollForSecondaryNodeStatus() {
$('.js-geo-secondary-node', this.$container).each((i, el) => {
new GeoNodeStatus(el);
});
}
}
export default GeoNodes;
class Admin::GeoNodesController < Admin::ApplicationController class Admin::GeoNodesController < Admin::ApplicationController
before_action :check_license, except: [:index, :destroy] before_action :check_license, except: [:index, :destroy]
before_action :load_node, only: [:destroy, :repair, :toggle] before_action :load_node, only: [:destroy, :repair, :toggle, :status]
def index def index
# Ensure all nodes are using their Presenter @nodes = GeoNode.all
@nodes = GeoNode.all.map(&:present)
@node = GeoNode.new @node = GeoNode.new
unless Gitlab::Geo.license_allows? unless Gitlab::Geo.license_allows?
...@@ -57,6 +56,16 @@ class Admin::GeoNodesController < Admin::ApplicationController ...@@ -57,6 +56,16 @@ class Admin::GeoNodesController < Admin::ApplicationController
redirect_to admin_geo_nodes_path redirect_to admin_geo_nodes_path
end end
def status
status = Geo::NodeStatusService.new.call(@node)
respond_to do |format|
format.json do
render json: GeoNodeStatusSerializer.new.represent(status)
end
end
end
private private
def geo_node_params def geo_node_params
......
...@@ -4,15 +4,10 @@ module EE ...@@ -4,15 +4,10 @@ module EE
if node.primary? if node.primary?
icon 'star fw', class: 'has-tooltip', title: 'Primary node' icon 'star fw', class: 'has-tooltip', title: 'Primary node'
else else
status = status = node.enabled? ? 'healthy' : 'disabled'
if node.enabled?
node.healthy? ? 'healthy' : 'unhealthy'
else
'disabled'
end
icon 'globe fw', icon 'globe fw',
class: "geo-node-icon-#{status} has-tooltip", class: "js-geo-node-icon geo-node-icon-#{status} has-tooltip",
title: status.capitalize title: status.capitalize
end end
end end
......
class GeoNodeStatus class GeoNodeStatus
include ActiveModel::Model include ActiveModel::Model
attr_accessor :id
attr_writer :health attr_writer :health
def health def health
...@@ -39,24 +40,24 @@ class GeoNodeStatus ...@@ -39,24 +40,24 @@ class GeoNodeStatus
@repositories_failed_count = value.to_i @repositories_failed_count = value.to_i
end end
def lfs_objects_total def lfs_objects_count
@lfs_objects_total ||= LfsObject.count @lfs_objects_count ||= LfsObject.count
end end
def lfs_objects_total=(value) def lfs_objects_count=(value)
@lfs_objects_total = value.to_i @lfs_objects_count = value.to_i
end end
def lfs_objects_synced def lfs_objects_synced_count
@lfs_objects_synced ||= Geo::FileRegistry.where(file_type: :lfs).count @lfs_objects_synced_count ||= Geo::FileRegistry.where(file_type: :lfs).count
end end
def lfs_objects_synced=(value) def lfs_objects_synced_count=(value)
@lfs_objects_synced = value.to_i @lfs_objects_synced_count = value.to_i
end end
def lfs_objects_synced_in_percentage def lfs_objects_synced_in_percentage
sync_percentage(lfs_objects_total, lfs_objects_synced) sync_percentage(lfs_objects_count, lfs_objects_synced_count)
end end
private private
......
class GeoNodePresenter < Gitlab::View::Presenter::Delegated
presents :geo_node
delegate :healthy?, :health, :repositories_count, :repositories_synced_count,
:repositories_synced_in_percentage, :repositories_failed_count,
:lfs_objects_total, :lfs_objects_synced, :lfs_objects_synced_in_percentage,
to: :status
private
def status
@status ||= Geo::NodeStatusService.new.call(geo_node.status_url)
end
end
class GeoNodeStatusEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
expose :id
expose :healthy?, as: :healthy
expose :health do |node|
node.healthy? ? 'No Health Problems Detected' : node.health
end
expose :lfs_objects_count
expose :lfs_objects_synced_count
expose :lfs_objects_synced_in_percentage do |node|
number_to_percentage(node.lfs_objects_synced_in_percentage, precision: 2)
end
expose :repositories_count
expose :repositories_failed_count
expose :repositories_synced_count
expose :repositories_synced_in_percentage do |node|
number_to_percentage(node.repositories_synced_in_percentage, precision: 2)
end
end
class GeoNodeStatusSerializer < BaseSerializer
entity GeoNodeStatusEntity
end
...@@ -3,12 +3,19 @@ module Geo ...@@ -3,12 +3,19 @@ module Geo
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include HTTParty include HTTParty
KEYS = %w(health repositories_count repositories_synced_count repositories_failed_count lfs_objects_total lfs_objects_synced).freeze KEYS = %w(
health
repositories_count
repositories_synced_count
repositories_failed_count
lfs_objects_count
lfs_objects_synced_count
).freeze
def call(status_url) def call(geo_node)
values = values =
begin begin
response = self.class.get(status_url, headers: headers, timeout: timeout) response = self.class.get(geo_node.status_url, headers: headers, timeout: timeout)
if response.success? if response.success?
response.parsed_response.values_at(*KEYS) response.parsed_response.values_at(*KEYS)
...@@ -19,7 +26,7 @@ module Geo ...@@ -19,7 +26,7 @@ module Geo
[e.message] [e.message]
end end
GeoNodeStatus.new(KEYS.zip(values).to_h) GeoNodeStatus.new(KEYS.zip(values).to_h.merge(id: geo_node.id))
end end
private private
......
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Geo nodes (#{@nodes.count}) Geo nodes (#{@nodes.count})
%ul.well-list %ul.well-list.geo-nodes
- @nodes.each do |node| - @nodes.each do |node|
%li %li{ id: dom_id(node), class: ('js-geo-secondary-node' if node.secondary?), data: { status_url: status_admin_geo_node_path(node) } }
.list-item-name .list-item-name
%span %span
= node_status_icon(node) = node_status_icon(node)
...@@ -24,17 +24,21 @@ ...@@ -24,17 +24,21 @@
- if node.primary? - if node.primary?
%span.help-block Primary node %span.help-block Primary node
- else - else
%p .js-geo-node-status{ style: 'display: none' }
%span.help-block %p
Repositories synced: #{node.repositories_synced_count}/#{node.repositories_count} (#{number_to_percentage(node.repositories_synced_in_percentage, precision: 2)}) %span.help-block
%p Repositories synced:
%span.help-block %span.js-repositories-synced
Repositories failed: #{node.repositories_failed_count} %p
%p %span.help-block
%span.help-block Repositories failed:
LFS objects synced: #{node.lfs_objects_synced}/#{node.lfs_objects_total} (#{number_to_percentage(node.lfs_objects_synced_in_percentage, precision: 2)}) %span.js-repositories-failed
%p %p
%span.help-block= node.healthy? ? 'No Health Problems Detected' : node.health %span.help-block
LFS objects synced:
%span.js-lfs-objects-synced
%p
%span.help-block.js-health
.pull-right .pull-right
- if Gitlab::Geo.license_allows? - if Gitlab::Geo.license_allows?
......
---
title: Get Geo secondaries nodes statuses over AJAX
merge_request:
author:
...@@ -119,6 +119,7 @@ namespace :admin do ...@@ -119,6 +119,7 @@ namespace :admin do
member do member do
post :repair post :repair
post :toggle post :toggle
get :status
end end
end end
## EE-specific ## EE-specific
......
...@@ -782,8 +782,8 @@ module API ...@@ -782,8 +782,8 @@ module API
expose :repositories_count expose :repositories_count
expose :repositories_synced_count expose :repositories_synced_count
expose :repositories_failed_count expose :repositories_failed_count
expose :lfs_objects_total expose :lfs_objects_count
expose :lfs_objects_synced expose :lfs_objects_synced_count
end end
class PersonalAccessToken < Grape::Entity class PersonalAccessToken < Grape::Entity
......
...@@ -195,4 +195,41 @@ describe Admin::GeoNodesController do ...@@ -195,4 +195,41 @@ describe Admin::GeoNodesController do
end end
end end
end end
describe '#status' do
let(:geo_node) { create(:geo_node) }
context 'without add-on license' do
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(false)
get :status, id: geo_node, format: :json
end
it_behaves_like 'unlicensed geo action'
end
context 'with add-on license' do
let(:geo_node_status) do
GeoNodeStatus.new(
id: 1,
health: nil,
lfs_objects_count: 256,
lfs_objects_synced_count: 123,
repositories_count: 10,
repositories_synced_count: 5
)
end
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
allow_any_instance_of(Geo::NodeStatusService).to receive(:call).and_return(geo_node_status)
end
it 'returns the status' do
get :status, id: geo_node, format: :json
expect(response).to match_response_schema('geo_node_status')
end
end
end
end end
{
"type": "object",
"required" : [
"id",
"healthy",
"health",
"lfs_objects_count",
"lfs_objects_synced_count",
"lfs_objects_synced_in_percentage",
"repositories_count",
"repositories_failed_count",
"repositories_synced_count",
"repositories_synced_in_percentage"
],
"properties" : {
"id": { "type": "integer" },
"healthy": { "type": "boolean" },
"health": { "type": "string" },
"lfs_objects_count": { "type": "integer" },
"lfs_objects_synced_count": { "type": "integer" },
"lfs_objects_synced_in_percentage": { "type": "string" },
"repositories_count": { "type": "integer" },
"repositories_failed_count": { "type": "integer" },
"repositories_synced_count": { "type": "integer" },
"repositories_synced_in_percentage": { "type": "string" }
},
"additionalProperties": false
}
require 'spec_helper'
describe Geo::GeoNodeStatus, models: true do
subject { GeoNodeStatus.new }
describe '#lfs_objects_synced_in_percentage' do
it 'returns 0 when no objects are available' do
subject.lfs_objects_total = 0
subject.lfs_objects_synced = 0
expect(subject.lfs_objects_synced_in_percentage).to eq(0)
end
it 'returns the right percentage' do
subject.lfs_objects_total = 4
subject.lfs_objects_synced = 1
expect(subject.lfs_objects_synced_in_percentage).to be_within(0.0001).of(25)
end
end
end
require 'spec_helper'
describe GeoNodeStatus, model: true do
subject { described_class.new }
describe '#healthy?' do
context 'when health is blank' do
it 'returns true' do
subject.health = ''
expect(subject.healthy?).to eq true
end
end
context 'when health is present' do
it 'returns false' do
subject.health = 'something went wrong'
expect(subject.healthy?).to eq false
end
end
end
describe '#health' do
it 'delegates to the HealthCheck' do
subject.health = nil
expect(HealthCheck::Utils).to receive(:process_checks).with(['geo']).once
subject.health
end
end
describe '#lfs_objects_synced_in_percentage' do
it 'returns 0 when no objects are available' do
subject.lfs_objects_count = 0
subject.lfs_objects_synced_count = 0
expect(subject.lfs_objects_synced_in_percentage).to eq(0)
end
it 'returns the right percentage' do
subject.lfs_objects_count = 4
subject.lfs_objects_synced_count = 1
expect(subject.lfs_objects_synced_in_percentage).to be_within(0.0001).of(25)
end
end
describe '#repositories_synced_in_percentage' do
it 'returns 0 when no objects are available' do
subject.repositories_count = 0
subject.repositories_synced_count = 0
expect(subject.repositories_synced_in_percentage).to eq(0)
end
it 'returns the right percentage' do
subject.repositories_count = 4
subject.repositories_synced_count = 1
expect(subject.repositories_synced_in_percentage).to be_within(0.0001).of(25)
end
end
context 'when no values are available' do
it 'returns 0 for each attribute' do
subject.lfs_objects_count = nil
subject.lfs_objects_synced_count = nil
subject.repositories_count = nil
subject.repositories_synced_count = nil
subject.repositories_failed_count = nil
expect(subject.repositories_count).to be_zero
expect(subject.repositories_synced_count).to be_zero
expect(subject.repositories_synced_in_percentage).to be_zero
expect(subject.repositories_failed_count).to be_zero
expect(subject.lfs_objects_count).to be_zero
expect(subject.lfs_objects_synced_count).to be_zero
expect(subject.lfs_objects_synced_in_percentage).to be_zero
end
end
end
require 'spec_helper'
describe GeoNodeStatusEntity do
let(:geo_node_status) do
GeoNodeStatus.new(
id: 1,
health: nil,
lfs_objects_count: 256,
lfs_objects_synced_count: 123,
repositories_count: 10,
repositories_synced_count: 5
)
end
let(:entity) do
described_class.new(geo_node_status, request: double)
end
let(:error) do
'Could not connect to Geo database'
end
subject { entity.as_json }
it { is_expected.to have_key(:id) }
it { is_expected.to have_key(:healthy) }
it { is_expected.to have_key(:health) }
it { is_expected.to have_key(:lfs_objects_count) }
it { is_expected.to have_key(:lfs_objects_synced_count) }
it { is_expected.to have_key(:lfs_objects_synced_in_percentage) }
it { is_expected.to have_key(:repositories_count) }
it { is_expected.to have_key(:repositories_failed_count) }
it { is_expected.to have_key(:repositories_synced_count)}
it { is_expected.to have_key(:repositories_synced_in_percentage) }
describe '#healthy' do
context 'when node is healthy' do
it 'returns true' do
expect(subject[:healthy]).to eq true
end
end
context 'when node is unhealthy' do
before do
geo_node_status.health = error
end
subject { entity.as_json }
it 'returns false' do
expect(subject[:healthy]).to eq false
end
end
end
describe '#health' do
context 'when node is healthy' do
it 'exposes the health message' do
expect(subject[:health]).to eq 'No Health Problems Detected'
end
end
context 'when node is unhealthy' do
before do
geo_node_status.health = error
end
subject { entity.as_json }
it 'exposes the error message' do
expect(subject[:health]).to eq error
end
end
end
describe '#lfs_objects_synced_in_percentage' do
it 'formats as percentage' do
expect(subject[:lfs_objects_synced_in_percentage]).to eq '48.05%'
end
end
describe '#repositories_synced_in_percentage' do
it 'formats as percentage' do
expect(subject[:repositories_synced_in_percentage]).to eq '50.00%'
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment