diff options
-rw-r--r-- | src/assets/styles/_colors.scss | 2 | ||||
-rw-r--r-- | src/components/AppHeader/AppHeader.vue | 89 | ||||
-rw-r--r-- | src/components/Global/StatusIcon.vue | 38 | ||||
-rw-r--r-- | src/store/api.js | 7 | ||||
-rw-r--r-- | src/store/index.js | 5 | ||||
-rw-r--r-- | src/store/modules/GlobalStore.js | 35 | ||||
-rw-r--r-- | src/store/plugins/WebSocketPlugin.js | 46 | ||||
-rw-r--r-- | vue.config.js | 13 |
8 files changed, 174 insertions, 61 deletions
diff --git a/src/assets/styles/_colors.scss b/src/assets/styles/_colors.scss index 04351231..4a8b62c9 100644 --- a/src/assets/styles/_colors.scss +++ b/src/assets/styles/_colors.scss @@ -91,7 +91,7 @@ $info: $teal; $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 7974f70a..244eeb32 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> - <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"> + <!-- Left aligned nav items --> <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-nav-text>BMC System Management</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 @@ export default { </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 00000000..bb208409 --- /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 da6f3982..463e0d86 100644 --- a/src/store/api.js +++ b/src/store/api.js @@ -4,10 +4,6 @@ const api = Axios.create({ 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 @@ export default { }, all(promises) { return Axios.all(promises); - }, - defaults: api.defaults + } }; diff --git a/src/store/index.js b/src/store/index.js index 4ef1c9d0..889e52b4 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,6 +5,8 @@ import GlobalStore from './modules/GlobalStore'; 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 @@ export default new Vuex.Store({ global: GlobalStore, authentication: AuthenticationStore, localUsers: LocalUserManagementStore - } + }, + plugins: [WebSocketPlugin] }); diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js index 8cf2e8eb..80d9c1a3 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 @@ const GlobalStore = { mutations: { setHostName(state, hostName) { state.hostName = hostName; + }, + setHostStatus(state, hostState) { + state.hostStatus = hostStateMapper(hostState); } }, actions: { @@ -28,6 +52,15 @@ const GlobalStore = { 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 00000000..3e2139dd --- /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 9e1e1e1a..429b273e 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 |