5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-16 22:36:21 +00:00

Merge branch 'blazer' into staging

This commit is contained in:
f 2021-08-10 18:20:56 -03:00
commit 063d2625fb
24 changed files with 405 additions and 53 deletions

View file

@ -3,6 +3,7 @@
# Forma de ingreso a Sutty
class ApplicationController < ActionController::Base
include ExceptionHandler
include Pundit
protect_from_forgery with: :null_session, prepend: true
@ -10,6 +11,7 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
around_action :set_locale
rescue_from Pundit::NilPolicyError, with: :page_not_found
rescue_from ActionController::RoutingError, with: :page_not_found
rescue_from ActionController::ParameterMissing, with: :page_not_found
@ -33,7 +35,7 @@ class ApplicationController < ActionController::Base
def find_site
id = params[:site_id] || params[:id]
unless (site = current_usuarie.sites.find_by_name(id))
unless (site = current_usuarie&.sites&.find_by_name(id))
raise SiteNotFound
end
@ -62,6 +64,21 @@ class ApplicationController < ActionController::Base
render 'application/page_not_found', status: :not_found
end
# Necesario para poder acceder a Blazer. Solo les usuaries de este
# sitio pueden acceder al panel.
def require_usuarie
site = find_site
authorize SiteBlazer.new(site)
# Necesario para los breadcrumbs.
ActionView::Base.include Loaf::ViewExtensions unless ActionView::Base.included_modules.include? Loaf::ViewExtensions
breadcrumb current_usuarie.email, main_app.edit_usuarie_registration_path
breadcrumb 'sites.index', main_app.sites_path, match: :exact
breadcrumb site.title, main_app.site_path(site), match: :exact
breadcrumb 'stats.index', root_path, match: :exact
end
protected
def configure_permitted_parameters

View file

@ -0,0 +1,194 @@
# frozen_string_literal: true
# Modificaciones para Blazer
module BlazerDecorator
# No poder obtener información de la base de datos.
module DisableDatabaseInfo
extend ActiveSupport::Concern
included do
def docs; end
def tables; end
def schema; end
end
end
# Deshabilitar edición de consultas y chequeos.
module DisableEdits
extend ActiveSupport::Concern
included do
def create; end
def update; end
def destroy; end
def run; end
def refresh; end
def cancel; end
end
end
# Blazer hace un gran esfuerzo para ejecutar consultas de forma
# asincrónica pero termina enviándolas por JS.
module RunSync
extend ActiveSupport::Concern
included do
alias_method :original_show, :show
include Blazer::BaseHelper
def show
original_show
options = { user: blazer_user, query: @query, run_id: SecureRandom.uuid, async: false }
@data_source = Blazer.data_sources[@query.data_source]
@result = Blazer::RunStatement.new.perform(@data_source, @statement, options)
chart_data
end
private
# Solo mostrar las consultas de le usuarie
def set_queries(_ = nil)
@queries = (@current_usuarie || current_usuarie).blazer_queries
end
# blazer-2.4.2/app/views/blazer/queries/run.html.erb
def chart_type
case @result.chart_type
when /\Aline(2)?\z/
chart_options.merge! min: nil
when /\Abar(2)?\z/
chart_options.merge! library: { tooltips: { intersect: false, axis: 'x' } }
when 'pie'
chart_options
when 'scatter'
chart_options.merge! library: { tooltips: { intersect: false } }, xtitle: @result.columns[0],
ytitle: @result.columns[1]
when nil
else
if @result.column_types.size == 2
chart_options.merge! library: { tooltips: { intersect: false, axis: 'x' } }
else
chart_options.merge! library: { tooltips: { intersect: false } }
end
end
@result.chart_type
end
def chart_data
@chart_data ||=
case chart_type
when 'line'
@result.columns[1..-1].each_with_index.map do |k, i|
{
name: blazer_series_name(k),
data: @result.rows.map do |r|
[r[0], r[i + 1]]
end,
library: series_library[i]
}
end
when 'line2'
@result.rows.group_by do |r|
v = r[1]
(@result.boom[@result.columns[1]] || {})[v.to_s] || v
end.each_with_index.map do |(name, v), i|
{
name: blazer_series_name(name),
data: v.map do |v2|
[v2[0], v2[2]]
end,
library: series_library[i]
}
end
when 'pie'
@result.rows.map do |r|
[(@result.boom[@result.columns[0]] || {})[r[0].to_s] || r[0], r[1]]
end
when 'bar'
(@result.rows.first.size - 1).times.map do |i|
name = @result.columns[i + 1]
{
name: blazer_series_name(name),
data: @result.rows.first(20).map do |r|
[(@result.boom[@result.columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]]
end
}
end
when 'bar2'
first_20 = @result.rows.group_by { |r| r[0] }.values.first(20).flatten(1)
labels = first_20.map { |r| r[0] }.uniq
series = first_20.map { |r| r[1] }.uniq
labels.each do |l|
series.each do |s|
first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s }
end
end
first_20.group_by do |r|
v = r[1]
(@result.boom[@result.columns[1]] || {})[v.to_s] || v
end.each_with_index.map do |(name, v), _i|
{
name: blazer_series_name(name),
data: v.sort_by do |r2|
labels.index(r2[0])
end.map do |v2|
v3 = v2[0]
[(@result.boom[@result.columns[0]] || {})[v3.to_s] || v3, v2[2]]
end
}
end
when 'scatter'
@result.rows
end
end
def target_index
@target_index ||= @result.columns.index do |k|
k.downcase == 'target'
end
end
def series_library
@series_library ||= {}.tap do |sl|
if target_index
color = '#109618'
sl[target_index - 1] = {
pointStyle: 'line',
hitRadius: 5,
borderColor: color,
pointBackgroundColor: color,
backgroundColor: color,
pointHoverBackgroundColor: color
}
end
end
end
def chart_options
@chart_options ||= { id: SecureRandom.hex }
end
end
end
end
classes = [Blazer::QueriesController, Blazer::ChecksController, Blazer::DashboardsController]
modules = [BlazerDecorator::DisableDatabaseInfo, BlazerDecorator::DisableEdits]
classes.each do |klass|
modules.each do |modul|
klass.include modul unless klass.included_modules.include? modul
end
end
Blazer::QueriesController.include BlazerDecorator::RunSync

