Skip to content

Instantly share code, notes, and snippets.

@sharkey11
Last active September 23, 2024 23:06
Show Gist options
  • Save sharkey11/86972bb14ac2d4ec1c67d4a8f5962cbb to your computer and use it in GitHub Desktop.
Save sharkey11/86972bb14ac2d4ec1c67d4a8f5962cbb to your computer and use it in GitHub Desktop.
This is context for an LLM to make a rails admin dashboard

Building an admin dashboard

Our ruby on rails app contains an admin dashboard that lives on /admin. The admin dashboard allows users to interact with our database or features. Often times, when a feature is built, an admin dashboard will be made. The admin dashboard will often allow a user to read the data and/or write data back to the database.

The practice of building an admin dashboard is very repeatable. I will list out the example steps for a process for a feature known as the wheel.

Procedure

Your job is to understand this pattern and build an admin dashboard pull request. I will provide some inputs such as:

  • Database table name:
  • Tab category:
  • Required user roles: admin | support | manager (you can choose multiple)
  • List all mutable fields:
  • Data can be deleted: yes | no
  • Data can be created: yes | no

You will use these inputs to build the pull request. I will provde an example pull request below that built an admin dashboard. In each step, I will tell you which input parameters to use instead.

Step 1:

Edit the config/routes.rb file to include the defintion for the new routes. It should be under the namespace :admin do section. Here is an example:

resources :wheels

You will replace :wheels with the table name we are altering. This will define a GET/POST/PATCH/DELETE endpoint, so if the user only wants a way to read the data and not mutate you can do only: [:index]

For this step, use the Database table name input parameter to determine the resource name. Use the List all mutable fields fields to determine if an update route is needed. if Data can be deleted is yes, include a destroy endpoint. If Data can be created is yes, include a create and new endpoint.

Step 2:

Add the tab to the admin dashboard layout by modifying app/views/layouts/admin.html.erb. This layout file contains a dropdown tab category for all the tabs. It looks liek this

  <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
    Apps
  </a>

You will find the write tab category and place the link as a child of <ul class="dropdown-menu">.

Here is an example of adding the "wheels" tab to the XX category

            <ul class="dropdown-menu">
             <% if authorize?("admin/wheels-index") %>
               <li><%= link_to "Wheels", admin_wheels_path, class: "nav-link" %></li>
             <% end %>

We will implement the authorize?() method in a later step.

For this step, use the Tab category input parameter

Step 3: Setup proper permissions

Each page / API endpoint is only accessible via a certain permission level. Users can have multiple permission levels, often either admin, support, or manager. To add the permissions for the new endpoints, modify app/controllers/admin/base_controller.rb. You need to modify the ROLE_MAPPING constant. It is a KV, mapping the name of endpoint to an array of the roles that have access. The list is alphabeticzed.

Here is an example adding the permissions for the wheel endpoints:

      ROLE_MAPPING = {
       "admin/abuse_report_categories-create" => %w(admin manager),
                 ...
       "admin/review_reports-show" => %w(admin manager support),
       "admin/wheels-create" => %w(admin),
       "admin/wheels-destroy" => %w(admin),
       "admin/wheels-edit" => %w(admin),
       "admin/wheels-index" => %w(admin manager support),
       "admin/wheels-new" => %w(admin),
       "admin/wheels-show" => %w(admin manager support),
       "admin/wheels-update" => %w(admin)
     }.freeze

The endpoints that must be defined will match the ones exposed in step 1.

For this step, use the Required user roles input parameter. Replace the string arrays with the roles I gave you.

Step 4: Implement the controller

We need to build the controller that defines the endpoints. This controller will be specific based on the name of the database table, the routes that are defined, and the business logic that needs to be done. Your job is to scaffold this API controller. Here is an example API controller implementation for the wheel:

module Admin
   class WheelsController < BaseController
     before_action :authorize
     before_action :set_wheel, except: %i[index new create]

     def index
       @q = Wheel::Wheel.ransack(params[:q])
       @q.sorts = "created_at desc" if @q.sorts.empty?

       @wheels = @q.result

       @wheels = @wheels.order(id: :desc).page(params[:page]).per(20)
     end

     def show
       # Do nothing
     end

     def new
       @wheel = Wheel::Wheel.new
     end

     def edit; end

     def create
       @wheel = Wheel::Wheel.new(wheel_wheel_params)

       unless @wheel.save
         render :new
       end
     end

     def update
       unless @wheel.update(wheel_wheel_params)
         render :edit
       end
     end

     def destroy
       @wheel.destroy!

       redirect_back(fallback_location: admin_wheels_path, notice: "Wheel was successfully destroyed.")
     rescue ActiveRecord::DeleteRestrictionError => e
       raise ServiceError, e.message
     end

     private

     def wheel_wheel_params
       params.require(:wheel_wheel).permit!
     end

     def set_wheel
       @wheel = Wheel::Wheel.find_by!(tag: params[:id])
     end
   end
 end

