Build a Spyglass Lens
Spyglass lenses consist of two components: a frontend (which may be trivial) and a backend.
Lens backend
Today, a lens backend must be linked in to the deck
binary. As such, lenses must live under
prow/spyglass/lenses
. Additionally lenses must be in a folder that matches the
name of the lens. The content of this folder will be served by deck
, enabling you to reference
static content such as images, stylesheets, or scripts.
Inside your template you must implement the lenses.Lens
interface.
An instance of the struct implementing the lenses.Lens
interface must then be registered with
spyglass, by calling lenses.RegisterLens
.
A minimal example of a lens called samplelens
, located at lenses/samplelens
, might look like this:
package samplelens
import (
"encoding/json"
"sigs.k8s.io/prow/pkg/config"
"sigs.k8s.io/prow/pkg/spyglass/lenses"
)
type Lens struct{}
func init() {
lenses.RegisterLens(Lens{})
}
// Config returns the lens's configuration.
func (lens Lens) Config() lenses.LensConfig {
return lenses.LensConfig{
Title: "Human Readable Lens",
Name: "samplelens", // remember: this *must* match the location of the lens (and thus package name)
Priority: 0,
}
}
// Header returns the content of <head>
func (lens Lens) Header(artifacts []lenses.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string {
return ""
}
func (lens Lens) Callback(artifacts []lenses.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string {
return ""
}
// Body returns the displayed HTML for the <body>
func (lens Lens) Body(artifacts []lenses.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string {
return "Hi! I'm a lens!"
}
If you want to read resources included in your lens (such as templates), you can find them in the
provided resourceDir
.
Finally, you will need to import your lens from deck
in order to actually link it in. You can do
this by import
ing it from prow/cmd/deck/main.go
, alongside the other lenses:
import (
// ...
_ "sigs.k8s.io/prow/pkg/spyglass/lenses/samplelens"
)
Finally, you can then test it by running ./cmd/deck/runlocal
and loading a spyglass page.
Lens frontend
The HTML generated by a lens can reference static assets that will be served by Deck on behalf of
your lens. Scripts and stylesheets can be referenced in the output of the Header()
function (which
is inserted into the <head>
element). Relative references into your directory will work: spyglass
adds a <base>
tag that references the expected output directory.
Spyglass lenses have access to a spyglass
global that provides a number of APIs to interact with
your lens backend and the rest of the world. Your lens is rendered in a sandboxed iframe, so you
generally cannot interact without using these APIs.
We recommend writing lenses using TypeScript, and provide TypeScript declarations for the spyglass
APIs.
In order to build frontend resources in, you will need to notify the build system. Assuming you had
a template called template.html
, a typescript file called sample.ts
, a stylesheet called
style.css
, and an image called magic.png
. The changes are:
- Add a new file called
tsconfig.json
:
{
"extends": "../../../../tsconfig.json",
"include": [
"sample.ts",
],
}
- Add a line in prow/cmd/deck/.ts-packages:
prow/spyglass/lenses/sample/sample.ts->script_bundle.min.js
With this setup, you would reference your script in your HTML as script_bundle.min.js
, like so:
<script type="text/javascript" src="script_bundle.min.js"></script>
Lens APIs
Many Spyglass APIs are asynchronous, and so return a
Promise. We
recommend using async
/await
to use them, like this:
async function doStuff(): Promise<void> {
const someStuff = await spyglass.request("");
}
We provide the following methods under spyglass
in all lenses:
spyglass.contentUpdated(): void
contentUpdated
should be called whenever you make changes to the content of the page. It signals
to the Spyglass host page that it needs to recalculate how your lens is displayed. It is not
necessary to call it on initial page load.
spyglass.request(data: string): Promise<string>
request
is used to call back to your lens’s backend. Whatever data
you provide will be provided
unmodified to your lens backend’s Callback()
method. request
returns a Promise, which will
eventually be resolved with the string returned from Callback()
(unless an error occurs, in which
case it will fail). We recommend, but do not require, that both strings be JSON-encoded.
spyglass.updatePage(data: string): Promise<void>
updatePage
calls your lens backend’s Body()
method again, passing in whatever data
you
provide and shows a loading spinner. Once the call completes, the lens is re-displayed using the
newly-provided <body>
. Note that this does not reload the lens, and so your script will keep
running. The returned promise resolves once the new content is ready.
spyglass.requestPage(data: string): Promise<string>
requestPage
calls your lens backend’s Body()
method again, passing in whatever data
you
provide. Unlike updatePage
, it does not show a spinner, and does not change the page. Instead,
the returned promise will resolve with the newly-generated HTML.
spyglass.makeFragmentLink(fragment: string): string
makeFragmentLink
returns a link to the top-level page that will cause your lens to receive the
specified fragment
in location.hash
, and no other lens on the page to receive any fragment.
This is useful when generating links for the user to copy to your content, but should not be used
to perform direct navigation - instead, just update location.hash
, and propagation will be
handled transparently.
If the provided fragment
does not have a leading #
one will be added, for consistency with the
behaviour of location.hash
.
spyglass.scrollTo(x: number, y: number): Promise<void>
scrollTo
scrolls the parent Spyglass page such that the provided (x, y) document-relative
coordinate of your lens is visible. Note that we keep lenses at slightly under 100% page width, so
only y is currently meaningful.
Special considerations
Sandboxing
Lenses are contained in sandboxed iframes in the parent page. The most notably restricted activity is making XHR requests to Deck, which would be considered prohibited CORS requests. Lenses also cannot directly interact with their parent window, outside of the provided APIs.
Links
We set a default <base>
with href
set pointing in to your resource directory, and target
set
to _top
. This means that links will by default replace the entire spyglass page, which is usually
the intended effect. It also means that src
or href
HTML attributes are based in those
directories, which is usually what you want in this context.
Fragments / Anchor links
Fragment URLs (the part after the #
) are supported fairly transparently, despite being in an iframe.
The parent page muxes all the lens’s fragments and ensures that if the page is loaded, each lens
receives the fragment it expects. Changing your fragment will automatically update the parent page’s
fragment. If the fragment matches the ID or name of an element, the page will scroll such that that
element is visible.
Anchor links (<a href="#something">
) would usually not work well in conjunction with the <base>
tag. To resolve this, we rewrite all links of this form to behave as expected both on page load and
on DOM modification. In most cases, this should be transparent. If you want users to copy links via
right click -> copy link, however, this will not work nicely. Instead, consider setting the href
attribute to something from spyglass.makeFragmentLink
, but handling clicks by manually setting
location.hash
to the desired fragment.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.