View file

@ -2,9 +2,6 @@
# Controlador para artículos
class PostsController < ApplicationController
include Pundit
rescue_from Pundit::NilPolicyError, with: :page_not_found
before_action :authenticate_usuarie!
# TODO: Traer los comunes desde ApplicationController

View file

@ -6,8 +6,6 @@ class PrivateController < ApplicationController
# XXX: Permite ejecutar JS
skip_forgery_protection
include Pundit
# Enviar el archivo si existe, agregar una / al final siempre para no
# romper las direcciones relativas.
def show

View file

@ -2,9 +2,6 @@
# Controlador de sitios
class SitesController < ApplicationController
include Pundit
rescue_from Pundit::NilPolicyError, with: :page_not_found
before_action :authenticate_usuarie!
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path

View file

@ -1,18 +0,0 @@
# frozen_string_literal: true
# Estadísticas del sitio
class StatsController < ApplicationController
include Pundit
before_action :authenticate_usuarie!
def index
@site = find_site
authorize SiteStat.new(@site)
# Solo queremos el promedio de tiempo de compilación, no de
# instalación de dependencias.
stats = @site.build_stats.jekyll
@build_avg = stats.average(:seconds).to_f.round(2)
@build_max = stats.maximum(:seconds).to_f.round(2)
end
end

View file

@ -1,3 +1,3 @@
# frozen_string_literal: true
SiteStat = Struct.new(:site)
SiteBlazer = Struct.new(:site)

View file

@ -11,6 +11,8 @@ class Usuarie < ApplicationRecord
has_many :roles
has_many :sites, through: :roles
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
has_many :blazer_queries, foreign_key: 'creator_id', class_name: 'Blazer::Query'
def name
email.split('@', 2).first

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Les invitades no pueden ver las estadísticas (aun)
SiteBlazerPolicy = Struct.new(:usuarie, :site_blazer) do
def home?
site_blazer&.site&.usuarie? usuarie
end
alias_method :show?, :home?
end

