diff --git a/app/controllers/admin/clients_controller.rb b/app/controllers/admin/clients_controller.rb index b3ed6bf..d954390 100644 --- a/app/controllers/admin/clients_controller.rb +++ b/app/controllers/admin/clients_controller.rb @@ -1,106 +1,120 @@ class Admin::ClientsController < Admin::BaseController before_action :fetch_client, only: [:show, :jobs, :logs, :stats, :configuration, :disable, :revoke, :block, :unblock] before_action :fetch_logs, only: [:logs] # Shows all available clients # # GET /admin/clients def index - @clients = Client.includes(:jobs).all + @clients = Client.joins(:host).includes(:jobs).distinct.all @client_ids = @clients.map(&:id) fetch_jobs_info end + # Shows all clients that are not in Archiving but are still persisted to bacula + # + # GET /admin/clients/obsolete + def obsolete + in_archiving = Client.joins(:host).distinct.pluck(:ClientId) + @clients = Client.includes(:jobs).where.not(ClientId: in_archiving). + distinct.all + @client_ids = @clients.map(&:id) + fetch_jobs_info + @obsolete = true + + render :index + end + # Shows a specific client # # GET /admin/clients/1 def show if !@client.host.present? flash[:alert] = 'Client not configured through Archiving' return redirect_to admin_clients_path end get_charts end # GET /admin/clients/1/jobs def jobs @jobs = @client.recent_jobs.page(params[:page]) end # GET /admin/clients/1/logs def logs end # GET /admin/clients/1/stats # POST /admin/clients/1/stats def stats get_charts end # GET /admin/clients/1/configuration def configuration end # POST /admin/clients/1/disable def disable if @client.host.disable_jobs_and_update flash[:success] = 'Client disabled' else flash[:error] = 'Something went wrong, try again later' end redirect_to admin_client_path(@client) end # POST /admin/clients/1/block def block if @client.host.disable_jobs_and_lock flash[:success] = 'Client is disabled and locked' else flash[:error] = 'Something went wrong, try again' end redirect_to admin_client_path(@client) end # POST /admin/clients/1/unblock def unblock if @client.host.unblock flash[:success] = 'Client can now be configured by users' else flash[:error] = 'Client is still locked' end redirect_to admin_client_path(@client) end # DELETE /admin/clients/1/revoke def revoke if @client.host.remove_from_bacula(true) flash[:success] = 'Client removed. It will be visible to until its jobs get cleaned up' else flash[:error] = 'Something went wrong, try again later' end redirect_to admin_clients_path end private # Fetches the client based on the given id def fetch_client @client = Client.find(params[:id]) @client_ids = [@client.id] end def fetch_jobs_info @stats = JobStats.new end def get_charts @job_status = ChartGenerator.job_statuses(@client_ids, days_ago) @job_stats = ChartGenerator.job_stats(@client_ids, days_ago - 1) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 963b0c4..0ac92a7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,206 +1,206 @@ module ApplicationHelper # Custom helper for better display of big numbers # @example number_by_magnitude(4242) # "4.2K" # # @param number[Numeric] # @return [String] human friendly respresentation def number_by_magnitude(number) number_to_human(number, units: { thousand: :K, million: :M, billion: :G }) end # Creates a bootstrap form-group div with an additional 'Add' button next to the select field # # @param object[ActiveRecord::Object] the form's subject # @param resource[Symbol] the objects class # @param attr[Symbol] the select box's attribute # @param attr_name[String] the attribute's display name # @param options[Array] the select box options # @param path[String] the add button's path def select_with_errors_and_button(object, resource, attr, attr_name, options, path) has_errors = object.errors[attr].present? content_tag(:div, class: "form-group #{' has-error' if has_errors }") do attr_label = label(resource, attr, attr_name, class: 'control-label col-xs-5 required') select_div = content_tag(:div, class: 'col-xs-5') do select_part = select_tag([resource, attr].join('_').to_sym, options, include_blank: true, name: "#{resource}[#{attr}]", class: 'form-control' ) if has_errors select_part.concat(content_tag(:span, class: 'help-block') { object.errors[attr].first }) end select_part end button_part = content_tag(:div, class: 'col-xs-1') do link_to path do content_tag(:span, class: 'glyphicon glyphicon-plus text-success') {} end end attr_label.concat(select_div).concat(button_part) end end # Returns a style class depending on the given parameter # # @param status[Char] def success_class(status) case status when 'T' then 'success' when 'E' then 'danger' when 'f' then 'fatal' end end # Fetches the html class for a given path # # @param path[String] the path to check for # @param partial[Boolean] forces a left partial match # # @return [Hash] { class: 'active' } if the given path is the current page - def active_class(path, partial = false) - if current_page?(path) || (partial && request.path.starts_with?(path)) + def active_class(paths, partial = false) + if [paths].flatten.any? { |path| current_page?(path) || (partial && request.path.starts_with?(path)) } { class: 'active' } else {} end end # Constructs a breadcrumb out the given options # # @param options[Hash] a hash containing the breadcrumb links in name: path sets # @return an html ol breadcrumb def breadcrumb_with(options) content_tag(:ol, class: 'breadcrumb') do options.map { |name, path| content_tag(:li, active_class(path)) do link_to name, path end }.inject { |result, element| result.concat(element) } end end # Constructs a tabbed navigation menu out of the given options # # @param options[Hash] a hash containing the menu links in name: path sets # @return an html ul menu def tabs_with(options) content_tag(:ul, class: 'nav nav-tabs', role: 'tablist') do options.map { |title, path| content_tag(:li, active_class(path).merge({ role: 'presentation' })) do link_to title, path, 'role' => 'tab', 'aria-controls' => title.downcase end }.inject { |result, element| result.concat(element) } end end # Constructs a list with the given array elements # # @example: # inline_list([:foo, :bar]) # # # # @param arr[Array] # @return an html ul list def inline_list(arr) content_tag(:ul, class: 'list-inline') do arr.map { |element| content_tag(:li) do content_tag(:span, class: 'label label-default') do element end end }.inject { |result, element| result.concat(element) } end end # Generates a span with a yes or no and the corresponding formatting # according to the value's falseness # # @param value[Integer] def yes_no(value) klass = value == 1 ? 'label label-success' : 'label label-danger' text = value == 1 ? 'yes' : 'no' content_tag(:span, class: klass) { text } end # Generates a percentage and adds some color coding for better visibility # # @param ratio [Numeric] the ratio # @param quota [Integer] the client's space quota # # @return [String] an html label tag def pretty_percentage(ratio, quota) color = ([[ratio, 0.2].max, 0.99].min * 256).to_i.to_s(16) << '0000' content_tag(:label, class: 'label', style: "background-color:##{color}") do number_to_percentage(100 * ratio, precision: 1) end end # Generates a button that may be disabled # # @param disabled[Boolean] # @param display_text[String] # @param url[String] # @param opts[Hash] def button_or_disabled_with_label(disabled, display_text, url, opts = {}) icon_class = opts.delete(:icon_class) text_class = opts.delete(:text_class) if disabled url = '#' opts.merge!(disabled: true) opts.reverse_merge!(title: 'Client is blocked') opts.delete(:method) else opts.delete(:title) end link_to url, opts do [ content_tag(:label, class: [icon_class, text_class].join(' ')) { }, display_text ].join(' ').html_safe end end # Generates a span that contains a text and a questionmark label. # hovering on that questionmark will display a helper text # # @param text[String] the displayed text # @param tooltip[String] the displayed helper text def tooltip_label(text, tooltip) content_tag(:span, class: "data-toggle", title: tooltip) do [ text, content_tag(:label, class: "glyphicon glyphicon-question-sign") { } ].join(' ').html_safe end end # Generate a div that contains a helper text that is properly aligned with a form # # @param text[String] the displayed text # @param label_class[String] the form's labe_col # @param control_class[String] the form's control_col def help_block(text, label_class, control_class) content_tag(:div, class: 'form-group') do [ content_tag(:label, class: "#{label_class} control-label") { }, content_tag(:div, class: control_class) do content_tag(:p, class: 'form-control-static help-block') do text end end ].join(' ').html_safe end end end diff --git a/app/views/admin/clients/index.html.erb b/app/views/admin/clients/index.html.erb index bcfe5d5..df709bc 100644 --- a/app/views/admin/clients/index.html.erb +++ b/app/views/admin/clients/index.html.erb @@ -1,29 +1,36 @@ -

