Add host status plugin
- Create WebSocket and get host state changes from server
- Changed webpack devServer to https to allow for
secure WebSocket creation (wss)
- Updates to AppHeader to visually indicate changes
in host state
- Cleaned up api.js file
- Check if user is logged in when creating WebSocket
- Adds check if user is already authenticated so WebSocket
is created when browser refreshed.
- Add appliation header styles
- Add sass loader config changes to allow sass variables to
be used in single file components
URL must use https protocol when running locally or the page
will not load.
Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Signed-off-by: Derick Montague <derick.montague@ibm.com>
Change-Id: I35e89bdc09e1aa35a6215ef952409a8ed16dd9e1
diff --git a/src/assets/styles/_colors.scss b/src/assets/styles/_colors.scss
index 0435123..4a8b62c 100644
--- a/src/assets/styles/_colors.scss
+++ b/src/assets/styles/_colors.scss
@@ -91,7 +91,7 @@
$warning: $yellow;
$danger: $red;
$light: $gray-100;
-$dark: $gray-800;
+$dark: $black;
// Bootstrap will generate CSS variables for
// all of the colors in this map.
diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue
index 7974f70..244eeb3 100644
--- a/src/components/AppHeader/AppHeader.vue
+++ b/src/components/AppHeader/AppHeader.vue
@@ -1,49 +1,32 @@
<template>
<div>
- <a class="link-skip-nav btn btn-light" href="#main-content"
- >Skip to content</a
- >
+ <a class="link-skip-nav btn btn-light" href="#main-content">
+ Skip to content
+ </a>
<header id="page-header">
<b-navbar toggleable="lg" variant="dark" type="dark">
- <b-navbar-nav small>
+ <!-- Left aligned nav items -->
+ <b-navbar-nav>
<b-nav-text>BMC System Management</b-nav-text>
</b-navbar-nav>
- <b-navbar-nav small class="ml-auto">
- <b-nav-item @click="logout">
- <user-avatar-20 />
- Logout
- </b-nav-item>
- </b-navbar-nav>
- </b-navbar>
- <b-navbar toggleable="lg" variant="light">
- <b-navbar-nav>
- <b-navbar-brand href="/">
- {{ orgName }}
- </b-navbar-brand>
- </b-navbar-nav>
- <b-navbar-nav>
- <b-nav-text>{{ hostName }}</b-nav-text>
- <b-nav-text>{{ ipAddress }}</b-nav-text>
- </b-navbar-nav>
+ <!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-nav>
<b-nav-item>
- <b-button variant="link">
- Server health
- <b-badge pill variant="danger">Critical</b-badge>
- </b-button>
+ Health
+ <status-icon :status="'danger'" />
</b-nav-item>
<b-nav-item>
- <b-button variant="link">
- Server power
- <b-badge pill variant="success">Running</b-badge>
- </b-button>
+ Power
+ <status-icon :status="hostStatusIcon" />
</b-nav-item>
<b-nav-item>
- <b-button variant="link">
- <Renew20 />
- Refresh Data
- </b-button>
+ Refresh
+ <icon-renew />
+ </b-nav-item>
+ <b-nav-item @click="logout">
+ Logout
+ <icon-avatar />
</b-nav-item>
</b-nav>
</b-navbar-nav>
@@ -53,32 +36,34 @@
</template>
<script>
-import UserAvatar20 from "@carbon/icons-vue/es/user--avatar/20";
-import Renew20 from "@carbon/icons-vue/es/renew/20";
+import IconAvatar from "@carbon/icons-vue/es/user--avatar/20";
+import IconRenew from "@carbon/icons-vue/es/renew/20";
+import StatusIcon from "../Global/StatusIcon";
export default {
name: "AppHeader",
- components: { Renew20, UserAvatar20 },
+ components: { IconAvatar, IconRenew, StatusIcon },
created() {
this.getHostInfo();
},
- data() {
- return {
- orgName: "OpenBMC",
- serverName: "Server Name",
- ipAddress: "127.0.0.0"
- };
- },
computed: {
- hostName() {
- return this.$store.getters["global/hostName"];
- },
hostStatus() {
return this.$store.getters["global/hostStatus"];
+ },
+ hostStatusIcon() {
+ switch (this.hostStatus) {
+ case "on":
+ return "success";
+ case "error":
+ return "danger";
+ case "off":
+ default:
+ return "secondary";
+ }
}
},
methods: {
getHostInfo() {
- this.$store.dispatch("global/getHostName");
+ this.$store.dispatch("global/getHostStatus");
},
logout() {
this.$store.dispatch("authentication/logout").then(() => {
@@ -90,20 +75,20 @@
</script>
<style lang="scss" scoped>
-.navbar-text {
- padding: 0;
-}
-
.link-skip-nav {
position: absolute;
top: -60px;
left: 0.5rem;
z-index: 10;
transition: 150ms cubic-bezier(0.4, 0.14, 1, 1);
-
&:focus {
top: 0.5rem;
transition-timing-function: cubic-bezier(0, 0, 0.3, 1);
}
}
+.nav-item {
+ svg {
+ fill: $light;
+ }
+}
</style>
diff --git a/src/components/Global/StatusIcon.vue b/src/components/Global/StatusIcon.vue
new file mode 100644
index 0000000..bb20840
--- /dev/null
+++ b/src/components/Global/StatusIcon.vue
@@ -0,0 +1,38 @@
+<template>
+ <span :class="['status-icon', status]">
+ <icon-success v-if="status === 'success'" />
+ <icon-danger v-else-if="status === 'danger'" />
+ <icon-secondary v-else />
+ </span>
+</template>
+
+<script>
+import IconCheckmark from "@carbon/icons-vue/es/checkmark--filled/20";
+import IconWarning from "@carbon/icons-vue/es/warning--filled/20";
+import IconError from "@carbon/icons-vue/es/error--filled/20";
+
+export default {
+ name: "StatusIcon",
+ props: ["status"],
+ components: {
+ iconSuccess: IconCheckmark,
+ iconDanger: IconWarning,
+ iconSecondary: IconError
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+.status-icon {
+ vertical-align: text-bottom;
+ &.success {
+ fill: $success;
+ }
+ &.danger {
+ fill: $danger;
+ }
+ &.secondary {
+ fill: $secondary;
+ }
+}
+</style>
diff --git a/src/store/api.js b/src/store/api.js
index da6f398..463e0d8 100644
--- a/src/store/api.js
+++ b/src/store/api.js
@@ -4,10 +4,6 @@
withCredentials: true
});
-// TODO: Permanent authentication solution
-// Using defaults to set auth for sending
-// auth object in header
-
export default {
get(path) {
return api.get(path);
@@ -26,6 +22,5 @@
},
all(promises) {
return Axios.all(promises);
- },
- defaults: api.defaults
+ }
};
diff --git a/src/store/index.js b/src/store/index.js
index 4ef1c9d..889e52b 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -5,6 +5,8 @@
import AuthenticationStore from './modules/Authentication/AuthenticanStore';
import LocalUserManagementStore from './modules/AccessControl/LocalUserMangementStore';
+import WebSocketPlugin from './plugins/WebSocketPlugin';
+
Vue.use(Vuex);
export default new Vuex.Store({
@@ -15,5 +17,6 @@
global: GlobalStore,
authentication: AuthenticationStore,
localUsers: LocalUserManagementStore
- }
+ },
+ plugins: [WebSocketPlugin]
});
diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js
index 8cf2e8e..80d9c1a 100644
--- a/src/store/modules/GlobalStore.js
+++ b/src/store/modules/GlobalStore.js
@@ -1,10 +1,31 @@
import api from '../api';
+const HOST_STATE = {
+ on: 'xyz.openbmc_project.State.Host.HostState.Running',
+ off: 'xyz.openbmc_project.State.Host.HostState.Off',
+ error: 'xyz.openbmc_project.State.Host.HostState.Quiesced',
+ diagnosticMode: 'xyz.openbmc_project.State.Host.HostState.DiagnosticMode'
+};
+
+const hostStateMapper = hostState => {
+ switch (hostState) {
+ case HOST_STATE.on:
+ return 'on';
+ case HOST_STATE.off:
+ return 'off';
+ case HOST_STATE.error:
+ return 'error';
+ // TODO: Add mapping for DiagnosticMode
+ default:
+ return 'unreachable';
+ }
+};
+
const GlobalStore = {
namespaced: true,
state: {
hostName: '--',
- hostStatus: null
+ hostStatus: 'unreachable'
},
getters: {
hostName(state) {
@@ -17,6 +38,9 @@
mutations: {
setHostName(state, hostName) {
state.hostName = hostName;
+ },
+ setHostStatus(state, hostState) {
+ state.hostStatus = hostStateMapper(hostState);
}
},
actions: {
@@ -28,6 +52,15 @@
commit('setHostName', hostName);
})
.catch(error => console.log(error));
+ },
+ getHostStatus({ commit }) {
+ api
+ .get('/xyz/openbmc_project/state/host0/attr/CurrentHostState')
+ .then(response => {
+ const hostState = response.data.data;
+ commit('setHostStatus', hostState);
+ })
+ .catch(error => console.log(error));
}
}
};
diff --git a/src/store/plugins/WebSocketPlugin.js b/src/store/plugins/WebSocketPlugin.js
new file mode 100644
index 0000000..3e2139d
--- /dev/null
+++ b/src/store/plugins/WebSocketPlugin.js
@@ -0,0 +1,46 @@
+/**
+ * WebSocketPlugin will allow us to get new data from the server
+ * without having to poll for changes on the frontend.
+ *
+ * This plugin is subscribed to host state property changes, which
+ * is indicated in the app header Power status.
+ *
+ * https://github.com/openbmc/docs/blob/b41aff0fabe137cdb0cfff584b5fe4a41c0c8e77/rest-api.md#event-subscription-protocol
+ */
+const WebSocketPlugin = store => {
+ let ws;
+ const data = {
+ paths: ['/xyz/openbmc_project/state/host0'],
+ interfaces: ['xyz.openbmc_project.State.Host']
+ };
+
+ const initWebSocket = () => {
+ ws = new WebSocket(`wss://${window.location.host}/subscribe`);
+ ws.onopen = () => {
+ ws.send(JSON.stringify(data));
+ };
+ ws.onerror = event => {
+ console.error(event);
+ };
+ ws.onmessage = event => {
+ const {
+ properties: { CurrentHostState, RequestedHostTransition } = {}
+ } = JSON.parse(event.data);
+ const hostState = CurrentHostState || RequestedHostTransition;
+ store.commit('global/setHostStatus', hostState);
+ };
+ };
+
+ store.subscribe(({ type }) => {
+ if (type === 'authentication/authSuccess') {
+ initWebSocket();
+ }
+ if (type === 'authentication/logout') {
+ if (ws) ws.close();
+ }
+ });
+
+ if (store.getters['authentication/isLoggedIn']) initWebSocket();
+};
+
+export default WebSocketPlugin;
diff --git a/vue.config.js b/vue.config.js
index 9e1e1e1..429b273 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -1,11 +1,24 @@
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
+ css: {
+ loaderOptions: {
+ scss: {
+ prependData: `
+ @import "@/assets/styles/_obmc-custom.scss";
+ `
+ }
+ }
+ },
devServer: {
+ https: true,
proxy: {
'/': {
target: process.env.BASE_URL,
onProxyRes: proxyRes => {
+ // This header is igorned in the browser so removing
+ // it so we don't see warnings in the browser console
+ delete proxyRes.headers['strict-transport-security'];
if (proxyRes.headers['set-cookie']) {
// Need to remove 'Secure' flag on set-cookie value so browser
// can create cookie for local development