Skip to content
11 changes: 11 additions & 0 deletions app/assets/javascripts/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ $(document).on('turbo:load', function () {
BK.s('comment')[0].scrollIntoView()
})

$('[data-behavior~=expand_inspector]').on('click', e => {
const inspector = $('#inspector-container')
if (inspector.parent().hasClass('inspector--expanded')) {
inspector.parent().removeClass('inspector--expanded')
inspector.slideUp()
} else {
inspector.parent().addClass('inspector--expanded')
inspector.slideDown()
}
})

$('.input-group').on('click', e => {
// focus on the input when clicking on the input-group
e.currentTarget.querySelector('input').focus()
Expand Down
30 changes: 30 additions & 0 deletions app/controllers/admin_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,36 @@ def referral_program_create
def active_teenagers_leaderboard
end

def inspect_resource
if params[:resource].blank? && params[:type].blank? && params[:resource_type].present? && params[:resource_id].present?
return redirect_to inspect_resource_admin_index_path(resource: params[:resource_type], id: params[:resource_id])
end

@resource_type = params[:resource]
@resource_id = params[:id]

Zeitwerk::Loader.eager_load_all
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are likely some columns that we shouldn't even allow admins inspect. I'm specifically thinking of encrypted columns such as session tokens, plaid tokens, etc.

At the moment, encrypted columns are not available to admins via Blazer bc blazer doesn't decrypt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had considered making a concern, say inspectable, that could help us standardize configuration keep track of which columns should be accessible on the model itself. Does that sound like a good path forward?

At the moment, encrypted columns are not available to admins via Blazer bc blazer doesn't decrypt.

Do we want to hide encrypted columns in the inspection toolbar? I can see a case for either being made; on one hand, it allows for more destructive powers as an admin, but on the other hand, it reduces our reliance on the production Rails console.


# get all named classes extending ApplicationRecord
@all_resource_types = ObjectSpace.each_object(Class).select { |c| c < ApplicationRecord }.select(&:name).map(&:name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this different than calling descendants on ApplicationRecord?

Also, maybe we should do ActiveRecord::Base to capture more models such as those created by gems. See which classes it would add.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into this. I think I had that approach initially and it caused some issues, but I don't recall exactly why I switched away from this. I'll get back to you.

@associations = {}

begin
@resource = Inspector.find_object(@resource_type, @resource_id)

if @resource.nil?
flash.now[:error] = "Resource not found." and return
end

rescue NameError
flash.now[:error] = "Invalid resource type."
rescue ActiveRecord::RecordNotFound => e
flash.now[:error] = "Resource not found."
ensure
@associations = Inspector.find_relations(@resource) if @resource.present?
end
end

private

def stream_data(content_type, filename, data, download = true)
Expand Down
56 changes: 56 additions & 0 deletions app/models/inspector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Inspector
def self.resource_types
Zeitwerk::Loader.eager_load_all

descendants = ObjectSpace.each_object(Class).select { |c| c < ApplicationRecord }

descendants.filter_map(&:name)
end

def self.find_relations(object)
if object.class < ApplicationRecord
object = object.attributes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe AR has a method something along the lines of reflect_on_associations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does—this was a quick and dirty way of adding a shortcut for [anything]_id, but it would be nice to work with other association types too.

end

resource_type_keys = resource_types.index_by { |type| "#{type.underscore}_id" }

association_keys = object.keys.select do |key|
key.ends_with?("_id") && object[key].present? && resource_type_keys[key].present?
end

association_keys.map do |key|
[resource_type_keys[key], object[key]]
end.to_h
end

def self.find_object(resource, id)
return nil unless resource.in?(resource_types)
klass = resource.constantize

object = klass.find_by(id: id)
object ||= klass.try(:find_by_public_id, id)
object ||= klass.try(:find_by_hashid, id)
object ||= klass.find_by(hcb_code: id) if "hcb_code".in? klass.columns.collect(&:name)
object ||= klass.try(:friendly)&.find(id, allow_nil: true)
object ||= klass.try(:find_by_public_id, id)
object ||= klass.try(:search_name, id)
object ||= klass.try(:search_memo, id)
object ||= klass.try(:search_recipient, id)
object ||= klass.try(:search_description, id)

object = object.first if object.class < Enumerable

object
end

def self.object_for(path)
route = Rails.application.routes.recognize_path(path)
model_name = route[:controller].singularize.classify

if model_name.in?(resource_types)
find_object(model_name, route["#{model_name.underscore}_id".to_sym] || route[:id])
end
end
end
21 changes: 21 additions & 0 deletions app/views/admin/inspect_resource.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<%= turbo_frame_tag "inspect_frame" do %>

<%= form_with url: inspect_admin_index_path, method: :get, local: true, data: { turbo_frame: "inspect_frame" }, class: "flex flex-row items-center gap-2 mb-3 border border-smoke border-b-1 rounded-xl" do |form| %>
<%= form.select :resource_type, options_for_select(@all_resource_types, @resource_type), {}, { class: "mb-0 !border-none !bg-transparent" } %>
<div class="flex-shrink-0 h-[30px] w-[1px] border-left border-smoke border-b-1"></div>
<%= form.text_field :resource_id, value: @resource_id, class: "mb-0 !border-none !bg-transparent flex-grow", style: "max-width: unset!important" %>
<div>
<%= form.submit "Inspect", class: "!bg-steel hover:!bg-slate !text-sm p-1 mr-0.5 !rounded-[9px] !transform-none", style: "background-image: none!important; box-shadow: none!important; transition: all 100ms!important;" %>
</div>
<% end %>

<div class="[&_pre]:m-0 [&_pre]:rounded-xl">
<%== ap @resource %>
</div>

<div class="flex flex-row gap-2">
<% @associations.map do |class_name, id| %>
<%= link_to "Inspect #{class_name} ##{id} →", inspect_resource_admin_index_path(resource: class_name, id: id), data: { turbo_frame: "inspect_frame" }, class: "btn !bg-steel hover:!bg-slate !text-sm p-2 mr-0.5 !rounded-[9px] !transform-none mt-3", style: "background-image: none!important; box-shadow: none!important; transition: all 100ms!important;" %>
<% end %>
</div>
<% end %>
21 changes: 21 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,26 @@
}
</style>
<% end %>

<% if current_user.auditor? %>
<div class="bg-black text-white fixed bottom-0 left-0 right-0 z-50 [&.inspector--expanded_.up-caret]:hidden [&:not(.inspector--expanded)_.down-caret]:hidden">
<div class="bg-black text-white p-2 flex items-center justify-between cursor-pointer hover:bg-steel hover:pb-3 transition-all transition-duration-100 border-bottom border-smoke border-b-1" data-behavior="expand_inspector">
<div class="flex flex-row items-center gap-4">
<%= inline_icon "explore", size: 20 %>
<p class="m-0 text-sm text-center">Inspector</p>
<% object = Inspector.object_for(request.path) %>
<% if object.present? %>
<p class="m-0 text-xs text-center opacity-50"><%= object.class.name %>#<%= object.id %> • Found on page</p>
<% end %>
</div>
<%= inline_icon "up-caret", size: 20, class: "up-caret" %>
<%= inline_icon "down-caret", size: 20, class: "down-caret" %>
</div>
<div class="h-[calc(100vh-200px)] p-4 overflow-y-scroll" id="inspector-container" style="display: none;">
<% path_arguments = object.present? ? { resource: object.class.name, id: object.id } : { resource: "User", id: current_user.id } %>
<%= turbo_frame_tag "inspect_frame", src: inspect_resource_admin_index_path(**path_arguments) %>
</div>
</div>
<% end %>
</body>
</html>
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@

resources :admin, only: [] do
collection do
get "inspect/:resource/:id", to: "admin#inspect_resource", as: :inspect_resource
get "inspect", to: "admin#inspect_resource", as: :inspect
get "bank_accounts", to: "admin#bank_accounts"
get "hcb_codes", to: "admin#hcb_codes"
get "bank_fees", to: "admin#bank_fees"
Expand Down
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ module.exports = {
},
colors: {
slate: '#3c4858',
steel: '#273444',
smoke: '#e0e6ed',
muted: '#8492a6',
snow: '#f9fafc',
Expand Down
Loading