I want you to take this example code and implement it for the requested database table. You can replace all instances of wheel or wheel::wheel or wheel_wheel with the proper database table name / class name.

The methods here should match the routes defined in step 1 in the routes.rb file.

Step 5: Create the HTML files needed to read and write the data

You need to create the HTML views to read the data. This will include a file to list all the resources (index.html.erb), a file to retrieve a certain resource show.html.erb, and a form to mutate the fields _form.html.erb. You will use the input parameter list all mutable fields to determine which fields can be mutated. For the _form.html.erb file, use the template below where it uses the simple_form_for method.

TODO: INLCUDE DETAILS ON HOW A NEW RESOURCE IS MADE.

Here are example files for the wheel:

app/views/admin/wheels/index.html.erb

div class="d-flex justify-content-between align-items-center mb-3">
   <h1><%= number_with_delimiter(@wheels.total_count) %> Wheels</h1>
   <%= link_to("+ New", new_admin_wheel_path, class: "btn btn-primary", remote: true) %>
 </div>

 <%= search_form_for(@q, url: admin_wheels_path) do |f| %>
   <%= f.label :status_eq, "Status" %>
   <%= f.select :status_eq, Wheel::Wheel.statuses.keys, include_blank: "Select status" %>
   <%= f.label :tag_or_internal_name_cont, "Search" %>
   <%= f.search_field :tag_or_internal_name_cont %>
   <%= f.submit "Filter", class: 'btn btn-primary' %>
 <% end %>

 <table class="table table-striped table-hover">
   <thead>
   <tr>
     <th>ID</th>
     <th><%= sort_link(@q, :internal_name, "Internal Name") %></th>
     <th>Status</th>
     <th>Daily Start</th>
     <th><%= sort_link(@q, :created_at, "Created At") %></th>
     <th>Actions</th>
   </tr>
   </thead>
   <tbody>
   <% @wheels.each do |wheel| %>
     <tr>
       <td><%= wheel.tag %></td>
       <td><%= wheel.internal_name %></td>
       <td>
         <% case wheel.status %>
         <% when 'activated' %>
           <span class="badge bg-success">Activated</span>
         <% when 'deactivated' %>
           <span class="badge bg-danger">Deactivated</span>
         <% end %>
       </td>
       <td><%= wheel.formatted_daily_start %></td>
       <td><%= l(wheel.created_at, format: :long) %></td>
       <td>
         <%= link_to "View", admin_wheel_path(wheel), class: "btn btn-primary" %>
         <%= link_to "Edit", edit_admin_wheel_path(wheel), remote: true, class: 'btn btn-primary' %>
         <%= link_to "Delete", admin_wheel_path(wheel), method: :delete, data: { confirm: "Are you sure?" }, class: 'btn btn-danger' %>
       </td>
     </tr>
   <% end %>
   </tbody>
 </table>

 <div class="text-center">
   <%= paginate @wheels, theme: 'bootstrap-5' %>
 </div>

app/views/admin/wheels/show.html.erb

<table class="table table-striped table-hover">
   <thead>
   <tr>
     <th>ID</th>
     <th>Internal Name</th>
     <th>Status</th>
     <th>Daily Start</th>
     <th>Created At</th>
     <th>Actions</th>
   </tr>
   </thead>
   <tbody>
   <tr>
     <td><%= @wheel.tag %></td>
     <td><%= @wheel.internal_name %></td>
     <td>
       <% case @wheel.status %>
       <% when 'activated' %>
         <span class="badge bg-success">Activated</span>
       <% when 'deactivated' %>
         <span class="badge bg-danger">Deactivated</span>
       <% end %>
     </td>
     <td><%= @wheel.formatted_daily_start %></td>
     <td><%= l(@wheel.created_at, format: :long) %></td>
     <td>
       <%= link_to "Edit", edit_admin_wheel_path(@wheel), remote: true, class: 'btn btn-primary' %>
       <%= link_to "Delete", admin_wheel_path(@wheel), method: :delete, data: { confirm: "Are you sure?" }, class: 'btn btn-danger' %>
     </td>
   </tr>
   </tbody>
 </table>

