SSO ability for plugin dashboards

This facilitates the SSO solution for plugin dashboards
To accomplish this we must use a reverse proxy to
inject the correct header information on requests
so that the backend services will accept a prelogin.
This is specific to each plugin so it needs to be
extensible and allow for a dashboard to customize how
it's done.

This also allows for a fully hosted page when running
in headless mode.

Fixes #42

Change-Id: I442e22a19a387ae367c2518085cdca0f2786a0a4
This commit is contained in:
Schiefelbein, Andrew 2020-06-26 09:45:35 -05:00
parent 1d9e096fb5
commit 850df9d213
22 changed files with 270 additions and 279 deletions

View File

@ -25,38 +25,19 @@ $HOME/.airship/airshipui.json.
To add service dashboards, create a section at the top level of airshipui.json as follows:
```
"clusters": [
{
"name": "clusterA",
"baseFqdn": "svc.cluster.local",
"namespaces": [
"dashboards": [
{
"name": "ceph",
"dashboards": [
{
"name": "Ceph",
"protocol": "https",
"fqdn": "ceph-dash.example.domain",
"port": 443,
"baseURL": "https://ceph-dash.example.domain",
"path": ""
}
]
},
{
"name": "openstack",
"dashboards": [
{
"name": "Horizon",
"protocol": "http",
"hostname": "horizon",
"port": 80,
"baseURL": "http://horizon",
"path": "dashboard/auth/login"
}
]
}
]
}
]
```
For dashboards that are made available through service endpoints in your cluster, the FQDN for the dashboard will be constructed using the format
@ -68,21 +49,17 @@ For dashboards that are made available through service endpoints in your cluster
If both "hostname" and "fqdn" are provided, "fqdn" will take precedence.
The airshipui.json configuration file can also be used to launch "plugins", or external executables, in the background as Airship UI starts. Any processes
The airshipui.json configuration file can also be used to launch external executables that server your dashboards, in the background as Airship UI starts. Any processes
launched by Airship UI will be terminated when Airship UI exits, including any child processes started by the plugins. If the plugin launches a web
dashboard, it can be also be included in the list of service dashboards within Airship UI. The following example demonstrates how to add configuration to
launch and use Octant within Airship UI:
```
"plugins": [
"dashboards": [
{
"name": "Octant",
"dashboard": {
"protocol": "http",
"fqdn": "localhost",
"port": 7777,
"path": ""
},
"baseURL": "http://localhost:7777",
"path": "",
"executable": {
"autoStart": true,
"filepath": "/usr/local/bin/octant",
@ -96,7 +73,7 @@ launch and use Octant within Airship UI:
]
```
To prevent a plugin from launching but retain its configuration for later use, simply set "autoStart" to false.
To prevent a dashboard executable from launching but retain its configuration for later use, simply set "autoStart" to false.
## Developer's Guide

1
go.sum
View File

@ -893,6 +893,7 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.0.0-20191016110408-35e52d86657a/go.mod h1:/L5qH+AD540e7Cetbui1tuJeXdmNhO8jM6VkXeDdDhQ=
k8s.io/api v0.0.0-20191016225839-816a9b7df678/go.mod h1:LZQaT8MvVpl7Bg2lYFcQm7+Mpdxq8p1NFl3yh+5DCwY=

View File

