From dd482d7f2e7ed03bb3c5f8aeca6f81e221b52654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Fri, 10 Feb 2023 13:12:36 +0800 Subject: [PATCH] Add LndHub db/models, and quick stats for admin views --- .../quickstats_container_component.html.erb | 3 ++ .../quickstats_container_component.rb | 4 ++ .../quickstats_item_component.html.erb | 18 +++++++ app/components/quickstats_item_component.rb | 13 +++++ app/controllers/admin/donations_controller.rb | 6 ++- .../admin/invitations_controller.rb | 7 ++- app/controllers/admin/lightning_controller.rb | 12 +++++ app/models/lndhub_account.rb | 21 ++++++++ app/models/lndhub_account_ledger.rb | 3 ++ app/models/lndhub_base.rb | 4 ++ app/models/lndhub_user.rb | 15 ++++++ app/models/user.rb | 5 ++ app/views/admin/dashboard/index.html.erb | 11 +++-- app/views/admin/donations/index.html.erb | 34 ++++++++++--- app/views/admin/invitations/index.html.erb | 26 +++++++--- app/views/admin/lightning/index.html.erb | 48 +++++++++++++++++++ app/views/settings/account/index.html.erb | 8 ++-- app/views/shared/_admin_nav.html.erb | 6 ++- config/database.yml | 41 +++++++++++----- config/routes.rb | 3 +- .../illustrations/undraw_vault_re_s4my.svg | 1 + .../quickstats_container_component_spec.rb | 15 ++++++ .../quickstats_item_component_spec.rb | 15 ++++++ 23 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 app/components/quickstats_container_component.html.erb create mode 100644 app/components/quickstats_container_component.rb create mode 100644 app/components/quickstats_item_component.html.erb create mode 100644 app/components/quickstats_item_component.rb create mode 100644 app/controllers/admin/lightning_controller.rb create mode 100644 app/models/lndhub_account.rb create mode 100644 app/models/lndhub_account_ledger.rb create mode 100644 app/models/lndhub_base.rb create mode 100644 app/models/lndhub_user.rb create mode 100644 app/views/admin/lightning/index.html.erb create mode 100644 public/img/illustrations/undraw_vault_re_s4my.svg create mode 100644 spec/components/quickstats_container_component_spec.rb create mode 100644 spec/components/quickstats_item_component_spec.rb diff --git a/app/components/quickstats_container_component.html.erb b/app/components/quickstats_container_component.html.erb new file mode 100644 index 0000000..466f09c --- /dev/null +++ b/app/components/quickstats_container_component.html.erb @@ -0,0 +1,3 @@ +
+ <%= content %> +
diff --git a/app/components/quickstats_container_component.rb b/app/components/quickstats_container_component.rb new file mode 100644 index 0000000..ac229b7 --- /dev/null +++ b/app/components/quickstats_container_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class QuickstatsContainerComponent < ViewComponent::Base +end diff --git a/app/components/quickstats_item_component.html.erb b/app/components/quickstats_item_component.html.erb new file mode 100644 index 0000000..4d27fb6 --- /dev/null +++ b/app/components/quickstats_item_component.html.erb @@ -0,0 +1,18 @@ +
+
+ <%= @title %> +
+
+ <% if @type == :number %> + <%= number_with_delimiter @value %> + <% else %> + <%= @value %> + <% end %> + <% if @unit %> + <%= @unit %> + <% end %> + <% if @meta %> + <%= @meta %> + <% end %> +
+
diff --git a/app/components/quickstats_item_component.rb b/app/components/quickstats_item_component.rb new file mode 100644 index 0000000..a93b79e --- /dev/null +++ b/app/components/quickstats_item_component.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class QuickstatsItemComponent < ViewComponent::Base + def initialize(type:, title:, value:, unit: nil, meta: nil, icon_name: nil, icon_color_class: nil) + @type = type + @title = title + @value = value + @unit = unit + @meta = meta + @icon_name = icon_name + @icon_color_class = icon_color_class + end +end diff --git a/app/controllers/admin/donations_controller.rb b/app/controllers/admin/donations_controller.rb index e6c37cc..84d98d9 100644 --- a/app/controllers/admin/donations_controller.rb +++ b/app/controllers/admin/donations_controller.rb @@ -5,7 +5,11 @@ class Admin::DonationsController < Admin::BaseController # GET /donations # GET /donations.json def index - @donations = Donation.all + @donations = Donation.all.order('created_at desc') + @stats = { + overall_sats: @donations.all.sum("amount_sats"), + donor_count: Donation.distinct.count(:user_id) + } end # GET /donations/1 diff --git a/app/controllers/admin/invitations_controller.rb b/app/controllers/admin/invitations_controller.rb index 7bfa7be..ca9b407 100644 --- a/app/controllers/admin/invitations_controller.rb +++ b/app/controllers/admin/invitations_controller.rb @@ -1,8 +1,11 @@ class Admin::InvitationsController < Admin::BaseController def index @current_section = :invitations - @invitations_unused_count = Invitation.unused.count - @users_with_referrals_count = Invitation.used.distinct.count(:user_id) @invitations_used = Invitation.used.order('used_at desc') + @stats = { + available: Invitation.unused.count, + accepted: @invitations_used.length, + users_with_referrals: Invitation.used.distinct.count(:user_id) + } end end diff --git a/app/controllers/admin/lightning_controller.rb b/app/controllers/admin/lightning_controller.rb new file mode 100644 index 0000000..bd7ca99 --- /dev/null +++ b/app/controllers/admin/lightning_controller.rb @@ -0,0 +1,12 @@ +class Admin::LightningController < Admin::BaseController + def index + @current_section = :lightning + + @users = User.pluck(:cn, :ou, :ln_account) + @accounts = LndhubAccount.with_balances.order(balance: :desc).to_a + + @ln = {} + @ln[:current_balance] = LndhubAccount.current.joins(:ledgers).sum("account_ledgers.amount") + @ln[:users_with_sats] = @accounts.length + end +end diff --git a/app/models/lndhub_account.rb b/app/models/lndhub_account.rb new file mode 100644 index 0000000..b5eb437 --- /dev/null +++ b/app/models/lndhub_account.rb @@ -0,0 +1,21 @@ +class LndhubAccount < LndhubBase + self.table_name = "accounts" + self.inheritance_column = :_type_disabled + + has_many :ledgers, class_name: "LndhubAccountLedger", + foreign_key: "account_id" + + belongs_to :user, class_name: "LndhubUser", + foreign_key: "user_id" + + scope :current, -> { where(type: "current") } + scope :outgoing, -> { where(type: "outgoing") } + scope :incoming, -> { where(type: "incoming") } + scope :fees, -> { where(type: "fees") } + + scope :with_balances, -> { + current.joins(:user).joins(:ledgers) + .group("accounts.id", "users.login") + .select("accounts.id, users.login, SUM(account_ledgers.amount) AS balance") + } +end diff --git a/app/models/lndhub_account_ledger.rb b/app/models/lndhub_account_ledger.rb new file mode 100644 index 0000000..6858275 --- /dev/null +++ b/app/models/lndhub_account_ledger.rb @@ -0,0 +1,3 @@ +class LndhubAccountLedger < LndhubBase + self.table_name = "account_ledgers" +end diff --git a/app/models/lndhub_base.rb b/app/models/lndhub_base.rb new file mode 100644 index 0000000..d8a75ea --- /dev/null +++ b/app/models/lndhub_base.rb @@ -0,0 +1,4 @@ +class LndhubBase < ActiveRecord::Base + self.abstract_class = true + establish_connection :lndhub +end diff --git a/app/models/lndhub_user.rb b/app/models/lndhub_user.rb new file mode 100644 index 0000000..e5645c5 --- /dev/null +++ b/app/models/lndhub_user.rb @@ -0,0 +1,15 @@ +class LndhubUser < LndhubBase + self.table_name = "users" + self.inheritance_column = :_type_disabled + + has_many :accounts, class_name: "LndhubAccount", + foreign_key: "user_id" + + belongs_to :user, class_name: "User", + primary_key: "ln_account", + foreign_key: "login" + + def balance + accounts.current.first.ledgers.sum("account_ledgers.amount") + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 660d53c..e9237c2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,11 @@ class User < ApplicationRecord has_many :invitations, dependent: :destroy has_many :donations, dependent: :nullify + has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", + primary_key: "ln_account", foreign_key: "login" + + has_many :accounts, through: :lndhub_user + validates_uniqueness_of :cn validates_length_of :cn, :minimum => 3 validates_uniqueness_of :email diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb index bb66283..49d9e30 100644 --- a/app/views/admin/dashboard/index.html.erb +++ b/app/views/admin/dashboard/index.html.erb @@ -1,7 +1,12 @@ <%= render HeaderComponent.new(title: "Admin Panel") %> <%= render MainSimpleComponent.new do %> -

