-
Notifications
You must be signed in to change notification settings - Fork 84
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 all commits
8800a26
24add5d
9418b11
8759df5
b0d182a
6283ec5
2b15110
94df279
e238f00
89e77c9
93e0af3
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 |
|---|---|---|
|
|
@@ -1420,6 +1420,36 @@ def referral_program_create | |
| def active_teenagers_leaderboard | ||
| end | ||
|
|
||
| def inspect | ||
| redirect_to inspect_resource_admin_index_path(resource: params[:resource_type], id: params[:resource_id]) | ||
| end | ||
|
|
||
| def inspect_resource | ||
| @resource_type = params[:resource] | ||
| @resource_id = params[:id] | ||
|
|
||
| Zeitwerk::Loader.eager_load_all | ||
|
Member
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. 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.
Contributor
Author
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 had considered making a concern, say
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) | ||
|
Member
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.
Contributor
Author
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,57 @@ | ||
| # 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 | ||
|
Member
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.
Contributor
Author
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.
I believe this overrides a Ruby method
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.
ah, good catch. maybe I'll restructure these to be one route. there was previously a need for them to be separate but I can combine them now since it's all being done in turbo.