-
Notifications
You must be signed in to change notification settings - Fork 69
Allow admins to inspect objects on HCB #11856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
8800a26
24add5d
9418b11
8759df5
b0d182a
6283ec5
2b15110
94df279
e238f00
89e77c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
| # get all named classes extending ApplicationRecord | ||
| @all_resource_types = ObjectSpace.each_object(Class).select { |c| c < ApplicationRecord }.select(&:name).map(&:name) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this different than calling Also, maybe we should do ActiveRecord::Base to capture more models such as those created by gems. See which classes it would add. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 | ||
| 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 %> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?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.