@ -66,18 +66,20 @@ func launch(cmd *cobra.Command, args []string) {
// Read AirshipUI config file
if err := configs.SetUIConfig(airshipUIConfigPath); err == nil {
// launch any plugins marked as autoStart: true in airshipui.json
for _, p := range configs.UIConfig.Plugins {
if p.Executable.AutoStart {
for _, dashboard := range configs.UIConfig.Dashboards {
if dashboard.Executable != nil {
if dashboard.Executable.AutoStart {
waitgrp.Add(1)
go RunBinaryWithOptions(
ctx,
p.Executable.Filepath,
p.Executable.Args,
dashboard.Executable.Filepath,
dashboard.Executable.Args,
&waitgrp,
sigs,
)
}
}
}
} else {
log.Printf("config %s", err)
webservice.SendAlert(configs.Info, fmt.Sprintf("%s", err), true)

View File

@ -33,8 +33,7 @@ var (
// Config basic structure to hold configuration params for Airship UI
type Config struct {
AuthMethod *AuthMethod `json:"authMethod,omitempty"`
Plugins []Plugin `json:"plugins,omitempty"`
Clusters []Cluster `json:"clusters,omitempty"`
Dashboards []Dashboard `json:"dashboards,omitempty"`
}
// AuthMethod structure to hold authentication parameters
@ -44,21 +43,6 @@ type AuthMethod struct {
URL string `json:"url,omitempty"`
}
// Plugin structure to hold plugin specific parameters
type Plugin struct {
Name string `json:"name,omitempty"`
Dashboard *PluginDashboard `json:"dashboard,omitempty"`
Executable *Executable `json:"executable"`
}
// PluginDashboard structure to hold web dashboard parameters for plugins
type PluginDashboard struct {
Protocol string `json:"protocol,omitempty"`
FQDN string `json:"fqdn,omitempty"`
Port uint16 `json:"port,omitempty"`
Path string `json:"path,omitempty"`
}
// Executable structure to hold parameters for launching an executable plugin
type Executable struct {
AutoStart bool `json:"autoStart,omitempty"`
@ -69,24 +53,10 @@ type Executable struct {
// Dashboard structure
type Dashboard struct {
Name string `json:"name,omitempty"`
Protocol string `json:"protocol,omitempty"`
Hostname string `json:"hostname,omitempty"`
FQDN string `json:"fqdn,omitempty"`
Port uint16 `json:"port,omitempty"`
BaseURL string `json:"baseURL,omitempty"`
Path string `json:"path,omitempty"`
}
// Namespace structure
type Namespace struct {
Name string `json:"name,omitempty"`
Dashboards []Dashboard `json:"dashboards,omitempty"`
}
// Cluster basic structure describing a cluster
type Cluster struct {
Name string `json:"name,omitempty"`
BaseFqdn string `json:"baseFqdn,omitempty"`
Namespaces []Namespace `json:"namespaces,omitempty"`
IsProxied bool `json:"isProxied,omitempty"`
Executable *Executable `json:"executable,omitempty"`
}
// WsRequestType is used to set the specific types allowable for WsRequests
@ -144,8 +114,7 @@ type WsMessage struct {
YAML string `json:"yaml,omitempty"`
// information related to the init of the UI
Dashboards []Cluster `json:"dashboards,omitempty"`
Plugins []Plugin `json:"plugins,omitempty"`
Dashboards []Dashboard `json:"dashboards,omitempty"`
Authentication *AuthMethod `json:"authentication,omitempty"`
AuthInfoOptions *config.AuthInfoOptions `json:"authInfoOptions,omitempty"`
ContextOptions *config.ContextOptions `json:"contextOptions,omitempty"`

View File

@ -30,13 +30,7 @@ const (
func TestSetUIConfig(t *testing.T) {
conf := configs.Config{
Clusters: []configs.Cluster{
testutil.DummyClusterConfig(),
},
Plugins: []configs.Plugin{
testutil.DummyPluginWithDashboardConfig(),
testutil.DummyPluginNoDashboard(),
},
Dashboards: testutil.DummyDashboardsConfig(),
AuthMethod: testutil.DummyAuthMethodConfig(),
}

View File

@ -2,15 +2,11 @@
"authMethod": {
"url": "http://fake.auth.method.com/auth"
},
"plugins": [
"dashboards": [
{
"name": "dummy_plugin_with_dash",
"dashboard": {
"protocol": "http",
"fqdn": "localhost",
"port": 80,
"path": "index.html"
},
"name": "dummy_dashboard",
"baseURL": "http://dummyhost",
"path": "fake/login/path",
"executable": {
"autoStart": true,
"filepath": "/fake/path/to/executable",
@ -30,26 +26,11 @@
"fakevalue"
]
}
}
],
"clusters": [
},
{
"name": "dummy_cluster",
"baseFqdn": "dummy.cluster.local",
"namespaces": [
{
"name": "dummy_namespace",
"dashboards": [
{
"name": "dummy_dashboard",
"protocol": "http",
"hostname": "dummyhost",
"port": 80,
"name": "dummy_dashboard_no_exe",
"baseURL": "http://dummyhost",
"path": "fake/login/path"
}
]
}
]
}
]
}

View File

@ -2,6 +2,5 @@
"authMethod": {
"url": "http://fake.auth.method.com/auth"
},
"plugins": "",
"clusters": ""
"dashboards": [],
}

View File

@ -24,7 +24,7 @@ import (
// RunElectron executes the standalone electron app which serves up our web components
func RunElectron() error {
// determine ; or : depending on the OS
sep := string(os.PathListSeparator)
var sep = string(os.PathListSeparator)
// get the current working directory, should be the root of the airshipui tree
path, err := os.Getwd()

141
internal/webservice/proxy.go Executable file
View File

@ -0,0 +1,141 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webservice
import (
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"opendev.org/airship/airshipui/internal/configs"
)
// map of proxy targets which will be used based on the request
var proxyMap = map[string]*url.URL{}
type transport struct {
http.RoundTripper
}
var _ http.RoundTripper = &transport{}
func (t *transport) RoundTrip(request *http.Request) (response *http.Response, err error) {
// TODO: inject headers here for bearer token auth
// example:
// request.Header.Add("X-Auth-Token", "<token>")
response, err = t.RoundTripper.RoundTrip(request)
if err != nil {
return nil, err
}
// TODO: inject headers here for cookie auth
// example:
// response.Header.Add("Set-Cookie", "sessionid=<session>; expires=<date>; HttpOnly; Max-Age=3597; Path=/;")
return response, nil
}
// handle a proxy request
// this is essentially a man in the middle attack that allows us to inject headers for single sign on
func handleProxy(response http.ResponseWriter, request *http.Request) {
// retrieve the target URL from the proxy map
target := proxyMap[request.Host]
// short circuit for bad targets blowing up the backend
if target == nil {
response.WriteHeader(http.StatusInternalServerError)
if _, err := response.Write([]byte("500 - Unable to locate proxy for request!")); err != nil {
log.Println("Error writing response for proxy not found: ", err)
}
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Transport = &transport{http.DefaultTransport}
// Update the headers to allow for SSL redirection
request.URL.Host = target.Host
request.URL.Scheme = target.Scheme
host := request.Header.Get("Host")
request.Header.Set("X-Forwarded-Host", host)
request.Header.Set("X-Forwarded-For", host)
proxy.ServeHTTP(response, request)
}
func getRandomPort() (string, error) {
// get a random port for the proxy
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
return "", err
}
// close the port so we can start the proxy
defer listener.Close()
// get the string of the port
return listener.Addr().String(), nil
}
// proxyServer will proxy dashboard connections allowing us to inject headers
func proxyServer(port string) {
proxyServerMux := http.NewServeMux()
// some things may need a helping hand with the headers so we'll proxy it for them
proxyServerMux.HandleFunc("/", handleProxy)
if err := http.ListenAndServe(port, proxyServerMux); err != nil {
log.Fatal("Error starting proxy: ", err)
}
}
// helper function that kicks off all proxies prior to the start of the website
func startProxies() {
for index, dashboard := range configs.UIConfig.Dashboards {
port, err := getRandomPort()
if err != nil {
log.Fatal("Error starting proxy, unable to allocate port:", err)
}
// this will persuade the UI to use the proxy and not the original host
dashboard.IsProxied = true
// cache up the target for the proxy url
target, err := url.Parse(dashboard.BaseURL)
if err != nil {
log.Println(err)
}
// set the target for the proxied request to the original url
proxyMap[port] = target
// set the target for the link in the ui to the proxy address
dashboard.BaseURL = "http://" + port
// kick off proxy
log.Printf("Attempting to start proxy for %s on: %s\n", dashboard.Name, port)
// set the dashboard from this point on to go to the proxy
configs.UIConfig.Dashboards[index] = dashboard
// and away we go.........
go proxyServer(port)
}
}

View File

@ -22,19 +22,8 @@ import (
"time"
"opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/internal/integrations/ctl"
)
// this is a way to allow for arbitrary messages to be processed by the backend
// the message of a specifc component is shunted to that subsystem for further processing
var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
configs.AirshipUI: {
configs.Keepalive: keepaliveReply,
configs.Initialize: clientInit,
},
configs.AirshipCTL: ctl.CTLFunctionMap,
}
// semaphore to signal the UI to authenticate
var isAuthenticated bool
@ -69,6 +58,9 @@ func WebServer() {
// We can serve up static content if it's flagged as headless on command line
if configs.Headless {
// start proxies for web based use
startProxies()
// static file server
path, err := os.Getwd()
if err != nil {

View File

@ -83,5 +83,6 @@ func NewTestClient() (*websocket.Conn, error) {
}
time.Sleep(2 * time.Second)
}
return nil, err
}

View File

@ -22,6 +22,7 @@ import (
"github.com/gorilla/websocket"
"opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/internal/integrations/ctl"
)
// gorilla ws specific HTTP upgrade to WebSockets
@ -33,6 +34,16 @@ var upgrader = websocket.Upgrader{
// websocket that'll be reused by several places
var ws *websocket.Conn
// this is a way to allow for arbitrary messages to be processed by the backend
// the message of a specifc component is shunted to that subsystem for further processing
var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
configs.AirshipUI: {
configs.Keepalive: keepaliveReply,
configs.Initialize: clientInit,
},
configs.AirshipCTL: ctl.CTLFunctionMap,
}
// handle the origin request & upgrade to websocket
func onOpen(response http.ResponseWriter, request *http.Request) {
// gorilla ws will give a 403 on a cross origin request, so we silence its complaints
@ -144,8 +155,7 @@ func clientInit(configs.WsMessage) configs.WsMessage {
Type: configs.AirshipUI,
Component: configs.Initialize,
IsAuthenticated: isAuthenticated,
Dashboards: configs.UIConfig.Clusters,
Plugins: configs.UIConfig.Plugins,
Dashboards: configs.UIConfig.Dashboards,
Authentication: configs.UIConfig.AuthMethod,
}
}

View File

@ -42,13 +42,7 @@ func TestClientInit(t *testing.T) {
Type: configs.AirshipUI,
Component: configs.Initialize,
IsAuthenticated: true,
Dashboards: []configs.Cluster{
testutil.DummyClusterConfig(),
},
Plugins: []configs.Plugin{
testutil.DummyPluginWithDashboardConfig(),
testutil.DummyPluginNoDashboard(),
},
Dashboards: testutil.DummyDashboardsConfig(),
Authentication: testutil.DummyAuthMethodConfig(),
// don't fail on timestamp diff
Timestamp: response.Timestamp,
@ -75,12 +69,8 @@ func TestClientInitNoAuth(t *testing.T) {
Component: configs.Initialize,
// isAuthenticated should now be true in response
IsAuthenticated: true,
Dashboards: []configs.Cluster{
testutil.DummyClusterConfig(),
},
Plugins: []configs.Plugin{
testutil.DummyPluginWithDashboardConfig(),
testutil.DummyPluginNoDashboard(),
Dashboards: []configs.Dashboard{
testutil.DummyDashboardConfig(),
},
// don't fail on timestamp diff
Timestamp: response.Timestamp,

View File

@ -84,28 +84,40 @@ func InitConfig(t *testing.T) (conf *config.Config, configPath string,
// DummyDashboardConfig returns a populated Dashboard struct
func DummyDashboardConfig() configs.Dashboard {
e := DummyExecutableConfig()
return configs.Dashboard{
Name: "dummy_dashboard",
Protocol: "http",
Hostname: "dummyhost",
Port: 80,
BaseURL: "http://dummyhost",
Path: "fake/login/path",
Executable: e,
}
}
// DummyPluginDashboardConfig returns a populated PluginDashboard struct
func DummyPluginDashboardConfig() configs.PluginDashboard {
return configs.PluginDashboard{
Protocol: "http",
FQDN: "localhost",
Port: 80,
Path: "index.html",
// DummyDashboardsConfig returns an array of populated Dashboard structs
func DummyDashboardsConfig() []configs.Dashboard {
e := DummyExecutableConfig()
return []configs.Dashboard{
{
Name: "dummy_dashboard",
BaseURL: "http://dummyhost",
Path: "fake/login/path",
Executable: e,
},
{
Name: "dummy_plugin_no_dash",
Executable: e,
},
{
Name: "dummy_dashboard_no_exe",
BaseURL: "http://dummyhost",
Path: "fake/login/path",
},
}
}
// DummyExecutableConfig returns a populated Executable struct
func DummyExecutableConfig() configs.Executable {
return configs.Executable{
func DummyExecutableConfig() *configs.Executable {
return &configs.Executable{
AutoStart: true,
Filepath: "/fake/path/to/executable",
Args: []string{
@ -122,77 +134,21 @@ func DummyAuthMethodConfig() *configs.AuthMethod {
}
}
// DummyPluginWithDashboardConfig returns a populated Plugin struct
// with a populated PluginDashboard
func DummyPluginWithDashboardConfig() configs.Plugin {
d := DummyPluginDashboardConfig()
e := DummyExecutableConfig()
return configs.Plugin{
Name: "dummy_plugin_with_dash",
Dashboard: &d,
Executable: &e,
}
}
// DummyPluginNoDashboard returns a populated Plugin struct
// but omits the optional PluginDashboard
func DummyPluginNoDashboard() configs.Plugin {
e := DummyExecutableConfig()
return configs.Plugin{
Name: "dummy_plugin_no_dash",
Executable: &e,
}
}
// DummyNamespaceConfig returns a populated Namespace struct with
// a single Dashboard
func DummyNamespaceConfig() configs.Namespace {
d := DummyDashboardConfig()
return configs.Namespace{
Name: "dummy_namespace",
Dashboards: []configs.Dashboard{d},
}
}
// DummyClusterConfig returns a populated Cluster struct with
// a single Namespace
func DummyClusterConfig() configs.Cluster {
n := DummyNamespaceConfig()
return configs.Cluster{
Name: "dummy_cluster",
BaseFqdn: "dummy.cluster.local",
Namespaces: []configs.Namespace{n},
}
}
// DummyConfigNoAuth returns a populated Config struct but omits
// the optional AuthMethod
func DummyConfigNoAuth() configs.Config {
p := DummyPluginWithDashboardConfig()
pn := DummyPluginNoDashboard()
c := DummyClusterConfig()
d := DummyDashboardConfig()
return configs.Config{
Plugins: []configs.Plugin{p, pn},
Clusters: []configs.Cluster{c},
Dashboards: []configs.Dashboard{d},
}
}
// DummyCompleteConfig returns a fully populated Config struct
func DummyCompleteConfig() configs.Config {
a := DummyAuthMethodConfig()
p := DummyPluginWithDashboardConfig()
pn := DummyPluginNoDashboard()
c := DummyClusterConfig()
return configs.Config{
AuthMethod: a,
Plugins: []configs.Plugin{p, pn},
Clusters: []configs.Cluster{c},
AuthMethod: DummyAuthMethodConfig(),
Dashboards: DummyDashboardsConfig(),
}
}

BIN
web/favorite.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -43,14 +43,6 @@
<li><a class="c-sidebar-nav-link" id="LiDocument" onclick="ctlGetDefaults(this)">Document</a></span></li>
</ul>
</li>
<li class="c-sidebar-nav-item c-sidebar-nav-dropdown"><a
class="c-sidebar-nav-link c-sidebar-nav-dropdown-toggle" href="#">
<svg class="c-sidebar-nav-icon">
<use xlink:href="node_modules/@coreui/icons/sprites/free.svg#cil-puzzle"></use>
</svg> Plugins</a>
<ul id="PluginDropdown" class="c-sidebar-nav-dropdown-items">
</ul>
</li>
<li class="c-sidebar-nav-item c-sidebar-nav-dropdown"><a
class="c-sidebar-nav-link c-sidebar-nav-dropdown-toggle" href="#">
<svg class="c-sidebar-nav-icon">

View File

@ -36,14 +36,12 @@ function documentAction(element) { // eslint-disable-line no-unused-vars
Object.assign(json, { "subComponent": "yamlWrite" });
Object.assign(json, { "message": editorContents });
Object.assign(json, { "yaml": window.btoa(editor.getValue()) });
console.log(json);
break;
}
ws.send(JSON.stringify(json));
}
function ctlParseDocument(json) { // eslint-disable-line no-unused-vars
console.log(json["subComponent"]);
switch(json["subComponent"]) {
case "getDefaults": displayCTLInfo(json); addFolderToggles(); break;
case "yaml": insertEditor(json); break;

View File

@ -98,44 +98,18 @@ function insertGraph(data) { // eslint-disable-line no-unused-vars
}
// add dashboard links to Dropdown if present in $HOME/.airship/airshipui.json
function addServiceDashboards(json) { // eslint-disable-line no-unused-vars
function addDashboards(json) { // eslint-disable-line no-unused-vars
if (json !== undefined) {
for (let i = 0; i < json.length; i++) {
let cluster = json[i];
for (let j = 0; j < cluster.namespaces.length; j++) {
let namespace = cluster.namespaces[j];
for (let k = 0; k < namespace.dashboards.length; k++) {
let dash = namespace.dashboards[k];
let fqdn = null;
if (dash.hasOwnProperty("fqdn")) {
fqdn = `${dash.fqdn}`;
} else {
fqdn = `${dash.hostname}.${cluster.namespaces[j].name}.${cluster.baseFqdn}`;
}
let url = `${dash.protocol}://${fqdn}:${dash.port}/${dash.path || ""}`;
addDashboard("DashDropdown", dash.name, url)
}
}
let dashboard = json[i];
let url = `${dashboard.baseURL}/${dashboard.path || ""}`;
addDashboard(dashboard.name, url, dashboard.isProxied)
}
}
}
// if any plugins (external executables) have a corresponding web dashboard defined,
// add them to the dropdown
function addPluginDashboards(json) { // eslint-disable-line no-unused-vars
if (json !== undefined) {
for (let i = 0; i < json.length; i++) {
if (json[i].executable.autoStart && json[i].dashboard !== undefined) {
let dash = json[i].dashboard;
let url = `${dash.protocol}://${dash.fqdn}:${dash.port}/${dash.path || ""}`;
addDashboard("PluginDropdown", json[i].name, url);
}
}
}
}
function addDashboard(navElement, name, url) {
let nav = document.getElementById(navElement);
function addDashboard(name, url, proxied) {
let nav = document.getElementById("DashDropdown");
let li = document.createElement("li");
li.className = "c-sidebar-nav-item";
let a = document.createElement("a");
@ -143,15 +117,20 @@ function addDashboard(navElement, name, url) {
let span = document.createElement("span");
span.className = "c-sidebar-nav-icon";
a.innerText = name;
a.appendChild(span);
li.appendChild(a);
nav.appendChild(li);
if (proxied) {
a.target = "_blank";
a.href = "javascript:window.open('" + url + "')";
} else {
a.onclick = () => {
let view = document.getElementById("DashView");
view.src = url;
document.getElementById("ContentDiv").style.display = "none";
document.getElementById("DashView").style.display = "";
}
a.appendChild(span);
li.appendChild(a);
nav.appendChild(li);
}
}
function authenticate(json) { // eslint-disable-line no-unused-vars

View File

@ -28,6 +28,10 @@ function createWindow () {
// disable the default menu bar
//win.setMenu(null);
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({responseHeaders: Object.fromEntries(Object.entries(details.responseHeaders).filter(header => !/x-frame-options/i.test(header[0])))});
});
// and load the index.html of the app.
win.loadFile('index.html')

View File

@ -76,11 +76,8 @@ function hanldleAirshipUIMessages(json) {
} else {
authComplete();
}
if (json.hasOwnProperty("plugins")) {
addPluginDashboards(json["plugins"]);
}
if (json.hasOwnProperty("dashboards")) {
addServiceDashboards(json["dashboards"]);
addDashboards(json["dashboards"]);
}
} else if (json["component"] === "authcomplete") {
authComplete();

6
web/package-lock.json generated
View File

@ -667,9 +667,9 @@
}
},
"entities": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"dev": true
},
"env-paths": {

View File

@ -146,3 +146,11 @@
padding:0px;
opacity:97%;
}
/* iframe itself */
div#embedded-dashboard > iframe {
display: block;
width: 100%;
height: 100%;
border: none;
}