Add Sensors page

- Update api calls to use Redfish
- Add column sort to name and status columns
- Set default table sort to status column
- Added lodash package

Github story: https://github.com/openbmc/webui-vue/issues/4

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Ic6e76107475fbf5fb34deb01a4de4a4a9ccfeabf
diff --git a/package-lock.json b/package-lock.json
index 5222244..bcd1bbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5429,7 +5429,8 @@
     "date-fns": {
       "version": "1.30.1",
       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
-      "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw=="
+      "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
+      "dev": true
     },
     "de-indent": {
       "version": "1.0.2",
@@ -10698,8 +10699,7 @@
     "lodash": {
       "version": "4.17.15",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-      "dev": true
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
     },
     "lodash._reinterpolate": {
       "version": "3.0.0",
diff --git a/package.json b/package.json
index 2d5f4d5..b2a2423 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "bootstrap-vue": "2.5.0",
     "core-js": "3.3.2",
     "js-cookie": "^2.2.1",
+    "lodash": "4.17.15",
     "vue": "2.6.11",
     "vue-i18n": "8.15.3",
     "vue-router": "3.1.3",
diff --git a/src/assets/styles/_table.scss b/src/assets/styles/_table.scss
index 528cb80..2372d25 100644
--- a/src/assets/styles/_table.scss
+++ b/src/assets/styles/_table.scss
@@ -1,6 +1,10 @@
 table {
   position: relative;
   z-index: $zindex-dropdown;
+  .status-icon svg {
+    width: 1rem;
+    height: auto;
+  }
 }
 
 .table-light {
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index 48b94c3..d0fee43 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -21,7 +21,7 @@
               <b-nav-item href="javascript:void(0)">
                 {{ $t('appNavigation.hardwareStatus') }}
               </b-nav-item>
-              <b-nav-item href="javascript:void(0)">
+              <b-nav-item to="/health/sensors">
                 {{ $t('appNavigation.sensors') }}
               </b-nav-item>
             </b-collapse>
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 63247da..adc1185 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -180,6 +180,17 @@
       "successRebootStart": "Rebooting BMC."
     }
   },
+  "pageSensors": {
+    "table": {
+      "currentValue": "Current value",
+      "lowerWarning": "Lower warning",
+      "lowerCritical": "Lower critical",
+      "name": "Name",
+      "status": "Status",
+      "upperWarning": "Upper warning",
+      "upperCritical": "Upper critical"
+    }
+  },
   "pageServerPowerOperations": {
     "currentStatus": "Current status",
     "hostname": "Hostname",
diff --git a/src/router/index.js b/src/router/index.js
index 0d246cd..cd6cf8b 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -24,6 +24,13 @@
         }
       },
       {
+        path: '/health/sensors',
+        component: () => import('@/views/Health/Sensors'),
+        meta: {
+          title: 'appPageTitle.sensors'
+        }
+      },
+      {
         path: '/access-control/local-user-management',
         name: 'local-users',
         component: () => import('@/views/AccessControl/LocalUserManagement'),
diff --git a/src/store/index.js b/src/store/index.js
index 2721699..08ada05 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -11,6 +11,7 @@
 import PowerControlStore from './modules/Control/PowerControlStore';
 import NetworkSettingStore from './modules/Configuration/NetworkSettingsStore';
 import EventLogStore from './modules/Health/EventLogStore';
+import SensorsStore from './modules/Health/SensorsStore';
 
 import WebSocketPlugin from './plugins/WebSocketPlugin';
 
@@ -30,7 +31,8 @@
     controls: ControlStore,
     powerControl: PowerControlStore,
     networkSettings: NetworkSettingStore,
-    eventLog: EventLogStore
+    eventLog: EventLogStore,
+    sensors: SensorsStore
   },
   plugins: [WebSocketPlugin]
 });