- With great power comes great responsibility. -

+
+

+ <%= image_tag("/img/illustrations/undraw_vault_re_s4my.svg", class: 'h-48') %> +

+

+ With great power comes great responsibility. +

+
<% end %> diff --git a/app/views/admin/donations/index.html.erb b/app/views/admin/donations/index.html.erb index baf51cb..0f57949 100644 --- a/app/views/admin/donations/index.html.erb +++ b/app/views/admin/donations/index.html.erb @@ -1,7 +1,26 @@ <%= render HeaderComponent.new(title: "Donations") %> <%= render MainSimpleComponent.new do %> +
+ <%= render QuickstatsContainerComponent.new do %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Overall', + value: @stats[:overall_sats], + unit: 'sats' + ) %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Donors', + value: @stats[:donor_count], + meta: "/ #{User.count} users" + ) %> + <% end %> +
+ +
<% if @donations.any? %> +

Recent Donations

@@ -11,7 +30,7 @@ - + @@ -23,11 +42,13 @@ - - - - + + <% end %> @@ -37,6 +58,7 @@ No donations yet.

<% end %> +

<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %> diff --git a/app/views/admin/invitations/index.html.erb b/app/views/admin/invitations/index.html.erb index 4bad1fb..1bc5517 100644 --- a/app/views/admin/invitations/index.html.erb +++ b/app/views/admin/invitations/index.html.erb @@ -2,16 +2,28 @@ <%= render MainSimpleComponent.new do %>

