diff --git a/internal/commands/root.go b/internal/commands/root.go index 894afc6..f03d61c 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -16,6 +16,7 @@ package commands import ( "context" + "fmt" "log" "os" "os/signal" @@ -76,6 +77,13 @@ func launch(cmd *cobra.Command, args []string) { } } else { log.Printf("config %s", err) + webservice.Alerts = append( + webservice.Alerts, + webservice.Alert{ + Level: "info", + Message: fmt.Sprintf("%s", err), + }, + ) } // start the electron app diff --git a/internal/webservice/server.go b/internal/webservice/server.go index 39a6e18..e6a7443 100755 --- a/internal/webservice/server.go +++ b/internal/webservice/server.go @@ -32,6 +32,14 @@ type wsRequest struct { Data map[string]interface{} `json:"data"` } +// Alert basic structure to hold alert messages to pass to the UI +type Alert struct { + Level string + Message string +} + +var Alerts []Alert + // gorilla ws specific HTTP upgrade to WebSockets var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, @@ -70,6 +78,14 @@ func onOpen(w http.ResponseWriter, r *http.Request) { ws = wsConn log.Printf("WebSocket established with %s\n", ws.RemoteAddr().String()) + + // send any initialization alerts to UI and clear the queue + for len(Alerts) > 0 { + sendAlert(Alerts[0].Level, Alerts[0].Message) + Alerts[0] = Alert{} + Alerts = Alerts[1:] + } + go onMessage() } @@ -151,6 +167,20 @@ func handleAuth(w http.ResponseWriter, r *http.Request) { } } +// SendAlert sends an alert message to the frontend handler +// to display alerts in the UI itself +func sendAlert(alertLvl string, msg string) { + if err := ws.WriteJSON(map[string]interface{}{ + "type": "electron", + "component": "alert", + "level": alertLvl, + "message": msg, + "timestamp": time.Now().UnixNano() / 1000000, + }); err != nil { + onError(err) + } +} + // WebServer will run the handler functions for WebSockets // TODO: potentially add in the ability to serve static content func WebServer() { diff --git a/web/css/spinner.css b/web/css/spinner.css new file mode 100644 index 0000000..a232e95 --- /dev/null +++ b/web/css/spinner.css @@ -0,0 +1,43 @@ +/* loading spinner CSS taken from SpinKit, http://tobiasahlin.com/spinkit/ */ + +.spinner { + margin: 100px auto 0; + width: 70px; + text-align: center; +} + +.spinner > div { + width: 18px; + height: 18px; + background-color: #333; + + border-radius: 100%; + display: inline-block; + -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; + animation: sk-bouncedelay 1.4s infinite ease-in-out both; +} + +.spinner .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; +} + +.spinner .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; +} + +@-webkit-keyframes sk-bouncedelay { + 0%, 80%, 100% { -webkit-transform: scale(0) } + 40% { -webkit-transform: scale(1.0) } +} + +@keyframes sk-bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0); + transform: scale(0); + } 40% { + -webkit-transform: scale(1.0); + transform: scale(1.0); + } +} \ No newline at end of file diff --git a/web/images/Airship_Icon_2Color_RGB.svg b/web/images/Airship_Icon_2Color_RGB.svg new file mode 100644 index 0000000..b77e172 --- /dev/null +++ b/web/images/Airship_Icon_2Color_RGB.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/web/images/Airship_Logo_Horizontal_2Color_RGB.svg b/web/images/Airship_Logo_Horizontal_2Color_RGB.svg new file mode 100644 index 0000000..0065cf4 --- /dev/null +++ b/web/images/Airship_Logo_Horizontal_2Color_RGB.svg @@ -0,0 +1 @@ +Airship_Logo_Horizontal_2Color_RGB \ No newline at end of file diff --git a/web/images/avatar.jpg b/web/images/avatar.jpg new file mode 100644 index 0000000..6f22565 Binary files /dev/null and b/web/images/avatar.jpg differ diff --git a/web/index.html b/web/index.html index 3d8b6fb..fddbad9 100755 --- a/web/index.html +++ b/web/index.html @@ -1,39 +1,111 @@ - - - - - - Airship UI - - - - - - - - - -
-
- -
- - - + + + + + + Airship UI + + + + + + + + + + + + + +
+ +
+
+ + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ +
+
+
+ \ No newline at end of file diff --git a/web/js/common.js b/web/js/common.js index f980366..5c6e1a8 100755 --- a/web/js/common.js +++ b/web/js/common.js @@ -38,12 +38,14 @@ function addServiceDashboards(json) { // eslint-disable-line no-unused-vars let namespace = cluster.namespaces[j]; for (let k = 0; k < namespace.dashboards.length; k++) { let dash = namespace.dashboards[k]; - let { fqdn } = dash.fqdn; - if (fqdn === undefined || fqdn === "") { + let fqdn = ""; + if (dash.fqdn === undefined) { fqdn = `${dash.hostname}.${cluster.namespaces[j].name}.${cluster.baseFqdn}` + } else { + ({ fqdn } = dash.fqdn); } let url = `${dash.protocol}://${fqdn}:${dash.port}/${dash.path}`; - addDashboard("PluginDropdown", dash.name, url) + addDashboard("DashDropdown", dash.name, url) } } } @@ -53,7 +55,7 @@ function addServiceDashboards(json) { // eslint-disable-line no-unused-vars // add them to the dropdown function addPluginDashboards(json) { // eslint-disable-line no-unused-vars for (let i = 0; i < json.length; i++) { - if (json[i].executable.autoStart && json[i].dashboard !== undefined) { + if (json[i].executable.autoStart && json[i].dashboard.fqdn !== "") { let dash = json[i].dashboard; let url = `${dash.protocol}://${dash.fqdn}:${dash.port}/${dash.path}`; addDashboard("PluginDropdown", json[i].name, url) @@ -63,15 +65,21 @@ function addPluginDashboards(json) { // eslint-disable-line no-unused-vars function addDashboard(navElement, name, url) { let nav = document.getElementById(navElement); + let li = document.createElement("li"); + li.className = "c-sidebar-nav-item"; let a = document.createElement("a"); + a.className = "c-sidebar-nav-link"; + let span = document.createElement("span"); + span.className = "c-sidebar-nav-icon"; a.innerText = name; a.onclick = () => { let view = document.getElementById("DashView"); view.src = url; - document.getElementById("MainDiv").style.display = "none"; document.getElementById("DashView").style.display = ""; } - nav.appendChild(a); + a.appendChild(span); + li.appendChild(a); + nav.appendChild(li); } function authenticate(json) { // eslint-disable-line no-unused-vars @@ -87,4 +95,41 @@ function removeElement(id) { // eslint-disable-line no-unused-vars if (document.contains(document.getElementById(id))) { document.getElementById(id).remove(); } +} + +function showDismissableAlert(alertLevel, msg) { // eslint-disable-line no-unused-vars + let e = document.getElementById("alert-div"); + let alertHeading = ""; + + switch (alertLevel) { + case "danger": + alertHeading = "Error"; + break; + case "warning": + alertHeading = "Warning"; + break; + default: + alertHeading = "Info"; + } + + let div = document.createElement("div"); + div.className = `alert alert-${alertLevel} alert-dismissable fade show`; + div.setAttribute("role", "alert"); + div.innerHTML = `${alertHeading}: ${msg}`; + + // dismissable button + let btn = document.createElement("button"); + btn.className = "close"; + btn.type = "button"; + btn.setAttribute("data-dismiss", "alert"); + btn.setAttribute("aria-label", "Close"); + + let span = document.createElement("span"); + span.setAttribute("aria-hidden", "true"); + span.innerText = "×"; + + btn.appendChild(span); + div.appendChild(btn); + + e.appendChild(div); } \ No newline at end of file diff --git a/web/js/coreui-3.2.0/css/style.css b/web/js/coreui-3.2.0/css/style.css index 88ebeb4..64d47bd 100644 --- a/web/js/coreui-3.2.0/css/style.css +++ b/web/js/coreui-3.2.0/css/style.css @@ -9802,7 +9802,7 @@ html:not([dir="rtl"]) .c-sidebar.c-sidebar-show.c-sidebar-right, html:not([dir=" } .c-sidebar .c-sidebar-brand,.c-sidebar .c-sidebar-header { - background: rgba(0, 0, 21, 0.2); + background: rgb(226, 226, 241); } .c-sidebar .c-sidebar-form .c-form-control { @@ -11879,7 +11879,6 @@ html:not([dir="rtl"]) .list-inline { -ms-flex-positive: 1; flex-grow: 1; min-width: 0; - padding-top: 2rem; } @media (min-width: 768px) { diff --git a/web/js/websocket.js b/web/js/websocket.js index b456f25..c0eb6bc 100755 --- a/web/js/websocket.js +++ b/web/js/websocket.js @@ -68,11 +68,16 @@ function handleMessages(message) { } else { authComplete(); } - addPluginDashboards(json["plugins"]); - addServiceDashboards(json["dashboards"]); - authenticate(json["authentication"]); + if (json["plugins"] !== null) { + addPluginDashboards(json["plugins"]); + } + if (json["dashboards"] !== null) { + addServiceDashboards(json["dashboards"]); + } } else if (json["component"] === "authcomplete") { authComplete(); + } else if (json["component"] === "alert") { + showDismissableAlert(json["level"], json["message"]); } } else { // TODO: determine if we're dispatching events or just doing function calls @@ -115,10 +120,7 @@ function close(code) { } function authComplete() { - document.getElementById("HeaderDiv").style.display = ""; document.getElementById("MainDiv").style.display = ""; - document.getElementById("DashView").style.display = "none"; - document.getElementById("FooterDiv").style.display = ""; } function keepAlive() {