diff --git a/src/store/modules/Health/SensorsStore.js b/src/store/modules/Health/SensorsStore.js
new file mode 100644
index 0000000..5da1515
--- /dev/null
+++ b/src/store/modules/Health/SensorsStore.js
@@ -0,0 +1,113 @@
+import api from '../../api';
+import { uniqBy } from 'lodash';
+
+const SensorsStore = {
+  namespaced: true,
+  state: {
+    sensors: []
+  },
+  getters: {
+    sensors: state => state.sensors
+  },
+  mutations: {
+    setSensors: (state, sensors) => {
+      state.sensors = uniqBy([...state.sensors, ...sensors], 'name');
+    }
+  },
+  actions: {
+    getAllSensors({ dispatch }) {
+      dispatch('getChassisCollection').then(collection => {
+        collection.forEach(item => {
+          dispatch('getSensors', item);
+          dispatch('getThermalSensors', item);
+          dispatch('getPowerSensors', item);
+        });
+      });
+    },
+    getChassisCollection() {
+      return api
+        .get('/redfish/v1/Chassis')
+        .then(({ data: { Members } }) =>
+          Members.map(member => member['@odata.id'])
+        )
+        .catch(error => console.log(error));
+    },
+    getSensors({ commit }, id) {
+      api
+        .get(`${id}/Sensors`)
+        .then(({ data: { Members = [] } }) => {
+          const promises = Members.map(sensor => api.get(sensor['@odata.id']));
+          api.all(promises).then(
+            api.spread((...responses) => {
+              const sensorData = responses.map(({ data }) => {
+                return {
+                  name: data.Name,
+                  status: data.Status.Health,
+                  currentValue: data.Reading,
+                  lowerCaution: data.Thresholds.LowerCaution.Reading,
+                  upperCaution: data.Thresholds.UpperCaution.Reading,
+                  lowerCritical: data.Thresholds.LowerCritical.Reading,
+                  upperCritical: data.Thresholds.UpperCritical.Reading,
+                  units: data.ReadingUnits
+                };
+              });
+              commit('setSensors', sensorData);
+            })
+          );
+        })
+        .catch(error => console.log(error));
+    },
+    getThermalSensors({ commit }, id) {
+      api
+        .get(`${id}/Thermal`)
+        .then(({ data: { Fans = [], Temperatures = [] } }) => {
+          const sensorData = [];
+          Fans.forEach(sensor => {
+            sensorData.push({
+              // TODO: add upper/lower threshold
+              name: sensor.Name,
+              status: sensor.Status.Health,
+              currentValue: sensor.Reading,
+              units: sensor.ReadingUnits
+            });
+          });
+          Temperatures.forEach(sensor => {
+            sensorData.push({
+              name: sensor.Name,
+              status: sensor.Status.Health,
+              currentValue: sensor.ReadingCelsius,
+              lowerCaution: sensor.LowerThresholdNonCritical,
+              upperCaution: sensor.UpperThresholdNonCritical,
+              lowerCritical: sensor.LowerThresholdCritical,
+              upperCritical: sensor.UpperThresholdCritical,
+              units: '℃'
+            });
+          });
+          commit('setSensors', sensorData);
+        })
+        .catch(error => console.log(error));
+    },
+    getPowerSensors({ commit }, id) {
+      api
+        .get(`${id}/Power`)
+        .then(({ data: { Voltages = [] } }) => {
+          const sensorData = Voltages.map(sensor => {
+            return {
+              name: sensor.Name,
+              status: sensor.Status.Health,
+              currentValue: sensor.ReadingVolts,
+              lowerCaution: sensor.LowerThresholdNonCritical,
+              upperCaution: sensor.UpperThresholdNonCritical,
+              lowerCritical: sensor.LowerThresholdCritical,
+              upperCritical: sensor.UpperThresholdCritical,
+              units: 'Volts'
+            };
+          });
+          commit('setSensors', sensorData);
+        })
+        .catch(error => console.log(error));
+    }
+  }
+};
+
+export default SensorsStore;
diff --git a/src/views/Health/Sensors/Sensors.vue b/src/views/Health/Sensors/Sensors.vue
new file mode 100644
index 0000000..70d4f90
--- /dev/null
+++ b/src/views/Health/Sensors/Sensors.vue
@@ -0,0 +1,126 @@
+<template>
+  <b-container fluid>
+    <page-title />
+    <b-row>
+      <b-col xl="12">
+        <b-table
+          sort-icon-left
+          no-sort-reset
+          sticky-header="75vh"
+          sort-by="status"
+          :items="allSensors"
+          :fields="fields"
+          :sort-desc="true"
+          :sort-compare="sortCompare"
+        >
+          <template v-slot:cell(status)="{ value }">
+            <status-icon :status="statusIcon(value)" />
+            {{ value }}
+          </template>
+          <template v-slot:cell(currentValue)="data">
+            {{ data.value }} {{ data.item.units }}
+          </template>
+          <template v-slot:cell(lowerCaution)="data">
+            {{ data.value }} {{ data.item.units }}
+          </template>
+          <template v-slot:cell(upperCaution)="data">
+            {{ data.value }} {{ data.item.units }}
+          </template>
+          <template v-slot:cell(lowerCritical)="data">
+            {{ data.value }} {{ data.item.units }}
+          </template>
+          <template v-slot:cell(upperCritical)="data">
+            {{ data.value }} {{ data.item.units }}
+          </template>
+        </b-table>
+      </b-col>
+    </b-row>
+  </b-container>
+</template>
+
+<script>
+import PageTitle from '../../../components/Global/PageTitle';
+import StatusIcon from '../../../components/Global/StatusIcon';
+
+const valueFormatter = value => {
+  if (value === null || value === undefined) {
+    return '--';
+  }
+  return parseFloat(value.toFixed(3));
+};
+
+export default {
+  name: 'Sensors',
+  components: { PageTitle, StatusIcon },
+  data() {
+    return {
+      fields: [
+        {
+          key: 'name',
+          sortable: true,
+          label: this.$t('pageSensors.table.name')
+        },
+        {
+          key: 'status',
+          sortable: true,
+          label: this.$t('pageSensors.table.status')
+        },
+        {
+          key: 'lowerCritical',
+          formatter: valueFormatter,
+          label: this.$t('pageSensors.table.lowerCritical')
+        },
+        {
+          key: 'lowerCaution',
+          formatter: valueFormatter,
+          label: this.$t('pageSensors.table.lowerWarning')
+        },
+
+        {
+          key: 'currentValue',
+          formatter: valueFormatter,
+          label: this.$t('pageSensors.table.currentValue')
+        },
+        {
+          key: 'upperCaution',
+          formatter: valueFormatter,
+          label: this.$t('pageSensors.table.upperWarning')
+        },
+        {
+          key: 'upperCritical',
+          formatter: valueFormatter,
+          label: this.$t('pageSensors.table.upperCritical')
+        }
+      ]
+    };
+  },
+  computed: {
+    allSensors() {
+      return this.$store.getters['sensors/sensors'];
+    }
+  },
+  created() {
+    this.$store.dispatch('sensors/getAllSensors');
+  },
+  methods: {
+    statusIcon(status) {
+      switch (status) {
+        case 'OK':
+          return 'success';
+        case 'Warning':
+          return 'warning';
+        case 'Critical':
+          return 'danger';
+        default:
+          return '';
+      }
+    },
+    sortCompare(a, b, key) {
+      if (key === 'status') {
+        const status = ['OK', 'Warning', 'Critical'];
+        return status.indexOf(a.status) - status.indexOf(b.status);
+      }
+    }
+  }
+};
+</script>
diff --git a/src/views/Health/Sensors/index.js b/src/views/Health/Sensors/index.js
new file mode 100644
index 0000000..fc71b61
--- /dev/null
+++ b/src/views/Health/Sensors/index.js
@@ -0,0 +1,2 @@
+import Sensors from './Sensors.vue';
+export default Sensors;