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:
parent
afa71c0997
commit
a7d68ebdbf
2
Makefile
2
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)'
|
||||
|
1
go.mod
1
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
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
160
internal/integrations/ctl/testdata/config.html
vendored
160
internal/integrations/ctl/testdata/config.html
vendored
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -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>
|
@ -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,
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
@ -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);
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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
10
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
114
web/style.css
114
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;
|
||||
}
|
||||
/* 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;"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user