addition of monaco editor for online yaml & json editing

Much of this is place holders until further discussion happens

Modified the test to use a more dynamic style of test render

Change-Id: Id9324a66dcd0ad47ce20540d9aa6721747dfb703
This commit is contained in:
Schiefelbein, Andrew 2020-06-17 13:09:44 -05:00
parent afa71c0997
commit a7d68ebdbf
24 changed files with 461 additions and 275 deletions

View File

@ -18,7 +18,7 @@ NPX := $(JSLINTER_BIN)/npx
COVERAGE_OUTPUT := coverage.out
TESTFLAGS ?=
TESTFLAGS ?= -count=1
# Override the value of the version variable in main.go
LD_FLAGS= '-X opendev.org/airship/airshipui/internal/commands.version=$(GIT_VERSION)'

1
go.mod
View File

@ -12,6 +12,7 @@ require (
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.8
k8s.io/api v0.17.4
k8s.io/apimachinery v0.17.4
opendev.org/airship/airshipctl v0.0.0-20200518155418-7276dd68d8d0

View File

@ -119,6 +119,8 @@ const (
SetCredential WsSubComponentType = "credential"
GenerateISO WsSubComponentType = "generateISO"
DocPull WsSubComponentType = "docPull"
Yaml WsSubComponentType = "yaml"
YamlWrite WsSubComponentType = "yamlWrite"
)
// WsMessage is a request / return structure used for websockets
@ -130,11 +132,13 @@ type WsMessage struct {
Timestamp int64 `json:"timestamp,omitempty"`
// additional conditional components that may or may not be involved in the request / response
Error string `json:"error,omitempty"`
Fade bool `json:"fade,omitempty"`
HTML string `json:"html,omitempty"`
IsAuthenticated bool `json:"isAuthenticated,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
Fade bool `json:"fade,omitempty"`
HTML string `json:"html,omitempty"`
IsAuthenticated bool `json:"isAuthenticated,omitempty"`
Message string `json:"message,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
YAML string `json:"yaml,omitempty"`
// information related to the init of the UI
Dashboards []Cluster `json:"dashboards,omitempty"`

View File

@ -44,6 +44,8 @@ type ctlPage struct {
Version string
Disabled string
ButtonText string
YAMLTree string
YAMLHome string
}
// Client provides a library of functions that enable external programs (e.g. Airship UI) to perform airshipctl
@ -61,6 +63,9 @@ func NewClient() *Client {
settings: settings,
}
// set verbosity to true
c.settings.Debug = true
return c
}
@ -77,7 +82,7 @@ func getHTML(templateFile string, contents ctlPage) (string, error) {
var buff bytes.Buffer
// TODO: make the node path dynamic or setable at compile time
t, err := template.ParseFiles(templateFile)
t, err := template.ParseFiles(filepath.Join(basepath, templateFile))
if err != nil {
return "", err

View File

@ -16,7 +16,6 @@ package ctl
import (
"fmt"
"path/filepath"
"opendev.org/airship/airshipctl/pkg/bootstrap/isogen"
"opendev.org/airship/airshipui/internal/configs"
@ -36,7 +35,7 @@ func HandleBaremetalRequest(request configs.WsMessage) configs.WsMessage {
subComponent := request.SubComponent
switch subComponent {
case configs.GetDefaults:
response.HTML, err = getBaremetalHTML()
response.HTML, err = GetBaremetalHTML()
case configs.GenerateISO:
// since this is long running cache it up
runningRequests[subComponent] = true
@ -66,7 +65,8 @@ func (c *Client) generateIso() (string, error) {
return message, err
}
func getBaremetalHTML() (string, error) {
// GetBaremetalHTML will return the templated baremetal pagelet html
func GetBaremetalHTML() (string, error) {
p := ctlPage{
Title: "Baremetal",
Version: getAirshipCTLVersion(),
@ -78,5 +78,5 @@ func getBaremetalHTML() (string, error) {
p.ButtonText = "In Progress"
}
return getHTML(filepath.Join(basepath, "/templates/baremetal.html"), p)
return getHTML("/templates/baremetal.html", p)
}

View File

@ -15,7 +15,6 @@
package ctl
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
@ -23,12 +22,12 @@ import (
"opendev.org/airship/airshipui/internal/configs"
)
const (
testBaremetalHTML string = "testdata/baremetal.html"
)
func init() {
initCTL()
}
func TestHandleDefaultBaremetalRequest(t *testing.T) {
html, err := ioutil.ReadFile(testBaremetalHTML)
html, err := GetBaremetalHTML()
require.NoError(t, err)
request := configs.WsMessage{
@ -43,7 +42,7 @@ func TestHandleDefaultBaremetalRequest(t *testing.T) {
Type: configs.AirshipCTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
HTML: string(html),
HTML: html,
}
assert.Equal(t, expected, response)

View File

@ -16,7 +16,6 @@ package ctl
import (
"fmt"
"path/filepath"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipui/internal/configs"
@ -127,7 +126,7 @@ func getCredentialTableRows() string {
}
func getConfigHTML() (string, error) {
return getHTML(filepath.Join(basepath, "/templates/config.html"), ctlPage{
return getHTML("/templates/config.html", ctlPage{
ClusterRows: getClusterTableRows(),
ContextRows: getContextTableRows(),
CredentialRows: getCredentialTableRows(),

View File

@ -15,27 +15,26 @@
package ctl
import (
"io/ioutil"
"log"
"testing"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipui/internal/configs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/environment"
)
// TODO: Determine if this should be broken out into it's own file
const (
testConfigHTML string = "testdata/config.html"
testKubeConfig string = "testdata/kubeconfig.yaml"
testAirshipConfig string = "testdata/config.yaml"
)
func TestHandleDefaultConfigRequest(t *testing.T) {
html, err := ioutil.ReadFile(testConfigHTML)
require.NoError(t, err)
// TODO: Determine if this should be broken out into it's own file
// setup the airshipCTL env prior to running
func initCTL() {
// point airshipctl client toward test configs
c.settings = &environment.AirshipCTLSettings{
AirshipConfigPath: testAirshipConfig,
@ -43,10 +42,23 @@ func TestHandleDefaultConfigRequest(t *testing.T) {
Config: config.NewConfig(),
}
err = c.settings.Config.LoadConfig(
err := c.settings.Config.LoadConfig(
c.settings.AirshipConfigPath,
c.settings.KubeConfigPath,
)
if err != nil {
log.Fatal(err)
}
}
func init() {
initCTL()
}
func TestHandleDefaultConfigRequest(t *testing.T) {
// get the default html
html, err := getConfigHTML()
require.NoError(t, err)
// simulate incoming WsMessage from websocket client
@ -62,7 +74,7 @@ func TestHandleDefaultConfigRequest(t *testing.T) {
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.GetDefaults,
HTML: string(html),
HTML: html,
}
assert.Equal(t, expected, response)

View File

@ -15,8 +15,12 @@
package ctl
import (
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"opendev.org/airship/airshipctl/pkg/document/pull"
"opendev.org/airship/airshipui/internal/configs"
@ -34,9 +38,16 @@ func HandleDocumentRequest(request configs.WsMessage) configs.WsMessage {
var message string
switch request.SubComponent {
case configs.GetDefaults:
response.HTML, err = getDocumentHTML()
response.HTML, err = GetDocumentHTML()
response.Data = getGraphData()
case configs.DocPull:
message, err = c.docPull()
case configs.Yaml:
message = request.Message
response.YAML, err = getYaml(message)
case configs.YamlWrite:
message = request.Message
response.YAML, err = writeYaml(message, request.YAML)
default:
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
}
@ -50,6 +61,64 @@ func HandleDocumentRequest(request configs.WsMessage) configs.WsMessage {
return response
}
// network graphs have nodes and edges defined, just attempting to put some dynamically defined data in it
func getGraphData() map[string]interface{} {
return map[string]interface{}{
"nodes": []map[string]string{
{"id": "1", "label": ".airshipui"},
{"id": "2", "label": c.settings.KubeConfigPath},
{"id": "3", "label": c.settings.AirshipConfigPath},
},
"edges": []map[string]int64{
{"from": 1, "to": 2},
{"from": 1, "to": 3},
},
}
}
// getYaml reads the requested file and returns base64 encoded yaml for the front end to render
func getYaml(yamlType string) (string, error) {
yamlFile, err := os.Open(getYamlFile(yamlType))
if err != nil {
return "", err
}
defer yamlFile.Close()
// TODO: determine if this needs to be parsed as YAML as a validation effort
bytes, err := ioutil.ReadAll(yamlFile)
return base64.StdEncoding.EncodeToString(bytes), err
}
// a way to do a sanity check on the yaml passed from the frontend
func writeYaml(yamlType string, yaml64 string) (string, error) {
// base64 decode
yaml, err := base64.StdEncoding.DecodeString(yaml64)
if err != nil {
return "", err
}
// TODO: determine if we need to backup the existing before overwrite
err = ioutil.WriteFile(getYamlFile(yamlType), yaml, 0600)
if err != nil {
return "", err
}
return getYaml(yamlType)
}
func getYamlFile(yamlType string) string {
var fileName string
switch yamlType {
case "kube":
fileName = c.settings.KubeConfigPath
case "airship":
fileName = c.settings.AirshipConfigPath
}
return fileName
}
func (c *Client) docPull() (string, error) {
var message string
settings := pull.Settings{AirshipCTLSettings: c.settings}
@ -61,9 +130,31 @@ func (c *Client) docPull() (string, error) {
return message, err
}
func getDocumentHTML() (string, error) {
return getHTML(filepath.Join(basepath, "/templates/document.html"), ctlPage{
Title: "Document",
Version: getAirshipCTLVersion(),
// GetDocumentHTML will return the templated document pagelet
func GetDocumentHTML() (string, error) {
return getHTML("/templates/document.html", ctlPage{
Title: "Document",
Version: getAirshipCTLVersion(),
YAMLTree: getYamlTree(),
YAMLHome: filepath.Dir(c.settings.AirshipConfigPath),
})
}
// TODO: when we figure out what tree structure we're doing make this dynamic
// The string builder is unnecessary in an non dynamic role, so it may be needed later
func getYamlTree() string {
var s strings.Builder
s.WriteString("<li><table>" +
"<tr><td><span class=\"document\" id=\"AirshipConfigSpan\"> </span></td>" +
"<td><button id=\"AirshipConfigBtn\" class=\"unstyled-button\" onclick=\"return documentAction(this)\"> - " +
filepath.Base(c.settings.AirshipConfigPath) +
"</button></td></tr>" +
"<tr><td><span class=\"document\" id=\"KubeConfigSpan\"> </span></td>" +
"<td><button id=\"KubeConfigBtn\" class=\"unstyled-button\" onclick=\"return documentAction(this)\"> - " +
filepath.Base(c.settings.KubeConfigPath) +
"</button></td></tr>" +
"</table></li>")
return s.String()
}

View File

@ -15,7 +15,6 @@
package ctl
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
@ -23,12 +22,12 @@ import (
"opendev.org/airship/airshipui/internal/configs"
)
const (
testDocumentHTML string = "testdata/document.html"
)
func init() {
initCTL()
}
func TestHandleDefaultDocumentRequest(t *testing.T) {
html, err := ioutil.ReadFile(testDocumentHTML)
html, err := GetDocumentHTML()
require.NoError(t, err)
request := configs.WsMessage{
@ -43,7 +42,8 @@ func TestHandleDefaultDocumentRequest(t *testing.T) {
Type: configs.AirshipCTL,
Component: configs.Document,
SubComponent: configs.GetDefaults,
HTML: string(html),
HTML: html,
Data: getGraphData(),
}
assert.Equal(t, expected, response)

View File

@ -2,7 +2,6 @@
<p>Version: {{.Version}}</p>
<!-- Cluster details in accordion -->
<button class="accordion">Cluster</button>
<div class="panel">
<p>
<table class="table" id="ClusterTable">
@ -58,7 +57,6 @@
</div>
<!-- Context details in accordion -->
<button class="accordion">Context</button>
<div class="panel">
<p>
<table class="table" id="ContextTable">
@ -115,7 +113,6 @@
</div>
<!-- Credential details in accordion -->
<button class="accordion">Credential</button>
<div class="panel">
<p>
<table class="table" id="CredentialTable">
@ -158,3 +155,5 @@
</tbody>
</table>
</div>

View File

@ -1,5 +1,43 @@
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<h2>Document Pull</h2>
<button type="button" class="btn btn-info" id="DocPullBtn" onclick="documentAction(this)" style="width: 150px;">Document Pull</button>
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<!-- Tab links -->
<div class="tab">
<button class="tablinks active" onclick="tabAction(event, this)" id="DocOverviewTabBtn">Document Overview</button>
<button class="tablinks" onclick="tabAction(event, this)" id="DocPullTabBtn">Document Pull</button>
<button class="tablinks" onclick="tabAction(event, this)" id="YamlTabBtn">YAML</button>
</div>
<!-- Tab content -->
<div id="DocOverviewTab" class="tabcontent" style="display:block">
<p>
<div id="DocOverviewDiv" style="height: 60vh;overflow: hidden;border:1px solid grey"></div>
</p>
</div>
<div id="DocPullTab" class="tabcontent">
<p>
<button type="button" class="btn btn-info" id="DocPullBtn" onclick="documentAction(this)" style="width: 150px;">Document Pull</button>
</p>
</div>
<div id="YamlTab" class="tabcontent">
<p>
<table style="width:100%">
<tr>
<td>
<ul id="treeUL">
<li><span class="folder">{{.YAMLHome}}</span>
<ul class="nested">
{{.YAMLTree}}
</ul>
</li>
</ul>
</td>
<td align="right" style="vertical-align: top;">
<button type="button" class="btn btn-info" id="SaveYamlBtn" onclick="documentAction(this)" style="width: 150px;" disabled>Save</button>
</td>
</tr>
</table>
<h3>Contents</h3>
<div id="DocYamlDIV" style="height: 55vh;overflow: hidden;border:1px solid grey"></div>
</p>
</div>

View File

@ -1,5 +0,0 @@
<h1>Airship CTL Baremetal Base Information</h1>
<p>Version: devel</p>
<h2>Generate ISO</h2>
<button type="button" class="btn btn-info" id="GenIsoBtn" onclick="baremetalAction(this)" style="width: 150px;" >Generate ISO</button>

View File

@ -1,160 +0,0 @@
<h1>Airship CTL Config Base Information</h1>
<p>Version: devel</p>
<!-- Cluster details in accordion -->
<button class="accordion">Cluster</button>
<div class="panel">
<p>
<table class="table" id="ClusterTable">
<thead>
<tr>
<th scope="col">Bootstrap Info</th>
<th scope="col">Cluster Kube Conf</th>
<th scope="col">Management Configuration</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Server</th>
<th scope="col">Certificate Authority</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td><div contenteditable=true>default</div></td><td><div contenteditable=true>kubernetes_target</div></td><td><div contenteditable=true>default</div></td><td>testdata/kubeconfig.yaml</td><td><div contenteditable=true>https://10.0.0.1:6553</div></td><td><div contenteditable=true>pki/cluster-ca.pem</div></td><td><button type="button" class="btn btn-success" onclick="saveConfig(this)">Save</button></td></tr>
</tbody>
</table>
<button type="button" class="btn btn-info" id="ClusterBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the cluster add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ClusterModalTemplate" style="display:none">
<h2>Add Cluster Member</h2>
<table class="table" id="ClusterAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Server</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Context details in accordion -->
<button class="accordion">Context</button>
<div class="panel">
<p>
<table class="table" id="ContextTable">
<thead>
<tr>
<th scope="col">Context Kube Conf</th>
<th scope="col">Manifest</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td><div contenteditable=true>kubernetes_target</div></td><td><div contenteditable=true></div></td><td>testdata/kubeconfig.yaml</td><td><div contenteditable=true>kubernetes_target</div></td><td><div contenteditable=true>admin</div></td><td><button type="button" class="btn btn-success" onclick="saveConfig(this)">Save</button></td></tr>
</tbody>
</table>
</p>
<button type="button" class="btn btn-info" id="ContextBtn" onclick="addConfigModal(this)">Add</button>
</div>
<!-- This is used by the context add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ContextModalTemplate" style="display:none">
<h2>Add Context</h2>
<table class="table" id="ContextAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Credential details in accordion -->
<button class="accordion">Credential</button>
<div class="panel">
<p>
<table class="table" id="CredentialTable">
<thead>
<tr>
<th scope="col">Location Of Origin</th>
<th scope="col">Username</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="btn btn-info" id="CredentialBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the credential add modal and is injected into the DOM but not displayed except by the popup -->
<div id="CredentialModalTemplate" style="display:none">
<h2>Add Credential</h2>
<table class="table" id="CredentialAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,5 +0,0 @@
<h1>Airship CTL Document Base Information</h1>
<p>Version: devel</p>
<h2>Document Pull</h2>
<button type="button" class="btn btn-info" id="DocPullBtn" onclick="documentAction(this)" style="width: 150px;">Document Pull</button>

View File

@ -16,13 +16,13 @@ package webservice
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
"opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/internal/integrations/ctl"
"opendev.org/airship/airshipui/testutil"
"github.com/gorilla/websocket"
@ -31,9 +31,7 @@ import (
)
const (
serverAddr string = "localhost:8080"
testBaremetalHTML string = "../integrations/ctl/testdata/baremetal.html"
testDocumentHTML string = "../integrations/ctl/testdata/document.html"
serverAddr string = "localhost:8080"
// client messages
initialize string = `{"type":"electron","component":"initialize"}`
@ -201,7 +199,7 @@ func TestHandleDocumentRequest(t *testing.T) {
require.NoError(t, err)
defer client.Close()
expectedHTML, err := ioutil.ReadFile(testDocumentHTML)
expectedHTML, err := ctl.GetDocumentHTML()
require.NoError(t, err)
response, err := getResponse(client, document)
@ -211,12 +209,16 @@ func TestHandleDocumentRequest(t *testing.T) {
Type: configs.AirshipCTL,
Component: configs.Document,
SubComponent: configs.GetDefaults,
HTML: string(expectedHTML),
HTML: expectedHTML,
// don't fail on timestamp diff
Timestamp: response.Timestamp,
}
assert.Equal(t, expected, response)
// the non typed interface requires us to break up the checking otherwise the 2 will never be equal
assert.Equal(t, expected.Type, response.Type)
assert.Equal(t, expected.Component, response.Component)
assert.Equal(t, expected.SubComponent, response.SubComponent)
assert.Equal(t, expected.HTML, response.HTML)
}
func TestHandleBaremetalRequest(t *testing.T) {
@ -224,7 +226,7 @@ func TestHandleBaremetalRequest(t *testing.T) {
require.NoError(t, err)
defer client.Close()
expectedHTML, err := ioutil.ReadFile(testBaremetalHTML)
expectedHTML, err := ctl.GetBaremetalHTML()
require.NoError(t, err)
response, err := getResponse(client, baremetal)
@ -234,7 +236,7 @@ func TestHandleBaremetalRequest(t *testing.T) {
Type: configs.AirshipCTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
HTML: string(expectedHTML),
HTML: expectedHTML,
// don't fail on timestamp diff
Timestamp: response.Timestamp,
}

View File

@ -14,7 +14,9 @@
package testutil
import "opendev.org/airship/airshipui/internal/configs"
import (
"opendev.org/airship/airshipui/internal/configs"
)
// DummyDashboardConfig returns a populated Dashboard struct
func DummyDashboardConfig() configs.Dashboard {

View File

@ -14,6 +14,7 @@
<script src="js/common.js"></script>
<script src="js/airshipctl/airshipctl.js"></script>
<script src="js/coreui-3.2.0/vendors/@coreui/coreui/js/coreui.bundle.min.js"></script>
<script type="text/javascript" src="node_modules/vis-network/standalone/umd/vis-network.min.js"></script>
<link href="js/coreui-3.2.0/css/style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
@ -110,6 +111,11 @@
</div>
</div>
<webview class="webview" id="AuthView" autosize="on" style="height:92vh;"></webview>
</body>
<!-- For some reason monaco includes can't live in the head of html because they rely on some post content loaded listener -->
<script src="node_modules/monaco-editor/min/vs/loader.js"></script>
<script>require.config( { paths: { "vs": "node_modules/monaco-editor/min/vs" } } );</script>
<script src="node_modules/monaco-editor/min/vs/editor/editor.main.nls.js"></script>
<script src="node_modules/monaco-editor/min/vs/editor/editor.main.js"></script>
</body>
</html>

View File

@ -53,6 +53,9 @@ function displayCTLInfo(json) { // eslint-disable-line no-unused-vars
let div = document.getElementById("ContentDiv");
div.style.display = "";
div.innerHTML = json["html"];
if (!! document.getElementById("DocOverviewDiv") && json.hasOwnProperty("data")) {
insertGraph(json["data"]);
}
} else {
if (json.hasOwnProperty("error")) {
showDismissableAlert("danger", json["error"], false);

View File

@ -12,6 +12,9 @@
limitations under the License.
*/
var editor = null;
var editorContents = null;
function documentAction(element) { // eslint-disable-line no-unused-vars
let elementId = element.id;
@ -21,14 +24,88 @@ function documentAction(element) { // eslint-disable-line no-unused-vars
var json = { "type": "airshipctl", "component": "document" };
switch(elementId) {
case "DocPullBtn": Object.assign(json, { "subComponent": "docPull" }); break;
case "KubeConfigBtn":
Object.assign(json, { "subComponent": "yaml" });
Object.assign(json, { "message": "kube" });
break;
case "AirshipConfigBtn":
Object.assign(json, { "subComponent": "yaml" });
Object.assign(json, { "message": "airship" });
break;
case "SaveYamlBtn":
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); break;
case "getDefaults": displayCTLInfo(json); addFolderToggles(); break;
case "yaml": insertEditor(json); break;
case "yamlWrite": insertEditor(json); buttonHelper("SaveYamlBtn", "Save", true); break;
case "docPull": buttonHelper("DocPullBtn", "Document Pull",false); handleCTLResponse(json); break;
default: handleCTLResponse(json)
}
}
// adds the monaco editor to the UI and populates it with yaml
function insertEditor(json) {
// dispose of any detritus that may not have been disposed of before reuse
if (editor !== null) { editor.dispose(); editorContents = null; }
// disable the save button if it's not already
let saveBtn = document.getElementById("SaveYamlBtn");
saveBtn.disabled = true;
// create and populate the monaco editor
let div = document.getElementById("DocYamlDIV");
editor = monaco.editor.create(div, {
value: window.atob(json["yaml"]),
language: "yaml",
automaticLayout: true
});
toggleDocument();
// toggle the buttons back to the original message
switch(json["message"]) {
case "kube":
buttonHelper("KubeConfigBtn", " - kubeconfig", false);
editorContents = "kube";
document.getElementById("KubeConfigSpan").classList.toggle("document-open");
break;
case "airship":
buttonHelper("AirshipConfigBtn", " - config", false);
editorContents = "airship";
document.getElementById("AirshipConfigSpan").classList.toggle("document-open");
break;
}
// on change event for the editor
editor.onDidChangeModelContent(() => {
saveBtn.disabled = false;
});
}
function addFolderToggles() {
var toggler = document.getElementsByClassName("folder");
for (let i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function() {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("folder-open");
});
}
}
function toggleDocument() {
var toggler = document.getElementsByClassName("document");
for (let i = 0; i < toggler.length; i++) {
toggler[i].className = "document";
}
}

View File

@ -12,6 +12,8 @@
limitations under the License.
*/
var graph = null;
// add the footer and header when the page loads
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", function () {
@ -32,6 +34,69 @@ if (document.addEventListener) {
}, false);
}
function tabAction(event, element) { // eslint-disable-line no-unused-vars
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
let id = String(element.id);
let div = id.replace("Btn","");
switch (id) {
case "DocOverviewTabBtn": document.getElementById(div).style.display = "block"; break;
case "DocPullTabBtn": document.getElementById(div).style.display = "block"; break;
case "YamlTabBtn": document.getElementById(div).style.display = "block"; break;
}
event.currentTarget.className += " active";
}
function insertGraph(data) { // eslint-disable-line no-unused-vars
if (graph !== null) { graph.destroy(); }
// create a network
var container = document.getElementById("DocOverviewDiv");
// TODO: extract these to a constants file somewhere
var options = {
nodes: {
shape: "box",
scaling: {
max: 200, min: 100
}
},
physics: {
forceAtlas2Based: {
gravitationalConstant: -26,
centralGravity: 0.005,
springLength: 230,
springConstant: 0.18,
avoidOverlap: 1.5
},
maxVelocity: 146,
solver: "forceAtlas2Based",
timestep: 0.35,
stabilization: {
enabled: true,
iterations: 1000,
updateInterval: 25
}
}
};
graph = new vis.Network(container, data, options);
}
// add dashboard links to Dropdown if present in $HOME/.airship/airshipui.json
function addServiceDashboards(json) { // eslint-disable-line no-unused-vars
if (json !== undefined) {
@ -154,21 +219,4 @@ function alertFadeOut(id) { // eslint-disable-line no-unused-vars
element.addEventListener("transitionend", function() {
element.parentNode.removeChild(element);
});
}
function enableAccordion() { // eslint-disable-line no-unused-vars
var acc = document.getElementsByClassName("accordion");
var i;
for (i = 0; i < acc.length; i++) {
acc[i].addEventListener("click", function () {
this.classList.toggle("active");
var panel = this.nextElementSibling;
if (panel.style.maxHeight) {
panel.style.maxHeight = null;
} else {
panel.style.maxHeight = panel.scrollHeight + "px";
}
});
}
}

10
web/package-lock.json generated
View File

@ -1763,6 +1763,11 @@
"minimist": "^1.2.5"
}
},
"monaco-editor": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz",
"integrity": "sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -2762,6 +2767,11 @@
"extsprintf": "^1.2.0"
}
},
"vis-network": {
"version": "7.6.10",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-7.6.10.tgz",
"integrity": "sha512-wL1dHBWWpzxvUaM0miccDuSLQ2tkw93jCA3j4Zizh4ruph+UXnjkouayaOyJIx43wULUSoKGWkhE6na1q208TA=="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",

View File

@ -15,7 +15,9 @@
},
"dependencies": {
"electron-json-config": "^1.5.3",
"monaco-editor": "^0.20.0",
"node-sass": "^4.14.0",
"vis-network": "^7.6.10",
"xmlhttprequest": "^1.8.0"
}
}

View File

@ -42,40 +42,98 @@
}
}
/* accordion taken from w3schools, https://www.w3schools.com/howto/howto_js_accordion.asp */
.accordion {
background-color: #eee;
color: #444;
cursor: pointer;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
transition: 0.4s;
/** tab styles taken from w3schools.com https://www.w3schools.com/howto/howto_js_tabs.asp **/
/* Style the tab */
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
}
.active, .accordion:hover {
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
.accordion:after {
content: '\002B';
color: #777;
font-weight: bold;
float: right;
margin-left: 5px;
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border-top: none;
}
.active:after {
content: "\2212";
/* tree view taken from w3scools.com https://www.w3schools.com/howto/howto_js_treeview.asp */
/* Remove default bullets */
ul, #treeUL {
list-style-type: none;
}
.panel {
padding: 0 18px;
background-color: white;
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
/* Remove margins and padding from the parent ul */
#treeUL {
margin: 0;
padding: 0;
}
/* Style the folder/arrow */
.folder {
cursor: pointer;
user-select: none; /* Prevent text selection */
}
/* Create the folder/arrow with a unicode, and style it */
.folder::before {
content: "\1F5C0";
display: inline-block;
margin-right: 6px;
font-size:2em;
}
/* Rotate the folder/arrow icon when clicked on (using JavaScript) */
.folder-open::before {
content: "\1F5C1";
font-size:2em;
}
/* Hide the nested list */
.nested {
display: none;
}
/* Show the nested list when the user clicks on the folder/arrow (with JavaScript) */
.active {
display: block;
}
/* for use with the yaml tree view */
.unstyled-button {
border: none;
padding: 0;
background: none;
}
.document::before {
content: "\1F5B9";
font-size: 1.5em;
}
.document-open::before {
content: "\1F5CE";
font-size: 1.5em;
color:darkcyan;"
}