Add client sessions page

- This page will show the list of sessions that are
  currently connected to the BMC.

APIs used:
- To get all the sessions API used is
`/redfish/v1/SessionService/Sessions`
- To delete the sessions API used is
`/redfish/v1/SessionService/Sessions/<session id>`

Signed-off-by: Sukanya Pandey <sukapan1@in.ibm.com>
Change-Id: Ia81f62cbbea749809b9b7f7e62356cfe2db7fc18
diff --git a/src/components/AppNavigation/AppNavigationMixin.js b/src/components/AppNavigation/AppNavigationMixin.js
index b163d75..7fe63a0 100644
--- a/src/components/AppNavigation/AppNavigationMixin.js
+++ b/src/components/AppNavigation/AppNavigationMixin.js
@@ -125,6 +125,11 @@
           icon: 'iconAccessControl',
           children: [
             {
+              id: 'client-sessions',
+              label: this.$t('appNavigation.clientSessions'),
+              route: '/access-control/client-sessions',
+            },
+            {
               id: 'ldap',
               label: this.$t('appNavigation.ldap'),
               route: '/access-control/ldap',
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 0e28de5..dcb52a6 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -95,6 +95,7 @@
   },
   "appNavigation": {
     "accessControl": "Access control",
+    "clientSessions": "@:appPageTitle.clientSessions",
     "configuration": "Configuration",
     "control": "Control",
     "dateTimeSettings": "@:appPageTitle.dateTimeSettings",
@@ -121,6 +122,7 @@
   },
   "appPageTitle": {
     "changePassword": "Change password",
+    "clientSessions": "Client sessions",
     "dateTimeSettings": "Date and time settings",
     "eventLogs": "Event logs",
     "firmware": "Firmware",
@@ -153,6 +155,25 @@
     "newPassword": "New password",
     "username": "Username"
   },
+  "pageClientSessions" : {
+    "action": {
+      "disconnect" : "Disconnect"
+    },
+    "modal": {
+      "disconnectTitle": "Disconnect session| Disconnect sessions",
+      "disconnectMessage": "Are you sure you want to disconnect %{count} session? This action cannot be undone. | Are you sure you want to disconnect %{count} sessions? This action cannot be undone."
+    },
+    "table": {
+      "clientID": "Client ID",
+      "username": "Username",
+      "ipAddress": "IP address",
+      "searchSessions": "Search sessions"
+    },
+    "toast": {
+      "errorDelete": "Error disconnecting %{count} session. | Error disconnecting %{count} sessions.",
+      "successDelete": "Successfully disconnected %{count} session. | Successfully disconnected %{count} sessions."
+    }
+  },
   "pageDateTimeSettings": {
     "alert": {
       "message": "To change how date and time are displayed (either UTC or browser offset) throughout the application, visit ",
diff --git a/src/router/routes.js b/src/router/routes.js
index a82833a..9a9b713 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -7,6 +7,7 @@
 import HardwareStatus from '@/views/Health/HardwareStatus';
 import Kvm from '@/views/Control/Kvm';
 import KvmConsole from '@/views/Control/Kvm/KvmConsole';
+import ClientSessions from '../views/AccessControl/ClientSessions';
 import Ldap from '@/views/AccessControl/Ldap';
 import LocalUserManagement from '@/views/AccessControl/LocalUserManagement';
 import Login from '@/views/Login';
@@ -124,6 +125,14 @@
         },
       },
       {
+        path: '/access-control/client-sessions',
+        name: 'client-sessions',
+        component: ClientSessions,
+        meta: {
+          title: i18n.t('appPageTitle.clientSessions'),
+        },
+      },
+      {
         path: '/access-control/ldap',
         name: 'ldap',
         component: Ldap,
diff --git a/src/store/index.js b/src/store/index.js
index b4a77d8..151eb68 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -3,6 +3,7 @@
 
 import GlobalStore from './modules/GlobalStore';
 import AuthenticationStore from './modules/Authentication/AuthenticanStore';
+import ClientSessions from './modules/AccessControl/ClientSessionsStore';
 import LdapStore from './modules/AccessControl/LdapStore';
 import LocalUserManagementStore from './modules/AccessControl/LocalUserMangementStore';
 import SslCertificatesStore from './modules/AccessControl/SslCertificatesStore';
@@ -36,6 +37,7 @@
   modules: {
     global: GlobalStore,
     authentication: AuthenticationStore,
+    clientSessions: ClientSessions,
     dateTime: DateTimeStore,
     ldap: LdapStore,
     localUsers: LocalUserManagementStore,
diff --git a/src/store/modules/AccessControl/ClientSessionsStore.js b/src/store/modules/AccessControl/ClientSessionsStore.js
new file mode 100644
index 0000000..a09f766
--- /dev/null
+++ b/src/store/modules/AccessControl/ClientSessionsStore.js
@@ -0,0 +1,80 @@
+import api, { getResponseCount } from '@/store/api';
+import i18n from '@/i18n';
+
+const ClientSessionsStore = {
+  namespaced: true,
+  state: {
+    allConnections: [],
+  },
+  getters: {
+    allConnections: (state) => state.allConnections,
+  },
+  mutations: {
+    setAllConnections: (state, allConnections) =>
+      (state.allConnections = allConnections),
+  },
+  actions: {
+    async getClientSessionsData({ commit }) {
+      return await api
+        .get('/redfish/v1/SessionService/Sessions')
+        .then((response) =>
+          response.data.Members.map((sessionLogs) => sessionLogs['@odata.id'])
+        )
+        .then((sessionUris) =>
+          api.all(sessionUris.map((sessionUri) => api.get(sessionUri)))
+        )
+        .then((sessionUris) => {
+          const allConnectionsData = sessionUris.map((sessionUri) => {
+            return {
+              clientID: sessionUri.data?.Id,
+              username: sessionUri.data?.UserName,
+              ipAddress: sessionUri.data?.Oem?.OpenBMC.ClientID.slice(2),
+              uri: sessionUri.data['@odata.id'],
+            };
+          });
+          commit('setAllConnections', allConnectionsData);
+        })
+        .catch((error) => {
+          console.log('Client Session Data:', error);
+        });
+    },
+    async disconnectSessions({ dispatch }, uris = []) {
+      const promises = uris.map((uri) =>
+        api.delete(uri).catch((error) => {
+          console.log(error);
+          return error;
+        })
+      );
+      return await api
+        .all(promises)
+        .then((response) => {
+          dispatch('getClientSessionsData');
+          return response;
+        })
+        .then(
+          api.spread((...responses) => {
+            const { successCount, errorCount } = getResponseCount(responses);
+            const toastMessages = [];
+
+            if (successCount) {
+              const message = i18n.tc(
+                'pageClientSessions.toast.successDelete',
+                successCount
+              );
+              toastMessages.push({ type: 'success', message });
+            }
+
+            if (errorCount) {
+              const message = i18n.tc(
+                'pageClientSessions.toast.errorDelete',
+                errorCount
+              );
+              toastMessages.push({ type: 'error', message });
+            }
+            return toastMessages;
+          })
+        );
+    },
+  },
+};
+export default ClientSessionsStore;
diff --git a/src/views/AccessControl/ClientSessions/ClientSessions.vue b/src/views/AccessControl/ClientSessions/ClientSessions.vue
new file mode 100644
index 0000000..04dd052
--- /dev/null
+++ b/src/views/AccessControl/ClientSessions/ClientSessions.vue
@@ -0,0 +1,292 @@
+<template>
+  <b-container fluid="xl">
+    <page-title />
+    <b-row class="align-items-end">
+      <b-col sm="6" md="5" xl="4">
+        <search
+          :placeholder="$t('pageClientSessions.table.searchSessions')"
+          @change-search="onChangeSearchInput"
+          @clear-search="onClearSearchInput"
+        />
+      </b-col>
+      <b-col sm="3" md="3" xl="2">
+        <table-cell-count
+          :filtered-items-count="filteredRows"
+          :total-number-of-cells="allConnections.length"
+        ></table-cell-count>
+      </b-col>
+    </b-row>
+    <b-row>
+      <b-col>
+        <table-toolbar
+          ref="toolbar"
+          :selected-items-count="selectedRows.length"
+          :actions="batchActions"
+          @clear-selected="clearSelectedRows($refs.table)"
+          @batch-action="onBatchAction"
+        >
+        </table-toolbar>
+        <b-table
+          id="table-session-logs"
+          ref="table"
+          responsive="md"
+          selectable
+          no-select-on-click
+          hover
+          show-empty
+          sort-by="clientID"
+          :fields="fields"
+          :items="allConnections"
+          :filter="searchFilter"
+          :empty-text="$t('global.table.emptyMessage')"
+          :per-page="perPage"
+          :current-page="currentPage"
+          @filtered="onFiltered"
+          @row-selected="onRowSelected($event, allConnections.length)"
+        >
+          <!-- Checkbox column -->
+          <template #head(checkbox)>
+            <b-form-checkbox
+              v-model="tableHeaderCheckboxModel"
+              data-test-id="sessionLogs-checkbox-selectAll"
+              :indeterminate="tableHeaderCheckboxIndeterminate"
+              @change="onChangeHeaderCheckbox($refs.table)"
+            >
+              <span class="sr-only">{{ $t('global.table.selectAll') }}</span>
+            </b-form-checkbox>
+          </template>
+          <template #cell(checkbox)="row">
+            <b-form-checkbox
+              v-model="row.rowSelected"
+              :data-test-id="`sessionLogs-checkbox-selectRow-${row.index}`"
+              @change="toggleSelectRow($refs.table, row.index)"
+            >
+              <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+            </b-form-checkbox>
+          </template>
+
+          <!-- Actions column -->
+          <template #cell(actions)="row" class="ml-3">
+            <table-row-action
+              v-for="(action, index) in row.item.actions"
+              :key="index"
+              :value="action.value"
+              :title="action.title"
+              :row-data="row.item"
+              :data-test-id="`sessionLogs-button-deleteRow-${row.index}`"
+              @click-table-action="onTableRowAction($event, row.item)"
+            ></table-row-action>
+          </template>
+        </b-table>
+      </b-col>
+    </b-row>
+
+    <!-- Table pagination -->
+    <b-row>
+      <b-col sm="6">
+        <b-form-group
+          class="table-pagination-select"
+          :label="$t('global.table.itemsPerPage')"
+          label-for="pagination-items-per-page"
+        >
+          <b-form-select
+            id="pagination-items-per-page"
+            v-model="perPage"
+            :options="itemsPerPageOptions"
+          />
+        </b-form-group>
+      </b-col>
+      <b-col sm="6">
+        <b-pagination
+          v-model="currentPage"
+          first-number
+          last-number
+          :per-page="perPage"
+          :total-rows="getTotalRowCount(allConnections.length)"
+          aria-controls="table-session-logs"
+        />
+      </b-col>
+    </b-row>
+  </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import Search from '@/components/Global/Search';
+import TableCellCount from '@/components/Global/TableCellCount';
+import TableRowAction from '@/components/Global/TableRowAction';
+import TableToolbar from '@/components/Global/TableToolbar';
+
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import BVPaginationMixin, {
+  currentPage,
+  perPage,
+  itemsPerPageOptions,
+} from '@/components/Mixins/BVPaginationMixin';
+import BVTableSelectableMixin, {
+  selectedRows,
+  tableHeaderCheckboxModel,
+  tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import SearchFilterMixin, {
+  searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+
+export default {
+  components: {
+    PageTitle,
+    Search,
+    TableCellCount,
+    TableRowAction,
+    TableToolbar,
+  },
+  mixins: [
+    BVPaginationMixin,
+    BVTableSelectableMixin,
+    BVToastMixin,
+    LoadingBarMixin,
+    SearchFilterMixin,
+  ],
+  beforeRouteLeave(to, from, next) {
+    // Hide loader if the user navigates to another page
+    // before request is fulfilled.
+    this.hideLoader();
+    next();
+  },
+  data() {
+    return {
+      fields: [
+        {
+          key: 'checkbox',
+        },
+        {
+          key: 'clientID',
+          label: this.$t('pageClientSessions.table.clientID'),
+        },
+        {
+          key: 'username',
+          label: this.$t('pageClientSessions.table.username'),
+        },
+        {
+          key: 'ipAddress',
+          label: this.$t('pageClientSessions.table.ipAddress'),
+        },
+        {
+          key: 'actions',
+          label: '',
+        },
+      ],
+      batchActions: [
+        {
+          value: 'disconnect',
+          label: this.$t('pageClientSessions.action.disconnect'),
+        },
+      ],
+      currentPage: currentPage,
+      itemsPerPageOptions: itemsPerPageOptions,
+      perPage: perPage,
+      selectedRows: selectedRows,
+      searchTotalFilteredRows: 0,
+      tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+      tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+      searchFilter: searchFilter,
+    };
+  },
+  computed: {
+    filteredRows() {
+      return this.searchFilter
+        ? this.searchTotalFilteredRows
+        : this.allConnections.length;
+    },
+    allConnections() {
+      return this.$store.getters['clientSessions/allConnections'].map(
+        (session) => {
+          return {
+            ...session,
+            actions: [
+              {
+                value: 'disconnect',
+                title: this.$t('pageClientSessions.action.disconnect'),
+              },
+            ],
+          };
+        }
+      );
+    },
+  },
+  created() {
+    this.startLoader();
+    this.$store
+      .dispatch('clientSessions/getClientSessionsData')
+      .finally(() => this.endLoader());
+  },
+  methods: {
+    onFiltered(filteredItems) {
+      this.searchTotalFilteredRows = filteredItems.length;
+    },
+    onChangeSearchInput(event) {
+      this.searchFilter = event;
+    },
+    disconnectSessions(uris) {
+      this.$store
+        .dispatch('clientSessions/disconnectSessions', uris)
+        .then((messages) => {
+          messages.forEach(({ type, message }) => {
+            if (type === 'success') {
+              this.successToast(message);
+            } else if (type === 'error') {
+              this.errorToast(message);
+            }
+          });
+        });
+    },
+    onTableRowAction(action, { uri }) {
+      if (action === 'disconnect') {
+        this.$bvModal
+          .msgBoxConfirm(
+            this.$tc('pageClientSessions.modal.disconnectMessage'),
+            {
+              title: this.$tc('pageClientSessions.modal.disconnectTitle'),
+              okTitle: this.$t('pageClientSessions.action.disconnect'),
+            }
+          )
+          .then((deleteConfirmed) => {
+            if (deleteConfirmed) this.disconnectSessions([uri]);
+          });
+      }
+    },
+    onBatchAction(action) {
+      if (action === 'disconnect') {
+        const uris = this.selectedRows.map((row) => row.uri);
+        this.$bvModal
+          .msgBoxConfirm(
+            this.$tc(
+              'pageClientSessions.modal.disconnectMessage',
+              this.selectedRows.length
+            ),
+            {
+              title: this.$tc(
+                'pageClientSessions.modal.disconnectTitle',
+                this.selectedRows.length
+              ),
+              okTitle: this.$t('pageClientSessions.action.disconnect'),
+            }
+          )
+          .then((deleteConfirmed) => {
+            if (deleteConfirmed) {
+              this.disconnectSessions(uris);
+            }
+          });
+      }
+    },
+  },
+};
+</script>
+<style lang="scss">
+#table-session-logs {
+  td .btn-link {
+    width: auto !important;
+  }
+}
+</style>
diff --git a/src/views/AccessControl/ClientSessions/index.js b/src/views/AccessControl/ClientSessions/index.js
new file mode 100644
index 0000000..6000ab7
--- /dev/null
+++ b/src/views/AccessControl/ClientSessions/index.js
@@ -0,0 +1,2 @@
+import ClientSessions from './ClientSessions.vue';
+export default ClientSessions;