app/views/admin/wheels/_form.html.erb

<%= simple_form_for(@wheel, url: @wheel.new_record? ? admin_wheels_path : admin_wheel_path(@wheel), remote: true) do |f| %>
   <% if @wheel.errors.any? %>
     <div class="alert alert-danger" role="alert">
       <strong>Please review the following errors:</strong>
       <ul>
         <% @wheel.errors.full_messages.each do |message| %>
           <li><%= message %></li>
         <% end %>
       </ul>
     </div>
   <% end %>
   <%= f.input :internal_name, required: true %>
   <p>Only one wheel can be activated globally at a time.</p>
   <%= f.input :status, collection: Wheel::Wheel.statuses.keys, required: true %>
   <p>The time (America/New_York) when the wheel period starts daily. Ex. 1430 is a 2:30 PM daily start time.</p>
   <%= f.input :daily_start, required: true %>
   <%= f.button :submit, "Submit", class: "btn btn-primary" %>
 <% end %>

Step 6: Create unit tests (rspecs)

The controller should have a unit test to ensure the functionality works correctly. You should copy this format of the spec file. This file should always be included and have the right path. The path is always spec/controllers/admin/<table_name>_controller_spec.rb

spec/controllers/admin/wheels_controller_spec.rb

Spec.describe Admin::WheelsController do
let(:user) { create(:user, :admin) }

before do
  session[:user_id] = user.id
end

describe "GET #index" do
  before do
    create(:wheel)
  end

  it "returns success" do
    get :index

    expect(response).to render_template(:index)
  end

  context "when there is sort provided" do
    it "returns success" do
      get :index, params: { q: { s: "created_at desc" } }

      expect(response).to render_template(:index)
    end
  end
end

describe "GET #show" do
  let(:wheel) { create(:wheel) }

  it "returns success" do
    get :show, params: { id: wheel.tag }

    expect(response).to render_template(:show)
  end
end

describe "GET #edit" do
  let(:wheel) { create(:wheel) }

  it "returns success" do
    get :edit, params: { id: wheel.tag }, xhr: true

    expect(response).to render_template(:edit)
  end
end

describe "GET #new" do
  it "returns success" do
    get :new, xhr: true

    expect(response).to render_template(:new)
  end
end

describe "POST #create" do
  let(:wheel_wheel_params) { attributes_for(:wheel) }

  it "returns success" do
    post :create, params: { wheel_wheel: wheel_wheel_params }, xhr: true

    expect(response).to have_http_status(:success)
  end

  context "when the creation fails" do
    let(:wheel_wheel_params) { { internal_name: "" } }

    it "re-renders the form" do
      post :create, params: { wheel_wheel: wheel_wheel_params }, xhr: true

      expect(response).to render_template(:new)
    end
  end
end

describe "PUT #update" do
  let(:wheel) { create(:wheel) }
  let(:wheel_wheel_params) { { internal_name: "Test Wheel" } }

  it "returns success" do
    put :update, params: { id: wheel.tag, wheel_wheel: wheel_wheel_params }, xhr: true

    expect(response).to have_http_status(:success)
  end

  context "when the update fails" do
    let(:wheel_wheel_params) { { internal_name: "" } }

    before do
      allow_any_instance_of(Wheel::Wheel).to receive(:update).and_return(false)
    end

    it "re-renders the form" do
      put :update, params: { id: wheel.tag, wheel_wheel: wheel_wheel_params }, xhr: true

      expect(response).to render_template(:edit)
    end
  end
end

describe "DELETE #destroy" do
  let(:wheel) { create(:wheel) }

  it "returns success" do
    delete :destroy, params: { id: wheel.tag }

    expect(response).to redirect_to(admin_wheels_path)
    expect(flash[:notice]).to eq("Wheel was successfully destroyed.")
    expect { wheel.reload }.to raise_error(ActiveRecord::RecordNotFound)
  end

  context "when the destroy fails" do
    before do
      allow_any_instance_of(Wheel::Wheel)
        .to receive(:destroy!)
              .and_raise(ActiveRecord::DeleteRestrictionError)
    end

    it "returns an error" do
      delete :destroy, params: { id: wheel.tag }

      expect(response).to have_http_status(:redirect)
      expect(flash[:alert]).to be_present
    end
  end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment