Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/bin
/dark-web-extension/*.wasm
/dark-web-extension/web-ext-artifacts
42 changes: 42 additions & 0 deletions cmd/wasm/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build js && wasm

package main

import (
"strings"
"syscall/js"
Comment thread
K-Phoen marked this conversation as resolved.

"github.com/K-Phoen/dark/internal/pkg/converter"
"go.uber.org/zap"
)

func dashboardToDarkFunc() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
return "invalid nnumber of arguments passed"
}

logger := zap.NewNop()
conv := converter.NewJSON(logger)

inputJSON := args[0].String()
output := &strings.Builder{}

if err := conv.ToK8SManifest(strings.NewReader(inputJSON), output, converter.K8SManifestOptions{
Folder: "folder_name",
Name: "name",
Namespace: "default",
}); err != nil {
logger.Fatal("Could not convert dashboard", zap.Error(err))
}

return output.String()
})
}

func main() {
js.Global().Set("dashboardToDark", dashboardToDarkFunc())

c := make(chan struct{}, 0)
<-c
}
37 changes: 37 additions & 0 deletions dark-web-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# DARK web extension

Browser extension to convert Grafana dashboards into an equivalent DARK YAML file.

Whenever a Grafana dashboard is viewed, the extension will add an export button to the dashboard's toolbar.

**Note:** only tested with Firefox.

## Building

### Compile the `./cmd/wasm` binary to WebAssembly

The extension re-uses the conversion logic written in Go, thanks to WebAssembly.

A binary specific to this use-case is bundled within the repository, and needs to be compiled for the extension to run:

```sh
GOOS=js GOARCH=wasm go build -o dark-web-extension/dark.wasm ./cmd/wasm
```

### `./dark-web-extension/wasm_exec.js` file

Distributed with Go:

```sh
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./dark-web-extension
```

### Building the extension

```sh
npm install --global web-ext
cd ./dark-web-extension/
web-ext build
```

The extension can then be installing from the file generated by `web-ext`: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing
41 changes: 41 additions & 0 deletions dark-web-extension/background_script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Put all the javascript code here, that you want to execute in background.

console.debug("starting background worker");

browser.runtime.onMessage.addListener(handleMessage);

function handleMessage(request, _sender, sendResponse) {
console.info(`content script sent a message`, request);

if (request.action === 'convert-to-k8s') {
convertToK8s(request.data.model).then(result => {
const blob = new Blob([result], {type: 'text/yaml'});

browser.downloads.download({
url: window.URL.createObjectURL(blob),
filename: 'dark-dashboard.yaml',
conflictAction: 'uniquify',
});

sendResponse({
success: true,
result: result,
});
});
}

return true;
}

async function convertToK8s(dashboardModel) {
const go = new Go();

return WebAssembly.instantiateStreaming(
fetch(browser.runtime.getURL("dark.wasm")),
go.importObject,
).then(result => {
go.run(result.instance);

return dashboardToDark(dashboardModel);
});
}
101 changes: 101 additions & 0 deletions dark-web-extension/content_script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Put all the javascript code here, that you want to execute after page load.
if (isGrafana()) {
console.debug("content_script bootstrap");

// hackish way of giving enough time for grafana to load the dashboard
waitForElm('.page-toolbar').then(() => {
console.debug('page-toolbar found, inserting DARK export button');
setupExportButton();
});
}

function setupExportButton() {
const buttonsContainer = document.querySelector('.page-toolbar > div:last-child');

if (buttonsContainer.length === 0) {
console.debug("buttons container not found");
return;
}

console.debug("appending export button");
const lastButton = buttonsContainer.childNodes[buttonsContainer.childNodes.length - 1];
const darkExportButton = lastButton.cloneNode(true);
darkExportButton.onclick = triggerExport;
darkExportButton.title = 'Export as DARK dashboard';

const darkExportButtonSVG = darkExportButton.querySelector('svg');
darkExportButtonSVG.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 493.525 493.525" xml:space="preserve">
<g>
<path d="M430.557,79.556H218.44c21.622,12.688,40.255,29.729,54.859,49.906h157.258
c7.196,0,13.063,5.863,13.063,13.06v238.662c0,7.199-5.866,13.064-13.063,13.064H191.894c-7.198,0-13.062-5.865-13.062-13.064
V222.173c-6.027-3.1-12.33-5.715-18.845-7.732c-3.818,11.764-12.105,21.787-23.508,27.781c-2.39,1.252-4.987,2.014-7.554,2.844
v136.119c0,34.717,28.25,62.971,62.968,62.971h238.663c34.718,0,62.969-28.254,62.969-62.971V142.522
C493.525,107.806,465.275,79.556,430.557,79.556z" />
<path d="M129.037,175.989c51.419,1.234,96.388,28.283,122.25,68.865c2.371,3.705,6.434,5.848,10.657,5.848
c1.152,0,2.322-0.162,3.46-0.486c5.377-1.545,9.114-6.418,9.179-12.006c0-0.504,0-1.01,0-1.51
c0-81.148-64.853-147.023-145.527-148.957V64.155c0-5.492-3.038-10.512-7.879-13.078c-2.16-1.139-4.533-1.707-6.889-1.707
c-2.94,0-5.848,0.88-8.35,2.584L5.751,120.526C2.162,122.98,0.018,127.041,0,131.394c-0.017,4.338,2.113,8.418,5.687,10.902
l100.17,69.451c2.518,1.753,5.459,2.631,8.414,2.631c2.355,0,4.696-0.553,6.857-1.676c4.855-2.549,7.909-7.6,7.909-13.092V175.989z" />
</g>
</svg>`;

buttonsContainer.appendChild(darkExportButton);
}

function triggerExport() {
const dashboardUID = getDashboardUID();

if (!dashboardUID) {
console.warn('could not infer dashboard UID');
return;
}

console.debug("exporting dashboard with UID", dashboardUID);

fetch(`${window.location.origin}/api/dashboards/uid/${dashboardUID}`)
.then(response => response.json())
.then(apiResponse => convertGrafanaDashboard(apiResponse.dashboard))
.then(response => console.debug('response', response))
.catch(error => {
console.error(`Error: ${error}`);
})
}

function getDashboardUID() {
const pagePath = window.location.pathname;

return pagePath.split('/')[2];
}

function convertGrafanaDashboard(model) {
return browser.runtime.sendMessage({
action: "convert-to-k8s",
data: {
model: JSON.stringify(model),
},
});
}

function isGrafana() {
return document.body.classList.contains('app-grafana');
}

function waitForElm(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, {
childList: true,
subtree: true
});
});
}
Binary file added dark-web-extension/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions dark-web-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"manifest_version": 2,
"name": "dark",
"description": "Converts Grafana dashboards into an equivalent DARK YAML file",
"version": "0.0.1",
"icons": {
"64": "icons/icon.png"
},

"permissions": [
"downloads"
],

"background": {
"scripts": [
"wasm_exec.js",
"background_script.js"
]
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"content_script.js"
]
}
]
}
Loading