-

- There are currently <%= @invitations_unused_count %> - unused invitations available to existing users. - <%= @users_with_referrals_count %> users have successfully - invited new users. -

+ <%= render QuickstatsContainerComponent.new do %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Available', + value: @stats[:available], + ) %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Accepted', + value: @stats[:accepted], + ) %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Users with referrals', + value: @stats[:users_with_referrals], + meta: "/ #{User.count}" + ) %> + <% end %>
<% if @invitations_used.any? %>
-

Accepted (<%= @invitations_used.length %>)

+

Recently Accepted

in USD Public name Date
<% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %> <% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %> <%= donation.public_name %><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d") : "" %><%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %><%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %><%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red', - data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %> + <%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %> + <%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %> + <%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red', + data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %> +
diff --git a/app/views/admin/lightning/index.html.erb b/app/views/admin/lightning/index.html.erb new file mode 100644 index 0000000..902186c --- /dev/null +++ b/app/views/admin/lightning/index.html.erb @@ -0,0 +1,48 @@ +<%= render HeaderComponent.new(title: "Lightning Network") %> + +<%= render MainSimpleComponent.new do %> +
+ <%= render QuickstatsContainerComponent.new do %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Current user balance', + value: @ln[:current_balance], + unit: 'sats' + ) %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Users with sats', + value: @ln[:users_with_sats], + meta: "/ #{User.count}" + ) %> + <% end %> +
+ +
+

Accounts

+
+ + + + + + + + + <% @accounts.each do |account| %> + + + + + + <% end %> + +
LN AccountUserBalance
+ <%= account.login[0, 8] %>...<%= account.login[12, 19] %> + + <% if user = @users.find{ |u| u[2] == account.login } %> + <%= "#{user[0]}@#{user[1]}" %> + <% end %> + <%= number_with_delimiter account.balance.to_i.to_s %>
+
+<% end %> diff --git a/app/views/settings/account/index.html.erb b/app/views/settings/account/index.html.erb index 708a174..279d37d 100644 --- a/app/views/settings/account/index.html.erb +++ b/app/views/settings/account/index.html.erb @@ -4,10 +4,10 @@

Password

Use the following button to request an email with a password reset link:

-

- <%= form_with(url: settings_reset_password_path, method: :post) do %> + <%= form_with(url: settings_reset_password_path, method: :post) do %> +

