Converting My Loggly Search Bookmarklets Into An Unpacked Chrome Extension
Four years ago, I created a Loggly Bookmarklet that would augment the Loggly Search page formatting to look and feel more like Kibana (which, in my opinion, has more accessible contrast and whitespace usage). Then, two years ago, I added a link between the Loggly JSON modal and my JSON explorer. Unfortunately, a few weeks ago, Loggly implemented a new Content Security Policy (CSP) that is blocking my bookmarklets. As such, I finally sat down and converted my bookmarklets into an unpacked Chrome extension.
View this code in my Loggly Search Extension MVP project on GitHub.
An "unpacked" Chrome extension is just an extension that is loaded off of the local file system, as opposed to an "official" extension which is installed via the Chrome app store. Since these are just for my personal consumption, I don't see any real need to dig into extensions any deeper than I need to (at this time).
This Loggly Chrome extension uses content_scripts
, which is an approach in which Chrome injects a set of CSS and JavaScript files into any page that matches a given glob-pattern. Here, I have a single CSS file and a single JavaScript file:
{
"name": "Loggly Search Plus",
"description": "Better styles and grid functionality for Loggly Search.",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_icon": "icon-128.png"
},
"content_scripts": [
{
"matches": [
"https://*.loggly.com/search"
],
"css": [
"custom.css"
],
"js": [
"custom.js"
],
"run_at": "document_end"
}
]
}
The CSS file both overrides existing CSS styles already in the Loggly stylesheet; and, adds new CSS styles that pertain to the new elements that I'm going to inject:
.ui-grid-cell-contents {
background-color: #FFFFFF ;
border-bottom: 1px solid #F0F0F0 ;
color: #000000 ;
font-family: 'Lucida Console', Monaco, monospace ;
font-size: 13px ;
padding: 11px 11px 11px 11px ;
}
.grid-view.ui-grid .ui-grid-cell[ ui-grid-cell ] {
height: 40px ;
}
.bnb-outer {
background-color: rgba( 0, 0, 0, 0.8 ) ;
bottom: 0px ;
display: none ;
left: 0px ;
position: fixed ;
right: 0px ;
top: 0px ;
z-index: 999999999999 ;
}
.bnb-outer--active {
display: block ;
}
.bnb-inner {
background-color: #ffffff ;
border: 1px solid #cccccc ;
border-radius: 7px 7px 7px 7px ;
bottom: 100px ;
color: #000000 ;
font-family: monospace ;
font-size: 18px ;
left: 100px ;
line-height: 27px ;
margin: 0px 0px 0px 0px ;
overflow: auto ;
padding: 30px 30px 30px 30px ;
position: absolute ;
right: 100px ;
top: 100px ;
white-space: pre-wrap ;
}
.bnb-inner strong {
color: #aa0000 ;
font-weight: 400 ;
}
.bnb-explore,
.bnb-explore:visited {
background-color: #ffffff ;
border-radius: 5px 5px 5px 5px ;
color: #aa0000 ;
display: none ;
padding: 7px 13px 8px 13px ;
position: absolute ;
right: 5px ;
top: 5px ;
}
.bnb-explore:hover {
background-color: #aa0000 ;
color: #ffffff ;
}
.bnb-explore--active {
display: block ;
}
The .bnb
prefixed styles are for the JSON-parsing modal window that I'm going to inject.
The custom.js
file is a bit more involved. Because I am attempting to reference and bind to the host window
object, I have to get a little tricky. I can't simply link to my JavaScript file since it runs in a sandbox. Instead, I have to stringify my main JavaScript function and then inject the function as the content of a <script>
block that I dynamically append to the page.
Here's my strinigification function:
// I inject and execute the given IIFE (immediately-invoked function expression) into the
// current HTML document.
function injectScriptContent( iifeFunction ) {
var script = document.createElement( "script" );
script.setAttribute( "type", "text/javascript" );
script.setAttribute( "data-source", "Injected by Loggly Chrome Extension." );
script.textContent = `;( ${ iifeFunction.toString() } )();`;
document.head.appendChild( script );
}
This function takes a Function
reference; and then, stringifies it using the .toString()
function. This returns the source representation of the Function
, which I then wrap in an IIFE (Immediately-Invoked Function Expression) and append to the head
of the document. By doing this, it allows my Function
to execute in the context of the host window, which ultimately gives me access to the window
object (and jQuery
).
Here's the full source of my custom.js
file - the main goal of this file is to allow me to double-click on any cell in the Grid layout and then automatically parse the contents of that cell as if they were JSON, which is then rendered in a modal window using a pretty-printed <pre>
tag:
// Inject script content at the top of the HTML head.
injectScriptContent( initLogglyAugmentation );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I inject and execute the given IIFE (immediately-invoked function expression) into the
// current HTML document.
function injectScriptContent( iifeFunction ) {
var script = document.createElement( "script" );
script.setAttribute( "type", "text/javascript" );
script.setAttribute( "data-source", "Injected by Loggly Chrome Extension." );
script.textContent = `;( ${ iifeFunction.toString() } )();`;
document.head.appendChild( script );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I augment the Loggly search styles and functionality to be a bit more helpful.
function initLogglyAugmentation() {
// CAUTION: We can use jQuery in this Chrome Extension because we know that Loggly
// uses jQuery in their application.
var doc = $( document );
var win = $( window );
var head = $( document.head );
var body = $( document.body );
var outer = $( "<div></div>" )
.addClass( "bnb-outer" )
;
var inner = $( "<pre></pre>" )
.addClass( "bnb-inner" )
;
var explore = $( "<a>Explore</a>" )
.addClass( "bnb-explore" )
.attr( "target", "_blank" )
;
// Add all the nodes to the active document.
body.append( outer.append( explore ).append( inner ) );
// ---
// Setup event handler.
// ---
// When the user double-clicks in the Grid cell, check for valid JSON and pretty-
// print it in a modal window.
doc.on(
"dblclick",
".ui-grid-cell-contents",
function handleDblClick( event ) {
var node = $( this );
// Parse the JSON content and then re-stringify it so that it is
// formatted for easier reading.
var nodeText = node.text();
// Since there's no native way to check to see if a value is valid JSON
// before parsing it, we might as well just try to parse it and catch
// any errors.
try {
var payload = JSON.parse( nodeText );
var content = JSON.stringify( payload, null, 4 );
var showExploreButton = true;
} catch ( error ) {
console.warn( "Could not parse JSON in Grid cell. Falling back to raw value." );
var content = nodeText;
// Since the content is not valid JSON, we can't show the Explore button
// as that will just lead to a failure later on with the external site.
var showExploreButton = false;
}
// Inject the value into the modal window using .text() so that any HTML
// that might be embedded in the JSON is escaped.
inner.text( content );
// Pull the HTML content out, which now contains ESCAPED embedded HTML if
// it exists. At this point, we can do some light string-manipulation to
// add additional HTML-based formatting.
var html = inner
.html()
// Replace escaped line-breaks in strings with actual line-breaks
// that indent based on the prefix of the JSON key-value pair.
.replace(
/(^\s*"[\w-]+":\s*")([^\r\n]+)/gm,
function( $0, leading, value ) {
var indentation = " ".repeat( leading.length );
var formattedValue = value.replace( /\\n/g, ( "\n" + indentation ) );
return( leading + formattedValue );
}
)
// Emphasize the JSON key.
.replace( /("[\w-]+":)/g, "<strong>$1</strong>" )
;
inner.html( html );
// Show the modal window.
outer.addClass( "bnb-outer--active" );
inner.scrollTop( 0 );
if ( ! showExploreButton ) {
return;
}
// Try to add the explore button, which requires the btoa() function.
try {
explore.attr( "href", `https://bennadel.github.io/JSON-Explorer/dist/#${ btoa( nodeText ) }` );
explore.addClass( "bnb-explore--active" );
} catch ( error ) {
console.warn( "Explore button could not be rendered." );
console.error( error );
}
}
);
// When the user clicks on the outer portion of the modal window, close it.
doc.on(
"click",
".bnb-outer",
function handleClick( event ) {
// Ignore any click events from the inner portion of the modal.
if ( ! outer.is( event.target ) ) {
return;
}
outer.removeClass( "bnb-outer--active" );
explore.removeClass( "bnb-explore--active" );
}
);
// When the user hits Escape, close the modal window.
win.on(
"keydown",
function handleKeyPress( event ) {
// Ignore any non-Escape keys.
if (
( event.key !== "Escape" ) &&
( event.key !== "Esc" )
) {
return;
}
if ( outer.is( ".bnb-outer--active" ) ) {
outer.removeClass( "bnb-outer--active" );
}
}
);
}
Nothing too tricky going on here. It uses old-school jQuery which - for you kids out there - is still a really powerful set of abstractions despite what others might be telling you. And now, when I search in Loggly using the grid layout, I can double-click on any cell to get a JSON-parsed modal window:
It feels so luxurious to have an accessible Loggly search grid once again! My eyes are just getting old; and, I have trouble using the default Loggly color scheme and list-layout (the fonts are so tiny). Being able to use the grid view and then get a separate modal window for JSON is just fantabulous!
Want to use code from this post? Check out the license.
Reader Comments