Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
PATH
remote: .
specs:
dev_toolbar (0.1.0)
rails (~> 7.0)
dev_toolbar (2.1.0)
rails (>= 7.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -122,6 +122,8 @@ GEM
nio4r (2.7.3)
nokogiri (1.16.5-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.5-x86_64-linux)
racc (~> 1.4)
psych (5.1.2)
stringio
racc (1.8.0)
Expand Down Expand Up @@ -180,6 +182,7 @@ GEM

PLATFORMS
x86_64-darwin-22
x86_64-linux

DEPENDENCIES
bundler (~> 2.0)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ if Rails.env.development?
end
```

Include the JavaScript:
```rb
# config/importmap.rb
pin "dev_toolbar", to: "dev_toolbar/index.js"
pin "dev_toolbar/toolbar", to: "dev_toolbar/toolbar.js"

# app/javascripts/application.js
import "dev_toolbar"
```

These routes will now appear on every page in your app while in development.

## Updating the gem
Expand Down
34 changes: 34 additions & 0 deletions app/assets/javascripts/dev_toolbar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Toolbar from "dev_toolbar/toolbar"

function waitForElementToExist(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}

const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});

observer.observe(document.body, {
subtree: true,
childList: true,
});
});
}
const loadEvent = self.hasOwnProperty("Turbo") ? "turbo:load" : "DOMContentLoaded";

document.addEventListener(loadEvent, function() {
if (!document.getElementById("dev-toolbar")) {
Toolbar.render();
}
waitForElementToExist("#dev-toolbar-toggle").then( () => {
document.getElementById("dev-toolbar-toggle").addEventListener("click", function() {
var links = document.getElementById("dev-toolbar-links");
links.classList.toggle("hidden");
});
});
});
24 changes: 24 additions & 0 deletions app/assets/javascripts/dev_toolbar/toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default class Toolbar {
static render() {
const configuration = document.querySelector("meta[name=dev_toolbar_config]")
const defined_links = JSON.parse(configuration.content)
let toolbar_links = ``
for (let index = 0; index < defined_links.length; index++) {
const link = defined_links[index];
toolbar_links += `<a href="${link.path}" target="_blank" class="dev-toolbar-link">${link.name}</a>`
}
const toolbar_html = `
<div id="dev-toolbar">
<div id="dev-toolbar-button">
<button id="dev-toolbar-toggle">🛠️</button>
</div>
<div id="dev-toolbar-links" class="hidden">
${toolbar_links}
</div>
</div>
`
document.body.insertAdjacentHTML('beforeend', toolbar_html)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make toolbar render idempotent on repeated Turbo loads

Toolbar.render() always appends a new #dev-toolbar node with insertAdjacentHTML, so after Turbo restores a cached page (for example, browser Back/Forward) and fires turbo:load again, the page accumulates duplicate toolbars and duplicated toggle controls. This regression was introduced by moving rendering client-side without a guard/removal step; add an existence check (or remove before cache) so repeated loads do not duplicate UI.

Useful? React with 👍 / 👎.

}

}
export { Toolbar }
41 changes: 41 additions & 0 deletions app/assets/stylesheets/dev_toolbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#dev-toolbar {
position: fixed;
right: 0;
top: 50vh;
transform: translateY(-50%);
background-color: #f0f0f0;
border: 1px solid #ccc;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
color: #808080;
}

#dev-toolbar-toggle {
all: unset;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I changed the toolbar icon from a link to a button, since clicking on it doesn't navigate to another (part of the) page. all: unset resets the default styling of the button.

font-size: 2em;
border: none;
cursor: pointer;
line-height: 1.5;
padding: 0 10px;
text-decoration: none;
}

#dev-toolbar-links {
display: flex;
flex-direction: column;
}

.dev-toolbar-link {
padding: 5px 10px;
border-bottom: 1px #f0f0f0 solid;
color: #808080;
text-decoration: none;
background-color: white;
}

#dev-toolbar-links.hidden {
display: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class ErdController < ActionController::Base

def show
@erd_path = Rails.root.join("erd.png")
render :show
render "dev_toolbar/erd/show", formats: [:html]
Copy link
Copy Markdown
Author

@jelaniwoods jelaniwoods May 21, 2026

Choose a reason for hiding this comment

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

The Rails app was not finding this view template until I specified it with this default. I don't this this should be necessary, but I wasn't able to figure it out. I'd love to learn why the previous way wasn't working.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I also looked into this for a bit, but wasn't able to figure it out. Claude 4.7 sent me down some paths, which were all wrong in the end. This is a weird one, but I'm going to leave it alone now.

end
end
end
22 changes: 19 additions & 3 deletions lib/dev_toolbar/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@ module DevToolbar
class Engine < ::Rails::Engine
isolate_namespace DevToolbar

config.autoload_paths << File.expand_path("../app/controllers", __FILE__)
config.paths["app/views"] << File.expand_path("../app/views", __FILE__)

config.assets.paths << root.join("app/assets/stylesheets")

initializer "dev_toolbar.assets_precompile", group: :all do |app|
# Only configure asset precompilation if Sprockets is available
if defined?(Sprockets) && app.config.respond_to?(:assets)
app.config.assets.precompile += [
"dev_toolbar/toolbar.js",
"dev_toolbar/index.js",
]
end
end

initializer "dev_toolbar.add_static_assets_middleware" do |app|
app.middleware.use ::Rack::Static,
# the url prefix to intercept
urls: ["/dev_toolbar"],
root: "#{root}/app/"
end

initializer "dev_toolbar.add_routes", after: :add_routing_paths do |app|
app.routes.append do
get "/erd", to: "dev_toolbar/erd#show"
Expand Down
70 changes: 4 additions & 66 deletions lib/dev_toolbar/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,65 +10,10 @@ def call(env)
if Rails.env.development? && headers["Content-Type"]&.include?("text/html")
response_body = response.body
toolbar_html = <<-HTML
<div id="dev-toolbar">
<div id="dev-toolbar-button">
<a id="dev-toolbar-toggle">🛠️</a>
</div>
<div id="dev-toolbar-links" class="hidden">
#{toolbar_links}
</div>
</div>
<style>
#dev-toolbar {
position: fixed;
right: 0;
top: 50vh;
transform: translateY(-50%);
background-color: #f0f0f0;
border: 1px solid #ccc;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
color: #808080;
}

#dev-toolbar-toggle {
font-size: 2em;
border: none;
cursor: pointer;
line-height: 1.5;
padding: 0 10px;
text-decoration: none;
}

#dev-toolbar-links {
display: flex;
flex-direction: column;
}

.dev-toolbar-link {
padding: 5px 10px;
border-bottom: 1px #f0f0f0 solid;
color: #808080;
text-decoration: none;
background-color: white;
}

#dev-toolbar-links.hidden {
display: none;
}
</style>
<script>
document.getElementById('dev-toolbar-toggle').addEventListener('click', function() {
var links = document.getElementById('dev-toolbar-links');
links.classList.toggle('hidden');
});
</script>
<meta name="dev_toolbar_config" content='#{toolbar_links_content}'>
Comment thread
jelaniwoods marked this conversation as resolved.
Copy link
Copy Markdown
Author

@jelaniwoods jelaniwoods May 21, 2026

Choose a reason for hiding this comment

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

The configured toolbar links needed to be passed from the initializer to the JavaScript file somehow so I decided to store them in a meta tag as JSON. Since it's in the <head> it won't clutter up the <body>, which is where students will mostly be looking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, this is very small and discrete in the source.

Comment thread
jelaniwoods marked this conversation as resolved.
Comment thread
jelaniwoods marked this conversation as resolved.
HTML
Comment thread
jelaniwoods marked this conversation as resolved.

response_body.sub!('</body>', "#{toolbar_html}</body>")
response_body.sub!('</head>', "#{toolbar_html}</head>")
headers["Content-Length"] = response_body.bytesize.to_s

response = [response_body]
Expand All @@ -79,15 +24,8 @@ def call(env)

private

def toolbar_links
DevToolbar.configuration.links.map do |link|
# if the erd.png file does not exist in /public, don't show the link
if link[:name] == "ERD" && !File.exist?(Rails.root.join("erd.png"))
next
else
"<a href='#{link[:path]}' target='_blank' class='dev-toolbar-link'>#{link[:name]}</a>"
end
end.compact.join(" ")
def toolbar_links_content
JSON.generate(DevToolbar.configuration.links)
end
end
end