<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %> - <% end %> -

+

+ <% end %>
<% end %> diff --git a/app/views/shared/_admin_nav.html.erb b/app/views/shared/_admin_nav.html.erb index 881cd93..b7ef06e 100644 --- a/app/views/shared/_admin_nav.html.erb +++ b/app/views/shared/_admin_nav.html.erb @@ -1,8 +1,10 @@ <%= link_to "Dashboard", admin_root_path, class: main_nav_class(@current_section, :dashboard) %> +<%= link_to "Users", admin_ldap_users_path, + class: main_nav_class(@current_section, :ldap_users) %> <%= link_to "Invitations", admin_invitations_path, class: main_nav_class(@current_section, :invitations) %> <%= link_to "Donations", admin_donations_path, class: main_nav_class(@current_section, :donations) %> -<%= link_to "LDAP Users", admin_ldap_users_path, - class: main_nav_class(@current_section, :ldap_users) %> +<%= link_to "Lightning", admin_lightning_path, + class: main_nav_class(@current_section, :lightning) %> diff --git a/config/database.yml b/config/database.yml index 10df6c5..49b5c31 100644 --- a/config/database.yml +++ b/config/database.yml @@ -10,21 +10,40 @@ default: &default timeout: 5000 development: - <<: *default - database: db/development.sqlite3 + primary: + <<: *default + database: db/development.sqlite3 + # lndhub: + # <<: *default + # database: db/lndhub.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - <<: *default - database: db/test.sqlite3 + primary: + <<: *default + database: db/test.sqlite3 + lndhub: + <<: *default + database_tasks: false + database: db/test.lndhub.sqlite3 production: - <<: *default - adapter: postgresql - database: akkounts - port: 5432 - host: <%= Rails.application.credentials.postgres[:host] rescue nil %> - username: <%= Rails.application.credentials.postgres[:username] rescue nil %> - password: <%= Rails.application.credentials.postgres[:password] rescue nil %> + primary: + <<: *default + adapter: postgresql + database: akkounts + port: 5432 + host: <%= Rails.application.credentials.postgres[:host] rescue nil %> + username: <%= Rails.application.credentials.postgres[:username] rescue nil %> + password: <%= Rails.application.credentials.postgres[:password] rescue nil %> + lndhub: + <<: *default + adapter: postgresql + database_tasks: false + database: lndhub + port: 5432 + host: <%= Rails.application.credentials.postgres[:host] rescue nil %> + username: <%= Rails.application.credentials.postgres[:username] rescue nil %> + password: <%= Rails.application.credentials.postgres[:password] rescue nil %> diff --git a/config/routes.rb b/config/routes.rb index 5ed99bd..00475c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -39,9 +39,10 @@ Rails.application.routes.draw do namespace :admin do root to: 'dashboard#index' - get 'invitations', to: 'invitations#index' get 'ldap_users', to: 'ldap_users#index' + get 'invitations', to: 'invitations#index' resources :donations + get 'lightning', to: 'lightning#index' end authenticate :user, ->(user) { user.is_admin? } do diff --git a/public/img/illustrations/undraw_vault_re_s4my.svg b/public/img/illustrations/undraw_vault_re_s4my.svg new file mode 100644 index 0000000..408326c --- /dev/null +++ b/public/img/illustrations/undraw_vault_re_s4my.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spec/components/quickstats_container_component_spec.rb b/spec/components/quickstats_container_component_spec.rb new file mode 100644 index 0000000..9ac0406 --- /dev/null +++ b/spec/components/quickstats_container_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe QuickstatsContainerComponent, type: :component do + pending "add some examples to (or delete) #{__FILE__}" + + # it "renders something useful" do + # expect( + # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html + # ).to include( + # "Hello, components!" + # ) + # end +end diff --git a/spec/components/quickstats_item_component_spec.rb b/spec/components/quickstats_item_component_spec.rb new file mode 100644 index 0000000..c886c37 --- /dev/null +++ b/spec/components/quickstats_item_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe QuickstatsItemComponent, type: :component do + pending "add some examples to (or delete) #{__FILE__}" + + # it "renders something useful" do + # expect( + # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html + # ).to include( + # "Hello, components!" + # ) + # end +end