View file

@ -1,15 +0,0 @@
# frozen_string_literal: true
# Política de acceso a las estadísticas
class SiteStatPolicy
attr_reader :site_stat, :usuarie
def initialize(usuarie, site_stat)
@usuarie = usuarie
@site_stat = site_stat
end
def index?
site_stat.site.usuarie? usuarie
end
end

View file

@ -0,0 +1,5 @@
%ul
- @checks.each do |check|
%li
= check.query.name
= check.state

View file

@ -0,0 +1,30 @@
!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%body{:style => "font-family: 'Helvetica Neue', Arial, Helvetica; font-size: 14px; color: #333;"}
- if @error
%p= @error
- elsif @rows_count > 0 && @check_type == "bad_data"
%p
- if @rows_count <= 10
= pluralize(@rows_count, "row")
- else
Showing 10 of #{@rows_count} rows
%table{:style => "width: 100%; border-spacing: 0; border-collapse: collapse;"}
%thead
%tr
- @columns.first(5).each do |column|
%th{:style => "padding: 8px; line-height: 1.4; text-align: left; vertical-align: bottom; border-bottom: 2px solid #ddd; width: #{(100 / @columns.size).round(2)}%;"}
= column
%tbody
- @rows.first(10).each do |row|
%tr
- @columns.first(5).each_with_index do |column, i|
%td{:style => "padding: 8px; line-height: 1.4; vertical-align: top; border-top: 1px solid #ddd;"}
- value = row[i]
- if @column_types[i] == "time" && value.to_s.length > 10
- value = Time.parse(value).in_time_zone(Blazer.time_zone) rescue value
= value
- if @columns.size > 5
%p{:style => "color: #999;"} Only first 5 columns shown

View file

@ -0,0 +1,9 @@
#queries
%table.table
%tbody.list
- @queries.each do |query|
%tr
-#
Por alguna razón no tenemos acceso a query_path para poder
generar la URL según Rails
%td= link_to query[:name], "/sites/#{params[:site_id]}/stats/queries/#{query.to_param}"

View file

@ -0,0 +1,51 @@
- blazer_title @query.name
.container
.row
.col-12
%h1= @query.name
- if @query.description.present?
%p.lead= @query.description
- unless @result.chart_type.blank?
.col-12
- case @result.chart_type
- when 'line'
= line_chart @chart_data, **@chart_options
- when 'line2'
= line_chart @chart_data, **@chart_options
- when 'pie'
= pie_chart @chart_data, **@chart_options
- when 'bar'
= column_chart @chart_data, **@chart_options
- when 'bar2'
= column_chart @chart_data, **@chart_options
- when 'scatter'
= scatter_chart @chart_data, **@chart_options
.col-12
%table.table
%thead
%tr
- @result.columns.each do |key|
- next if key.include? 'ciphertext'
- next if key.include? 'encrypted'
%th.position-sticky.background-white{ style: 'top: 0' }= t("blazer.columns.#{key}", default: key.titleize)
%tbody
- @result.rows.each do |row|
%tr
- row.each_with_index do |v, i|
- k = @result.columns[i]
- next if k.include? 'ciphertext'
- next if k.include? 'encrypted'
%td
- if v.is_a?(Time)
- v = blazer_time_value(@data_source, k, v)
- unless v.nil?
- if v.is_a?(String) && v.empty?
%span.text-muted= t('.empty')
- elsif @data_source.linked_columns[k]
= link_to blazer_format_value(k, v), @data_source.linked_columns[k].gsub('{value}', u(v.to_s)), target: '_blank'
- else
= blazer_format_value(k, v)
- if (v2 = (@result.boom[k] || {})[v.nil? ? v : v.to_s])
%span.text-muted= v2

View file

@ -12,7 +12,7 @@
- else
%span.line-clamp-1= link_to crumb.name, crumb.url
- if current_usuarie
- if @current_usuarie || current_usuarie
%ul.navbar-nav
- if @site&.tienda?
%li.nav-item
@ -20,5 +20,5 @@
role: 'button', class: 'btn'
%li.nav-item
= link_to t('.logout'), destroy_usuarie_session_path,
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
method: :delete, role: 'button', class: 'btn'

