From a7d68ebdbf8d3587b15f015b84aee66f20b65823 Mon Sep 17 00:00:00 2001 From: "Schiefelbein, Andrew" Date: Wed, 17 Jun 2020 13:09:44 -0500 Subject: [PATCH] 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 --- Makefile | 2 +- go.mod | 1 + internal/configs/configs.go | 14 +- internal/integrations/ctl/airshipctl.go | 7 +- internal/integrations/ctl/baremetal.go | 8 +- internal/integrations/ctl/baremetal_test.go | 11 +- internal/integrations/ctl/config.go | 3 +- internal/integrations/ctl/config_test.go | 32 ++-- internal/integrations/ctl/document.go | 101 ++++++++++- internal/integrations/ctl/document_test.go | 12 +- .../integrations/ctl/templates/config.html | 5 +- .../integrations/ctl/templates/document.html | 48 +++++- .../integrations/ctl/testdata/baremetal.html | 5 - .../integrations/ctl/testdata/config.html | 160 ------------------ .../integrations/ctl/testdata/document.html | 5 - internal/webservice/server_test.go | 20 ++- testutil/testconfig.go | 4 +- web/index.html | 8 +- web/js/airshipctl/airshipctl.js | 3 + web/js/airshipctl/document.js | 79 ++++++++- web/js/common.js | 82 +++++++-- web/package-lock.json | 10 ++ web/package.json | 2 + web/style.css | 114 ++++++++++--- 24 files changed, 461 insertions(+), 275 deletions(-) delete mode 100644 internal/integrations/ctl/testdata/baremetal.html delete mode 100644 internal/integrations/ctl/testdata/config.html delete mode 100644 internal/integrations/ctl/testdata/document.html diff --git a/Makefile b/Makefile index d625db7..1f55a2f 100644 --- a/Makefile +++ b/Makefile @@ -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)' diff --git a/go.mod b/go.mod index 545568e..e8f876e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/configs/configs.go b/internal/configs/configs.go index 43c554c..140d86a 100755 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -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"` diff --git a/internal/integrations/ctl/airshipctl.go b/internal/integrations/ctl/airshipctl.go index 1f38217..e3bc18d 100755 --- a/internal/integrations/ctl/airshipctl.go +++ b/internal/integrations/ctl/airshipctl.go @@ -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 diff --git a/internal/integrations/ctl/baremetal.go b/internal/integrations/ctl/baremetal.go index c19073b..f2df6c4 100755 --- a/internal/integrations/ctl/baremetal.go +++ b/internal/integrations/ctl/baremetal.go @@ -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) } diff --git a/internal/integrations/ctl/baremetal_test.go b/internal/integrations/ctl/baremetal_test.go index 9d5a509..53dfd71 100644 --- a/internal/integrations/ctl/baremetal_test.go +++ b/internal/integrations/ctl/baremetal_test.go @@ -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) diff --git a/internal/integrations/ctl/config.go b/internal/integrations/ctl/config.go index 2c49025..96a0f49 100755 --- a/internal/integrations/ctl/config.go +++ b/internal/integrations/ctl/config.go @@ -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(), diff --git a/internal/integrations/ctl/config_test.go b/internal/integrations/ctl/config_test.go index 2798a7b..ad38b51 100644 --- a/internal/integrations/ctl/config_test.go +++ b/internal/integrations/ctl/config_test.go @@ -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) diff --git a/internal/integrations/ctl/document.go b/internal/integrations/ctl/document.go index 881cb26..997f7e7 100755 --- a/internal/integrations/ctl/document.go +++ b/internal/integrations/ctl/document.go @@ -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("
  • " + + "" + + "" + + "" + + "" + + "
  • ") + + return s.String() +} diff --git a/internal/integrations/ctl/document_test.go b/internal/integrations/ctl/document_test.go index 3f4a779..f0c81fc 100644 --- a/internal/integrations/ctl/document_test.go +++ b/internal/integrations/ctl/document_test.go @@ -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) diff --git a/internal/integrations/ctl/templates/config.html b/internal/integrations/ctl/templates/config.html index 05fe6f3..54caf10 100755 --- a/internal/integrations/ctl/templates/config.html +++ b/internal/integrations/ctl/templates/config.html @@ -2,7 +2,6 @@

    Version: {{.Version}}

    -

    @@ -58,7 +57,6 @@ -

    @@ -115,7 +113,6 @@ -

    @@ -158,3 +155,5 @@

    + + diff --git a/internal/integrations/ctl/templates/document.html b/internal/integrations/ctl/templates/document.html index 1d2d60b..4c6e148 100755 --- a/internal/integrations/ctl/templates/document.html +++ b/internal/integrations/ctl/templates/document.html @@ -1,5 +1,43 @@ -

    Airship CTL {{.Title}} Base Information

    -

    Version: {{.Version}}

    - -

    Document Pull

    - +

    Airship CTL {{.Title}} Base Information

    +

    Version: {{.Version}}

    + + +
    + + + +
    + + +
    +

    +

    +

    +
    +
    +

    + +

    +
    +
    +

    + + + + + +
    +
      +
    • {{.YAMLHome}} +
        + {{.YAMLTree}} +
      +
    • +
    +
    + +
    +

    Contents

    +
    +

    +
    diff --git a/internal/integrations/ctl/testdata/baremetal.html b/internal/integrations/ctl/testdata/baremetal.html deleted file mode 100644 index cd08bdd..0000000 --- a/internal/integrations/ctl/testdata/baremetal.html +++ /dev/null @@ -1,5 +0,0 @@ -

    Airship CTL Baremetal Base Information

    -

    Version: devel

    - -

    Generate ISO

    - diff --git a/internal/integrations/ctl/testdata/config.html b/internal/integrations/ctl/testdata/config.html deleted file mode 100644 index 914ac31..0000000 --- a/internal/integrations/ctl/testdata/config.html +++ /dev/null @@ -1,160 +0,0 @@ -

    Airship CTL Config Base Information

    -

    Version: devel

    - - - -
    -

    - - - - - - - - - - - - - - - -
    Bootstrap InfoCluster Kube ConfManagement ConfigurationLocation Of OriginServerCertificate Authority
    default
    kubernetes_target
    default
    testdata/kubeconfig.yaml
    https://10.0.0.1:6553
    pki/cluster-ca.pem
    - -

    -
    - - - - - - -
    -

    - - - - - - - - - - - - - - -
    Context Kube ConfManifestLocation Of OriginClusterUser
    kubernetes_target
    testdata/kubeconfig.yaml
    kubernetes_target
    admin
    -

    - -
    - - - - - - -
    -

    - - - - - - - - - - - -
    Location Of OriginUsername
    - -

    -
    - - - diff --git a/internal/integrations/ctl/testdata/document.html b/internal/integrations/ctl/testdata/document.html deleted file mode 100644 index a8282e8..0000000 --- a/internal/integrations/ctl/testdata/document.html +++ /dev/null @@ -1,5 +0,0 @@ -

    Airship CTL Document Base Information

    -

    Version: devel

    - -

    Document Pull

    - diff --git a/internal/webservice/server_test.go b/internal/webservice/server_test.go index 89036f7..801b8d0 100644 --- a/internal/webservice/server_test.go +++ b/internal/webservice/server_test.go @@ -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, } diff --git a/testutil/testconfig.go b/testutil/testconfig.go index f56ccaf..a4bb9fe 100644 --- a/testutil/testconfig.go +++ b/testutil/testconfig.go @@ -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 { diff --git a/web/index.html b/web/index.html index 8df2d13..637fb1c 100755 --- a/web/index.html +++ b/web/index.html @@ -14,6 +14,7 @@ + @@ -110,6 +111,11 @@ - + + + + + + \ No newline at end of file diff --git a/web/js/airshipctl/airshipctl.js b/web/js/airshipctl/airshipctl.js index 0d279bf..bdeff99 100755 --- a/web/js/airshipctl/airshipctl.js +++ b/web/js/airshipctl/airshipctl.js @@ -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); diff --git a/web/js/airshipctl/document.js b/web/js/airshipctl/document.js index d43cf08..5dc84a7 100755 --- a/web/js/airshipctl/document.js +++ b/web/js/airshipctl/document.js @@ -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"; + } +} diff --git a/web/js/common.js b/web/js/common.js index 312032b..aad4ea4 100755 --- a/web/js/common.js +++ b/web/js/common.js @@ -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"; - } - }); - } } \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 99f968b..554c82f 100755 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 775f668..d437c59 100755 --- a/web/package.json +++ b/web/package.json @@ -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" } } diff --git a/web/style.css b/web/style.css index 6d53d60..10ae7e3 100644 --- a/web/style.css +++ b/web/style.css @@ -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; -} \ No newline at end of file +/* 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;" +}