Bacula Clients

+<% if @obsolete %> +

+ Obsolete Clients + +

+<% else %> +

Bacula Clients

+<% end %>
<%= render partial: 'client', collection: @clients %>
ClientId Name Uname Client Type Active Jobs Last Backup File Ret. (days) Job Ret. (days) Space Used Quota Space Used % Files AutoPrune
diff --git a/app/views/shared/_admin.html.erb b/app/views/shared/_admin.html.erb index 51d9e2e..367d2c1 100644 --- a/app/views/shared/_admin.html.erb +++ b/app/views/shared/_admin.html.erb @@ -1,55 +1,61 @@ diff --git a/config/routes.rb b/config/routes.rb index 5babe46..82f3dbb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,132 +1,136 @@ Rails.application.routes.draw do root 'application#index' get 'faq' => 'application#faq' post 'grnet' => 'application#grnet' get 'institutional' => 'application#institutional' match 'vima', to: 'application#vima', :via => [:get, :post] get 'logout' => 'application#logout' resources :clients, only: [:index, :show] do member do get :jobs get :logs get :stats post :stats get :users get :restore post :run_restore post :restore_selected delete :remove_user end collection do post :index end end resources :clients, only: [], param: :client_id do member do get :tree end end resources :invitations, only: [:create] get '/invitations/:host_id/:verification_code/accept' => 'invitations#accept', as: :accept_invitation resources :hosts, only: [:new, :create, :show, :edit, :update, :destroy] do member do post :submit_config post :disable post :regenerate_token delete :revoke get :fd_config end collection do get :fetch_vima_hosts, to: 'hosts#fetch_vima_hosts', as: :fetch_vima end resources :simple_configs, only: [:new, :create] resources :jobs, only: [:new, :create, :show, :edit, :update, :destroy] do member do patch :toggle_enable post :backup_now end end resources :filesets, only: [:show, :new, :create, :edit, :update, :destroy] resources :schedules, only: [:show, :new, :edit, :create, :update, :destroy] end resources :users, only: :show do member do patch :generate_token end end namespace :admin do match '/', to: 'base#index', via: [:get, :post] get '/login' => 'base#login', as: :login resources :settings, only: [:index, :new, :create, :edit, :update] do member do delete :reset end end resources :clients, only: [:index, :show] do member do get :jobs get :logs get :stats post :stats get :configuration post :disable post :block post :unblock delete :revoke end + + collection do + get :obsolete + end end resources :hosts, only: [] do collection do get :unverified get :rejected end member do post :verify post :reject put :set_quota end end resources :users, only: [:index, :new, :create, :show, :edit, :update] do member do patch :ban patch :unban patch :revoke_admin patch :grant_admin end end resources :pools, only: [:index, :new, :create] resources :faqs end namespace :api, defaults: { format: :json } do scope module: :v1, constraints: ApiVersion.new(version: 1, default: true) do resources :clients, only: [:index, :show] do member do post :backup post :restore end end end end end