View file

@ -0,0 +1,14 @@
!!!
%html
%head
%meta{content: 'text/html; charset=UTF-8', 'http-equiv': 'Content-Type'}/
%title= blazer_title ? blazer_title : 'Sutty'
%meta{charset: 'utf-8'}/
= favicon_link_tag 'blazer/favicon.png'
= stylesheet_link_tag 'application'
= javascript_pack_tag 'blazer', 'data-turbolinks-track': 'reload'
= csrf_meta_tags
%body{ class: yield(:body) }
.container-fluid#sutty
= render 'layouts/breadcrumb'
= yield

View file

@ -38,6 +38,13 @@ module Sutty
config.active_storage.variant_processor = :vips
config.to_prepare do
# Load application's model / class decorators
Dir.glob(File.join(File.dirname(__FILE__), '../app/**/*_decorator.rb')) do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
config.after_initialize do
ActiveStorage::DirectUploadsController.include ActiveStorage::AuthenticatedDirectUploadsController

View file

@ -50,7 +50,7 @@ user_method: current_usuarie
user_name: email
# custom before_action to use for auth
# before_action_method: require_admin
before_action_method: require_usuarie
# email to send checks from
from_email: blazer@<%= ENV.fetch('SUTTY', 'sutty.nl') %>

View file

@ -576,3 +576,14 @@ en:
edit: 'Editing'
usuaries:
index: 'Users'
stats:
index: 'Statistics'
blazer:
columns:
total: 'Total'
dia: 'Date'
date: 'Date'
visitas: 'Visits'
queries:
show:
empty: '(empty)'

View file

@ -584,3 +584,14 @@ es:
edit: 'Editando'
usuaries:
index: 'Usuaries'
stats:
index: 'Estadísticas'
blazer:
columns:
total: 'Total'
dia: 'Fecha'
date: 'Fecha'
visitas: 'Visitas'
queries:
show:
empty: '(vacío)'

View file

@ -4,8 +4,6 @@ Rails.application.routes.draw do
devise_for :usuaries
get '/.well-known/change-password', to: redirect('/usuaries/edit')
mount Blazer::Engine, at: 'blazer'
root 'application#index'
constraints(Constraints::ApiSubdomain.new) do
@ -38,6 +36,9 @@ Rails.application.routes.draw do
match '/api/v3/projects/:site_id/notices' => 'api/v1/notices#create', via: %i[post]
resources :sites, constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do
# Usar Blazer para mostrar estadísticas
mount Blazer::Engine, at: 'stats', as: 'stats'
# Gestionar actualizaciones del sitio
get 'pull', to: 'sites#fetch'
post 'pull', to: 'sites#merge'
@ -73,7 +74,5 @@ Rails.application.routes.draw do
# Compilar el sitio
post 'enqueue', to: 'sites#enqueue'
post 'reorder_posts', to: 'sites#reorder_posts'
resources :stats, only: [:index]
end
end

View file

@ -3,8 +3,6 @@
# Blazer
class InstallBlazer < ActiveRecord::Migration[6.0]
def change
return unless Rails.env.production?
create_table :blazer_queries do |t|
t.references :creator
t.string :name

View file

@ -14,6 +14,8 @@
"@rails/webpacker": "5.2.1",
"@suttyweb/editor": "0.0.7",
"babel-loader": "^8.2.2",
"chart.js": "2.9.3",
"chartkick": "3.2.1",
"circular-dependency-plugin": "^5.2.2",
"commonmark": "^0.29.0",
"fork-awesome": "^1.1.7",

View file

@ -2124,6 +2124,34 @@ chalk@^4.1.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
chartkick@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-3.2.1.tgz#a80c2005ae353c5ae011d0a756b6f592fc8fc7a9"
integrity sha512-zV0kUeZNqrX28AmPt10QEDXHKadbVFOTAFkCMyJifHzGFkKzGCDXxVR8orZ0fC1HbePzRn5w6kLCOVxDQbMUCg==
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -2243,7 +2271,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0, color-convert@^1.9.1:
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -5010,6 +5038,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.10.2:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"