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.
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.
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.
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
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.
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.
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.
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 %>
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