summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey V.Kosteltsev <AKosteltsev@IBS.RU>2022-07-04 23:59:32 +0300
committerAndrey V.Kosteltsev <AKosteltsev@IBS.RU>2022-07-04 23:59:32 +0300
commit8047ae3d83ba0718fb7a42907036157e5c680b85 (patch)
tree600b017fe3a75ab4d1577eb9367afe8548401f9f
parent3f4094d08b873e17464a51c817ea7d41177f848d (diff)
downloadwebui-vue-8047ae3d83ba0718fb7a42907036157e5c680b85.tar.xz
IBS: _sila UI theme
-rw-r--r--.env.sila12
-rw-r--r--src/assets/images/_sila/built-on-openbmc-logo.svg13
-rw-r--r--src/assets/images/_sila/login-company-logo.svg1
-rw-r--r--src/assets/images/_sila/logo-header.svg1
-rw-r--r--src/assets/styles/_obmc-sila.scss6
-rw-r--r--src/assets/styles/bmc/_sila/_alert.scss70
-rw-r--r--src/assets/styles/bmc/_sila/_badge.scss21
-rw-r--r--src/assets/styles/bmc/_sila/_base.scss50
-rw-r--r--src/assets/styles/bmc/_sila/_bootstrap-grid.scss8
-rw-r--r--src/assets/styles/bmc/_sila/_buttons.scss82
-rw-r--r--src/assets/styles/bmc/_sila/_calendar.scss17
-rw-r--r--src/assets/styles/bmc/_sila/_card.scss5
-rw-r--r--src/assets/styles/bmc/_sila/_dropdown.scss31
-rw-r--r--src/assets/styles/bmc/_sila/_forms.scss132
-rw-r--r--src/assets/styles/bmc/_sila/_index.scss18
-rw-r--r--src/assets/styles/bmc/_sila/_kvm.scss12
-rw-r--r--src/assets/styles/bmc/_sila/_modal.scss12
-rw-r--r--src/assets/styles/bmc/_sila/_pagination.scss24
-rw-r--r--src/assets/styles/bmc/_sila/_section-divider.scss3
-rw-r--r--src/assets/styles/bmc/_sila/_sol.scss3
-rw-r--r--src/assets/styles/bmc/_sila/_tables.scss171
-rw-r--r--src/assets/styles/bmc/_sila/_toasts.scss61
-rw-r--r--src/components/_sila/AppHeader/AppHeader.vue384
-rw-r--r--src/components/_sila/AppHeader/index.js2
-rw-r--r--src/components/_sila/AppNavigation/AppNavigation.vue255
-rw-r--r--src/components/_sila/AppNavigation/AppNavigationMixin.js182
-rw-r--r--src/components/_sila/AppNavigation/index.js2
-rw-r--r--src/components/_sila/Global/Alert.vue47
-rw-r--r--src/components/_sila/Global/ButtonBackToTop.vue68
-rw-r--r--src/components/_sila/Global/FormFile.vue119
-rw-r--r--src/components/_sila/Global/InfoTooltip.vue35
-rw-r--r--src/components/_sila/Global/InputPasswordToggle.vue54
-rw-r--r--src/components/_sila/Global/LoadingBar.vue93
-rw-r--r--src/components/_sila/Global/PageContainer.vue37
-rw-r--r--src/components/_sila/Global/PageSection.vue29
-rw-r--r--src/components/_sila/Global/PageTitle.vue32
-rw-r--r--src/components/_sila/Global/Search.vue83
-rw-r--r--src/components/_sila/Global/StatusIcon.vue61
-rw-r--r--src/components/_sila/Global/TableCellCount.vue35
-rw-r--r--src/components/_sila/Global/TableDateFilter.vue165
-rw-r--r--src/components/_sila/Global/TableFilter.vue114
-rw-r--r--src/components/_sila/Global/TableRowAction.vue112
-rw-r--r--src/components/_sila/Global/TableToolbar.vue130
-rw-r--r--src/components/_sila/Global/TableToolbarExport.vue36
-rw-r--r--src/components/_sila/Mixins/BVPaginationMixin.js34
-rw-r--r--src/components/_sila/Mixins/BVTableSelectableMixin.js41
-rw-r--r--src/components/_sila/Mixins/BVToastMixin.js115
-rw-r--r--src/components/_sila/Mixins/DataFormatterMixin.js30
-rw-r--r--src/components/_sila/Mixins/JumpLinkMixin.js27
-rw-r--r--src/components/_sila/Mixins/LoadingBarMixin.js19
-rw-r--r--src/components/_sila/Mixins/LocalTimezoneLabelMixin.js14
-rw-r--r--src/components/_sila/Mixins/SearchFilterMixin.js14
-rw-r--r--src/components/_sila/Mixins/TableFilterMixin.js58
-rw-r--r--src/components/_sila/Mixins/TableRowExpandMixin.js15
-rw-r--r--src/components/_sila/Mixins/TableSortMixin.js11
-rw-r--r--src/components/_sila/Mixins/VuelidateMixin.js10
-rw-r--r--src/env/assets/styles/_sila.scss92
-rw-r--r--src/env/components/AppNavigation/sila.js182
-rw-r--r--src/env/router/sila.js286
-rw-r--r--src/env/store/sila.js10
-rw-r--r--src/layouts/_sila/AppLayout.vue91
-rw-r--r--src/layouts/_sila/ConsoleLayout.vue9
-rw-r--r--src/layouts/_sila/LoginLayout.vue112
-rw-r--r--src/views/_sila/ChangePassword/ChangePassword.vue134
-rw-r--r--src/views/_sila/ChangePassword/index.js2
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/Inventory.vue196
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryServiceIndicator.vue76
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableAssembly.vue153
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableBmcManager.vue245
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableChassis.vue191
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableDimmSlot.vue255
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableFans.vue190
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue208
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableProcessors.vue251
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/InventoryTableSystem.vue224
-rw-r--r--src/views/_sila/HardwareStatus/Inventory/index.js2
-rw-r--r--src/views/_sila/HardwareStatus/Sensors/Sensors.vue256
-rw-r--r--src/views/_sila/HardwareStatus/Sensors/index.js2
-rw-r--r--src/views/_sila/Login/Login.vue146
-rw-r--r--src/views/_sila/Login/index.js2
-rw-r--r--src/views/_sila/Logs/Dumps/Dumps.vue404
-rw-r--r--src/views/_sila/Logs/Dumps/DumpsForm.vue97
-rw-r--r--src/views/_sila/Logs/Dumps/DumpsModalConfirmation.vue75
-rw-r--r--src/views/_sila/Logs/Dumps/index.js2
-rw-r--r--src/views/_sila/Logs/EventLogs/EventLogs.vue604
-rw-r--r--src/views/_sila/Logs/EventLogs/index.js2
-rw-r--r--src/views/_sila/Logs/PostCodeLogs/PostCodeLogs.vue347
-rw-r--r--src/views/_sila/Logs/PostCodeLogs/index.js2
-rw-r--r--src/views/_sila/Operations/FactoryReset/FactoryReset.vue117
-rw-r--r--src/views/_sila/Operations/FactoryReset/FactoryResetModal.vue113
-rw-r--r--src/views/_sila/Operations/FactoryReset/index.js2
-rw-r--r--src/views/_sila/Operations/Firmware/Firmware.vue93
-rw-r--r--src/views/_sila/Operations/Firmware/FirmwareAlertServerPower.vue50
-rw-r--r--src/views/_sila/Operations/Firmware/FirmwareCardsBmc.vue136
-rw-r--r--src/views/_sila/Operations/Firmware/FirmwareCardsHost.vue73
-rw-r--r--src/views/_sila/Operations/Firmware/FirmwareFormUpdate.vue187
-rw-r--r--src/views/_sila/Operations/Firmware/FirmwareModalSwitchToRunning.vue31
-rw-r--r--src/views/_sila/Operations/Firmware/FirmwareModalUpdateFirmware.vue44
-rw-r--r--src/views/_sila/Operations/Firmware/index.js2
-rw-r--r--src/views/_sila/Operations/KeyClear/KeyClear.vue106
-rw-r--r--src/views/_sila/Operations/KeyClear/index.js2
-rw-r--r--src/views/_sila/Operations/Kvm/Kvm.vue24
-rw-r--r--src/views/_sila/Operations/Kvm/KvmConsole.vue170
-rw-r--r--src/views/_sila/Operations/Kvm/index.js2
-rw-r--r--src/views/_sila/Operations/RebootBmc/RebootBmc.vue83
-rw-r--r--src/views/_sila/Operations/RebootBmc/index.js2
-rw-r--r--src/views/_sila/Operations/SerialOverLan/SerialOverLan.vue24
-rw-r--r--src/views/_sila/Operations/SerialOverLan/SerialOverLanConsole.vue172
-rw-r--r--src/views/_sila/Operations/SerialOverLan/index.js2
-rw-r--r--src/views/_sila/Operations/ServerPowerOperations/BootSettings.vue132
-rw-r--r--src/views/_sila/Operations/ServerPowerOperations/ServerPowerOperations.vue260
-rw-r--r--src/views/_sila/Operations/ServerPowerOperations/index.js2
-rw-r--r--src/views/_sila/Operations/VirtualMedia/ModalConfigureConnection.vue145
-rw-r--r--src/views/_sila/Operations/VirtualMedia/VirtualMedia.vue221
-rw-r--r--src/views/_sila/Operations/VirtualMedia/index.js2
-rw-r--r--src/views/_sila/Overview/Overview.vue100
-rw-r--r--src/views/_sila/Overview/OverviewCard.vue81
-rw-r--r--src/views/_sila/Overview/OverviewDumps.vue54
-rw-r--r--src/views/_sila/Overview/OverviewEvents.vue85
-rw-r--r--src/views/_sila/Overview/OverviewFirmware.vue49
-rw-r--r--src/views/_sila/Overview/OverviewInventory.vue57
-rw-r--r--src/views/_sila/Overview/OverviewNetwork.vue71
-rw-r--r--src/views/_sila/Overview/OverviewPower.vue48
-rw-r--r--src/views/_sila/Overview/OverviewQuickLinks.vue56
-rw-r--r--src/views/_sila/Overview/OverviewServer.vue47
-rw-r--r--src/views/_sila/Overview/index.js2
-rw-r--r--src/views/_sila/PageNotFound/PageNotFound.vue12
-rw-r--r--src/views/_sila/PageNotFound/index.js2
-rw-r--r--src/views/_sila/ProfileSettings/ProfileSettings.vue222
-rw-r--r--src/views/_sila/ProfileSettings/index.js2
-rw-r--r--src/views/_sila/ResourceManagement/Power.vue170
-rw-r--r--src/views/_sila/ResourceManagement/index.js2
-rw-r--r--src/views/_sila/SecurityAndAccess/Certificates/Certificates.vue322
-rw-r--r--src/views/_sila/SecurityAndAccess/Certificates/CsrCountryCodes.js345
-rw-r--r--src/views/_sila/SecurityAndAccess/Certificates/ModalGenerateCsr.vue496
-rw-r--r--src/views/_sila/SecurityAndAccess/Certificates/ModalUploadCertificate.vue168
-rw-r--r--src/views/_sila/SecurityAndAccess/Certificates/index.js2
-rw-r--r--src/views/_sila/SecurityAndAccess/Ldap/Ldap.vue435
-rw-r--r--src/views/_sila/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue164
-rw-r--r--src/views/_sila/SecurityAndAccess/Ldap/TableRoleGroups.vue269
-rw-r--r--src/views/_sila/SecurityAndAccess/Ldap/index.js2
-rw-r--r--src/views/_sila/SecurityAndAccess/Policies/Policies.vue213
-rw-r--r--src/views/_sila/SecurityAndAccess/Policies/index.js2
-rw-r--r--src/views/_sila/SecurityAndAccess/Sessions/Sessions.vue294
-rw-r--r--src/views/_sila/SecurityAndAccess/Sessions/index.js2
-rw-r--r--src/views/_sila/SecurityAndAccess/UserManagement/ModalSettings.vue215
-rw-r--r--src/views/_sila/SecurityAndAccess/UserManagement/ModalUser.vue386
-rw-r--r--src/views/_sila/SecurityAndAccess/UserManagement/TableRoles.vue92
-rw-r--r--src/views/_sila/SecurityAndAccess/UserManagement/UserManagement.vue391
-rw-r--r--src/views/_sila/SecurityAndAccess/UserManagement/index.js2
-rw-r--r--src/views/_sila/Settings/DateTime/DateTime.vue417
-rw-r--r--src/views/_sila/Settings/DateTime/index.js2
-rw-r--r--src/views/_sila/Settings/Network/ModalDns.vue92
-rw-r--r--src/views/_sila/Settings/Network/ModalHostname.vue110
-rw-r--r--src/views/_sila/Settings/Network/ModalIpv4.vue165
-rw-r--r--src/views/_sila/Settings/Network/ModalMacAddress.vue109
-rw-r--r--src/views/_sila/Settings/Network/Network.vue167
-rw-r--r--src/views/_sila/Settings/Network/NetworkGlobalSettings.vue161
-rw-r--r--src/views/_sila/Settings/Network/NetworkInterfaceSettings.vue117
-rw-r--r--src/views/_sila/Settings/Network/TableDns.vue145
-rw-r--r--src/views/_sila/Settings/Network/TableIpv4.vue169
-rw-r--r--src/views/_sila/Settings/Network/index.js2
-rw-r--r--src/views/_sila/Settings/PowerRestorePolicy/PowerRestorePolicy.vue91
-rw-r--r--src/views/_sila/Settings/PowerRestorePolicy/index.js2
164 files changed, 16870 insertions, 0 deletions
diff --git a/.env.sila b/.env.sila
new file mode 100644
index 00000000..f5000278
--- /dev/null
+++ b/.env.sila
@@ -0,0 +1,12 @@
+NODE_ENV=production
+VUE_APP_ENV_NAME="sila"
+VUE_APP_COMPANY_NAME="IBS"
+VUE_APP_GUI_NAME="BMC System Management"
+VUE_APP_SUBSCRIBE_SOCKET_DISABLED="true"
+VUE_APP_SWITCH_TO_BACKUP_IMAGE_DISABLED="true"
+VUE_APP_MODIFY_SSH_POLICY_DISABLED="true"
+VUE_APP_VIRTUAL_MEDIA_LIST_ENABLED="true"
+CUSTOM_STYLES="true"
+CUSTOM_APP_NAV="true"
+CUSTOM_STORE="true"
+CUSTOM_ROUTER="true"
diff --git a/src/assets/images/_sila/built-on-openbmc-logo.svg b/src/assets/images/_sila/built-on-openbmc-logo.svg
new file mode 100644
index 00000000..53e7fdc5
--- /dev/null
+++ b/src/assets/images/_sila/built-on-openbmc-logo.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="615.034" height="606.872" viewBox="88.47 5 615.034 606.872">
+ <path fill="#A7A8AB" d="M703.504 524.604c-10.908-23.902-39.128-34.437-63.03-23.528s-34.437 39.128-23.528 63.03 39.128 34.437 63.031 23.528a47.578 47.578 0 0022.916-22.239l-9.51-4.513c-9.083 18.301-31.281 25.773-49.582 16.69s-25.772-31.282-16.689-49.583c9.083-18.3 31.281-25.772 49.582-16.689a36.994 36.994 0 0117.225 17.816l9.585-4.512z"/>
+ <path fill="#579AC8" d="M256.378 213.7a136.858 136.858 0 0029.599 85.178c-.255.739-.382 1.529-.561 2.294a27.54 27.54 0 00-.867 6.553c0 15.487 12.556 28.044 28.044 28.044s28.044-12.557 28.044-28.044c0-15.489-12.556-28.045-28.044-28.045a27.219 27.219 0 00-3.06.179c-.79 0-1.555.204-2.32.331-36.755-47.793-27.808-116.333 19.986-153.088a109.135 109.135 0 0116.115-10.282V10.277l-1.963.536a204.136 204.136 0 00-18.152 5.609c-3.289 1.198-6.578 2.549-9.79 3.773v82.169l-.332.255A137.14 137.14 0 00256.378 213.7z"/>
+ <path fill="#579AC8" d="M395.146 251.228c-20.315.016-36.857-16.322-37.095-36.636H327.1c.124 31.721 22.467 59.011 53.539 65.394v115.619c-99.899-7.141-175.094-93.913-167.954-193.811a181.344 181.344 0 0138.721-99.659c.791.153 1.606.255 2.55.332.943.076 1.631 0 2.549 0 15.249.036 27.64-12.296 27.676-27.545.037-15.249-12.295-27.64-27.544-27.676-15.249-.037-27.64 12.295-27.677 27.544a27.607 27.607 0 001.413 8.785c-72.331 90.409-57.675 222.336 32.735 294.667a209.641 209.641 0 00130.89 45.941c3.595 0 7.165-.153 10.708-.356l4.156-.229V248.653a36.9 36.9 0 01-13.716 2.575z"/>
+ <path fill="#A3CE4D" d="M526.877 179.741a137.108 137.108 0 00-25.112-50.25c.337-1.088.609-2.194.815-3.314.391-1.819.596-3.672.612-5.532-.006-15.488-12.566-28.04-28.055-28.034-15.488.006-28.04 12.567-28.033 28.055.006 15.488 12.566 28.04 28.055 28.033 1.802 0 3.6-.175 5.368-.521 36.745 47.818 27.769 116.371-20.05 153.116a109.15 109.15 0 01-16.076 10.253v106.466a212.604 212.604 0 008.158-2.218 209.772 209.772 0 0021.747-7.648v-82.144c46.035-33.228 66.902-91.341 52.52-146.263h.051z"/>
+ <path fill="#A3CE4D" d="M559.204 343.442c71.162-91.297 54.84-222.996-36.457-294.157A209.588 209.588 0 00393.896 5c-4.181 0-8.311.153-12.416.408h-.28v174.358a37.046 37.046 0 0113.232-2.728h.688c20.5-.056 37.166 16.517 37.222 37.018V214.668h28.248c-.067-30.889-21.298-57.705-51.347-64.859v-116c99.813 8.435 173.888 96.185 165.453 195.997a181.375 181.375 0 01-37.495 95.994h-.179c-15.327-2.225-29.557 8.397-31.781 23.725a28.227 28.227 0 00-.291 4.014c0 15.488 12.557 28.044 28.045 28.044 15.488-.008 28.038-12.569 28.03-28.058a28.06 28.06 0 00-1.159-7.967 16.997 16.997 0 00-.662-2.116z"/>
+ <path fill="#A7A8AB" d="M601.5 589.998v-91.194h-11.957l-37.044 75.183-37.044-75.183h-12.109v91.194h10.631v-69.371l34.264 69.371h8.516l34.239-69.371v69.371z"/>
+ <path fill="#636567" d="M393.973 531.31a27.02 27.02 0 00-20.141-8.516 25.878 25.878 0 00-10.606 2.116 40.606 40.606 0 00-9.229 5.711v-6.884h-10.784v66.286h10.784v-37.706a19.726 19.726 0 015.303-14.022c7.39-7.505 19.463-7.599 26.968-.209l.209.209a19.783 19.783 0 015.303 13.971v37.682h10.478v-38.242a28.352 28.352 0 00-8.26-20.396M250.846 572.101a31.638 31.638 0 01-12.391 13.181 34.52 34.52 0 01-17.846 4.717h-16.011v21.874H194.4v-88.135h25.75a34.186 34.186 0 0124.398 9.586c10.573 10.042 13.149 25.905 6.298 38.777m-46.247 8.26h14.71a24.178 24.178 0 0017.184-6.73 21.899 21.899 0 007.036-16.674 21.362 21.362 0 00-6.883-16.342 24.037 24.037 0 00-17.03-6.399h-15.017v46.145zM334.367 560.246a11.65 11.65 0 00.357-3.468c0-18.814-15.985-34.061-35.693-34.061-19.707 0-35.692 15.297-35.692 34.061 0 18.765 15.985 34.062 35.692 34.062a35.695 35.695 0 0030.416-16.623l-8.286-6.246a25.215 25.215 0 01-22.129 12.747c-13.256.749-24.61-9.391-25.358-22.647s9.391-24.61 22.647-25.358a23.576 23.576 0 012.711 0 24.985 24.985 0 0123.838 16.673c0 .179.382 1.606.433 1.785H283.76v8.974l50.607.101zM173.113 544.388c0 20.459-16.585 37.044-37.044 37.044-20.458 0-37.044-16.585-37.044-37.044 0-20.458 16.585-37.043 37.044-37.043 20.459 0 37.044 16.585 37.044 37.043m10.504 0c0-26.273-21.299-47.572-47.573-47.572S88.47 518.114 88.47 544.388c0 26.274 21.299 47.573 47.573 47.573s47.574-21.299 47.574-47.573"/>
+ <path fill="#A7A8AB" d="M482.058 562.643c.028 9.532-7.677 17.282-17.209 17.311h-39.466v-34.622h39.491c9.526.042 17.223 7.784 17.209 17.312m-5.839-40.615c.001 7.123-5.752 12.909-12.874 12.951h-37.962v-25.928h37.859c7.129.027 12.889 5.822 12.875 12.951m2.397 17.082c9.51-8.376 10.429-22.875 2.053-32.384a22.952 22.952 0 00-16.56-7.771h-49.204v91.144H466.888c15.067-1.113 26.379-14.229 25.266-29.297a27.357 27.357 0 00-13.589-21.667"/>
+ <g fill="#99C248">
+ <path d="M101.054 479.658l8.647-50.502h14.193c3.789 0 6.487.241 8.095.724 2.503.735 4.432 2.102 5.788 4.1 1.354 1.998 2.032 4.467 2.032 7.406 0 2.963-.654 5.442-1.963 7.44s-3.273 3.515-5.891 4.548c2.044.688 3.68 1.929 4.909 3.721 1.229 1.791 1.843 3.881 1.843 6.27 0 3.261-.74 6.212-2.222 8.853-1.481 2.642-3.411 4.542-5.788 5.702-2.377 1.159-5.748 1.739-10.11 1.739h-19.533zm9.884-8.13h8.215c3.221 0 5.43-.229 6.626-.689 1.197-.459 2.198-1.303 3.003-2.531s1.208-2.612 1.208-4.151c0-1.813-.528-3.238-1.585-4.271s-2.722-1.551-4.996-1.551H113.21l-2.272 13.193zm3.786-21.978h6.513c2.849 0 4.894-.229 6.134-.688a5.744 5.744 0 002.929-2.377c.712-1.125 1.068-2.412 1.068-3.858 0-1.171-.265-2.164-.793-2.979s-1.195-1.361-2-1.637c-.805-.275-2.54-.413-5.207-.413h-6.586l-2.058 11.952zM148.174 443.073h8.144l-3.709 21.592c-.435 2.586-.652 4.206-.652 4.861 0 1.123.324 2.06.972 2.809.648.748 1.438 1.122 2.371 1.122 1.091 0 2.171-.39 3.24-1.171 1.5-1.079 2.694-2.485 3.581-4.22.887-1.734 1.645-4.255 2.273-7.562l3.245-17.432h8.143l-6.788 36.585h-7.618l.905-4.961c-3.299 3.858-6.859 5.788-10.681 5.788-2.411 0-4.328-.845-5.75-2.532-1.422-1.688-2.133-4.014-2.133-6.976 0-1.24.323-3.743.968-7.51l3.489-20.393zM184.179 443.073h8.096l-6.27 36.585h-8.096l6.27-36.585zm2.377-13.917h8.13l-1.55 8.957h-8.096l1.516-8.957zM194.1 479.658l8.647-50.502h8.13l-8.682 50.502H194.1zM211.703 450.411l1.316-7.338h4.193l1.019-5.855 9.445-6.914-2.32 12.77h5.122l-1.245 7.338h-5.202l-2.836 15.281c-.491 2.747-.736 4.343-.736 4.788 0 .82.218 1.43.655 1.828.437.398 1.188.598 2.256.598.339 0 1.273-.08 2.802-.241l-1.346 7.338a19.843 19.843 0 01-4.342.482c-2.824 0-4.936-.644-6.336-1.93s-2.1-3.169-2.1-5.649c0-1.148.369-3.743 1.106-7.785l2.694-14.71h-4.145zM246.634 464.467c0-5.994 1.354-11.093 4.065-15.296 3.008-4.616 7.36-6.924 13.056-6.924 4.271 0 7.676 1.446 10.214 4.341 2.537 2.894 3.807 6.924 3.807 12.091 0 6.063-1.58 11.213-4.737 15.45-3.158 4.237-7.286 6.356-12.384 6.356-4.157 0-7.533-1.436-10.128-4.307-2.596-2.869-3.893-6.774-3.893-11.711zm23.115-7.063c0-2.411-.57-4.306-1.709-5.684-1.139-1.378-2.583-2.067-4.333-2.067-1.473 0-2.854.506-4.144 1.516-1.289 1.011-2.417 2.768-3.383 5.271a21.43 21.43 0 00-1.45 7.786c0 2.732.593 4.856 1.778 6.373 1.186 1.516 2.653 2.273 4.402 2.273 2.21 0 4.085-1.172 5.628-3.514 2.141-3.215 3.211-7.2 3.211-11.954zM287.105 443.073h7.711l-.795 4.754c1.993-2.089 3.819-3.542 5.48-4.357 1.661-.814 3.373-1.223 5.137-1.223 2.405 0 4.329.838 5.771 2.514 1.443 1.676 2.165 3.972 2.165 6.887 0 1.263-.296 3.686-.887 7.269l-3.515 20.742h-8.143l3.56-20.738c.525-3.101.788-4.96.788-5.58 0-1.31-.315-2.313-.944-3.015-.629-.7-1.448-1.051-2.454-1.051-1.098 0-2.219.437-3.364 1.31-1.647 1.239-2.872 2.716-3.672 4.426-.801 1.712-1.604 4.921-2.412 9.629l-2.594 15.02h-8.095l6.263-36.587z"/>
+ </g>
+</svg>
diff --git a/src/assets/images/_sila/login-company-logo.svg b/src/assets/images/_sila/login-company-logo.svg
new file mode 100644
index 00000000..d0fa158c
--- /dev/null
+++ b/src/assets/images/_sila/login-company-logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 241.23 240.05"><defs><style>.cls-1{fill:#a6a8ab;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:url(#linear-gradient-2);}.cls-4{fill:url(#linear-gradient-3);}.cls-5{fill:url(#linear-gradient-4);}.cls-6{fill:#626366;}</style><linearGradient id="linear-gradient" x1="82.9" y1="11.55" x2="82.9" y2="154.54" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00b0da"/><stop offset="1" stop-color="#008abf"/></linearGradient><linearGradient id="linear-gradient-2" x1="81.55" y1="27.55" x2="81.55" y2="158.66" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="156.66" y1="51.54" x2="156.66" y2="154.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a5d440"/><stop offset="1" stop-color="#8cce3f"/></linearGradient><linearGradient id="linear-gradient-4" x1="158.41" y1="51.54" x2="158.41" y2="154.8" xlink:href="#linear-gradient-3"/></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M241.23,205.77a18.66,18.66,0,1,0-.24,16L237.26,220a14.51,14.51,0,1,1,.21-12.46Z"/><path class="cls-2" d="M65.85,81.86a53.68,53.68,0,0,0,11.61,33.41c-.1.29-.15.6-.22.9a10.81,10.81,0,0,0-.34,2.57,11,11,0,1,0,11-11,10.75,10.75,0,0,0-1.2.07c-.31,0-.61.08-.91.13A42.82,42.82,0,0,1,99.95,43.86h0V2.07l-.77.21q-3.63.94-7.12,2.2c-1.29.47-2.58,1-3.84,1.48h0V38.19l-.13.1A53.79,53.79,0,0,0,65.85,81.86Z"/><path class="cls-3" d="M120.28,96.58a14.54,14.54,0,0,1-14.55-14.37H93.59v0a26.29,26.29,0,0,0,21,25.65v45.35A71.13,71.13,0,0,1,63.9,38.1c.31.06.63.1,1,.13s.64,0,1,0a10.83,10.83,0,1,0-10.25-7.41,82.23,82.23,0,0,0,64.18,133.6c1.41,0,2.81-.06,4.2-.14l1.63-.09h0V95.57A14.47,14.47,0,0,1,120.28,96.58Z"/><path class="cls-4" d="M171.95,68.54a53.78,53.78,0,0,0-9.85-19.71,11.31,11.31,0,0,0,.32-1.3,10.78,10.78,0,0,0,.24-2.17,11,11,0,1,0-8.89,10.8,42.83,42.83,0,0,1-14.17,64.08V162c1.08-.27,2.14-.56,3.2-.87a82.35,82.35,0,0,0,8.53-3V125.91a53.91,53.91,0,0,0,20.6-57.37Z"/><path class="cls-5" d="M184.63,132.75A82.21,82.21,0,0,0,119.79,0c-1.64,0-3.26.06-4.87.16h-.11V68.55h0A14.53,14.53,0,0,1,120,67.48h.27A14.56,14.56,0,0,1,134.87,82s0,.07,0,.11,0,.08,0,.13h11.08A26.21,26.21,0,0,0,125.81,56.8V11.3A71.14,71.14,0,0,1,176,125.83h-.07a11,11,0,0,0-12.58,10.88,11,11,0,0,0,11,11h0a11,11,0,0,0,10.54-14.13C184.82,133.3,184.73,133,184.63,132.75Z"/><polygon class="cls-1" points="201.22 231.42 201.22 195.65 196.53 195.65 182 225.14 167.47 195.65 162.72 195.65 162.72 231.42 166.89 231.42 166.89 204.21 180.33 231.42 183.67 231.42 197.1 204.21 197.1 231.42 201.22 231.42"/><path class="cls-6" d="M119.82,208.4a10.6,10.6,0,0,0-7.9-3.34,10.15,10.15,0,0,0-4.16.83,15.94,15.94,0,0,0-3.62,2.24v-2.7H99.91v26h4.23V216.64a7.74,7.74,0,0,1,2.08-5.5,7.48,7.48,0,0,1,10.66,0,7.76,7.76,0,0,1,2.08,5.48v14.78h4.11v-15a11.12,11.12,0,0,0-3.24-8"/><path class="cls-6" d="M63.68,224.4a12.41,12.41,0,0,1-4.86,5.17,13.54,13.54,0,0,1-7,1.85H45.54V240h-4V205.43H51.64a13.41,13.41,0,0,1,9.57,3.76,12.73,12.73,0,0,1,2.47,15.21m-18.14,3.24h5.77A9.48,9.48,0,0,0,58.05,225a8.59,8.59,0,0,0,2.76-6.54,8.38,8.38,0,0,0-2.7-6.41,9.43,9.43,0,0,0-6.68-2.51H45.54Z"/><path class="cls-6" d="M96.44,219.75a4.56,4.56,0,0,0,.14-1.36c0-7.38-6.27-13.36-14-13.36s-14,6-14,13.36,6.27,13.36,14,13.36a14,14,0,0,0,11.93-6.52l-3.25-2.45a9.89,9.89,0,0,1-8.68,5,9.43,9.43,0,1,1,0-18.83,9.8,9.8,0,0,1,9.35,6.54c0,.07.15.63.17.7H76.59v3.52Z"/><path class="cls-6" d="M33.19,213.53A14.53,14.53,0,1,1,18.66,199a14.53,14.53,0,0,1,14.53,14.53m4.12,0a18.66,18.66,0,1,0-18.66,18.66,18.66,18.66,0,0,0,18.66-18.66"/><path class="cls-1" d="M154.37,220.69a6.77,6.77,0,0,1-6.75,6.79H132.14V213.9h15.49a6.78,6.78,0,0,1,6.75,6.79m-2.29-15.93a5.08,5.08,0,0,1-5.05,5.08l-14.89,0V199.67H147a5.07,5.07,0,0,1,5.05,5.08m.94,6.7a9,9,0,0,0-5.69-15.75H128v35.75h20.14l.28,0v0A10.73,10.73,0,0,0,153,211.46"/></g></g></svg> \ No newline at end of file
diff --git a/src/assets/images/_sila/logo-header.svg b/src/assets/images/_sila/logo-header.svg
new file mode 100644
index 00000000..2dba9c4c
--- /dev/null
+++ b/src/assets/images/_sila/logo-header.svg
@@ -0,0 +1 @@
+<svg width="157" height="32" xmlns="http://www.w3.org/2000/svg"><g fill="#FFF" fill-rule="nonzero"><path d="M9.694 8.048c.016.761-.602 1.69-1.87 1.574-2.433 3.002-3.097 7.524-.99 11.37 1.078 1.966 2.637 3.435 4.661 4.409a10.04 10.04 0 003.743.981v-6.61c-1.17-.274-2.058-.918-2.626-1.978a3.713 3.713 0 01-.436-1.736h1.749c.046.723.343 1.313.954 1.72.607.405 1.264.455 1.96.227v9.957a11.43 11.43 0 01-4.16-.43c-2.385-.692-4.382-1.986-5.952-3.903C4.94 21.447 4.019 18.94 4 16.116c-.018-2.82.887-5.33 2.62-7.551-.34-1.039.44-1.988 1.329-2.06.97-.077 1.726.607 1.745 1.543zm12.485 2.994c-.017.08.01.191.06.257.897 1.191 1.45 2.524 1.562 4.006.217 2.864-.825 5.2-3.079 6.998-.044.035-.089.068-.132.103-.009.007-.015.016-.031.033v4.714l-1.692.561v-6.1c1.811-1.02 2.956-2.53 3.286-4.592.28-1.75-.154-3.342-1.212-4.774-.192 0-.383.019-.57-.003-.765-.088-1.374-.799-1.367-1.58.007-.802.62-1.493 1.398-1.577 1.144-.124 2.01.833 1.777 1.954zM16.372 4c.325.03.652.054.977.092 2.154.251 4.107 1.017 5.833 2.323 2.446 1.849 4.007 4.28 4.583 7.283.668 3.482-.094 6.666-2.202 9.527l-.103.141c.385 1.137-.411 2.074-1.344 2.18-.87.1-1.687-.57-1.76-1.472a1.61 1.61 0 011.834-1.707c2.157-2.655 3.036-6.877 1.246-10.714-1.976-4.235-5.944-5.847-8.593-5.997v6.625c1.82.567 2.806 1.787 2.95 3.7h-1.61c-.033-.74-.326-1.345-.947-1.766-.618-.417-1.286-.47-1.992-.226V4.04L15.61 4h.76zm-3.328.286v6.084c-1.916 1.087-3.064 2.704-3.309 4.902-.182 1.638.261 3.123 1.255 4.447a1.677 1.677 0 011.154.21c.597.355.888 1.099.7 1.796-.174.646-.806 1.137-1.479 1.15-1.09.02-1.842-.918-1.602-1.992a.244.244 0 00-.033-.18c-1.205-1.579-1.754-3.367-1.61-5.337.171-2.357 1.235-4.259 3.105-5.708l.14-.107V4.84l1.68-.553zM42.25 22c.476 0 .917-.049 1.323-.147a2.723 2.723 0 001.043-.483c.29-.224.516-.518.679-.882.163-.364.245-.807.245-1.33 0-.672-.17-1.232-.511-1.68-.34-.448-.861-.723-1.561-.826v-.028c.205-.037.404-.105.595-.203.191-.098.362-.233.511-.406.15-.173.27-.387.364-.644.093-.257.14-.562.14-.917 0-.373-.06-.716-.182-1.029a1.973 1.973 0 00-.581-.805c-.266-.224-.609-.397-1.029-.518-.42-.121-.924-.182-1.512-.182h-3.64V22h4.116zm-.756-5.628h-2.408v-3.64h2.198c.523 0 .964.03 1.323.091.36.06.651.159.875.294.224.135.387.313.49.532.103.22.154.488.154.805 0 .308-.044.581-.133.819a1.369 1.369 0 01-.441.602c-.205.163-.476.287-.812.371-.336.084-.751.126-1.246.126zm.014 4.816h-2.422v-4.004h2.534c.448 0 .854.026 1.218.077.364.051.677.147.938.287.261.14.462.338.602.595.14.257.21.595.21 1.015 0 .439-.063.791-.189 1.057a1.369 1.369 0 01-.567.616c-.252.145-.572.24-.959.287-.387.047-.842.07-1.365.07zm8.414 1.008c.317 0 .597-.04.84-.119a2.772 2.772 0 001.12-.679c.14-.14.266-.275.378-.406V22h.84v-7.252h-.84v3.948c0 .401-.051.77-.154 1.106a2.705 2.705 0 01-.434.868 1.946 1.946 0 01-.693.567 2.083 2.083 0 01-.931.203c-.663 0-1.139-.166-1.428-.497-.29-.331-.434-.81-.434-1.435v-4.76h-.84v4.774c0 .383.044.735.133 1.057.089.322.233.604.434.847.2.243.464.432.791.567.327.135.733.203 1.218.203zm6.216-9.03V11.92h-.924v1.246h.924zM56.096 22v-7.252h-.84V22h.84zm3.108 0V11.92h-.84V22h.84zm3.724.196c.205 0 .392-.023.56-.07.168-.047.303-.08.406-.098v-.686a4.055 4.055 0 01-.343.07 2.17 2.17 0 01-.343.028c-.177 0-.315-.019-.413-.056a.402.402 0 01-.217-.182.775.775 0 01-.084-.322 6.594 6.594 0 01-.014-.462v-4.97h1.358v-.7H62.48v-2.016h-.84v2.016h-1.022v.7h1.022v5.306c0 .541.11.917.329 1.127.22.21.539.315.959.315zm9.086 0a3.4 3.4 0 001.379-.273 2.99 2.99 0 001.064-.777 3.53 3.53 0 00.679-1.204c.159-.467.238-.99.238-1.568 0-.532-.068-1.031-.203-1.498a3.352 3.352 0 00-.623-1.211c-.28-.34-.63-.611-1.05-.812-.42-.2-.915-.301-1.484-.301-.56 0-1.05.1-1.47.301-.42.2-.77.474-1.05.819a3.52 3.52 0 00-.63 1.211c-.14.462-.21.959-.21 1.491 0 .635.089 1.19.266 1.666.177.476.418.873.721 1.19.303.317.658.555 1.064.714.406.159.842.243 1.309.252zm0-.756a2.41 2.41 0 01-1.064-.224 2.106 2.106 0 01-.77-.63 2.904 2.904 0 01-.469-.973 4.523 4.523 0 01-.161-1.239c0-.392.047-.772.14-1.141.093-.369.238-.695.434-.98.196-.285.45-.513.763-.686.313-.173.688-.259 1.127-.259.448 0 .826.08 1.134.238.308.159.56.376.756.651.196.275.34.6.434.973.093.373.14.775.14 1.204 0 .392-.047.77-.14 1.134-.093.364-.238.69-.434.98-.196.29-.45.52-.763.693-.313.173-.688.259-1.127.259zm5.74.56v-3.906c0-.252.01-.485.028-.7a2.67 2.67 0 01.154-.672 2.079 2.079 0 01.721-.98c.173-.13.366-.236.581-.315.215-.08.453-.119.714-.119.672 0 1.153.163 1.442.49.29.327.434.817.434 1.47V22h.84v-4.746c0-.299-.016-.586-.049-.861a1.806 1.806 0 00-.315-.805c-.261-.383-.583-.651-.966-.805a3.307 3.307 0 00-1.246-.231c-.504 0-.936.112-1.295.336-.36.224-.707.513-1.043.868v-1.008h-.84V22h.84zm15.199.294c1.654 0 2.928-.583 3.821-1.75.761-.994 1.142-2.24 1.142-3.74 0-1.385-.333-2.522-.998-3.41-.853-1.14-2.165-1.71-3.938-1.71-1.695 0-2.976.62-3.842 1.86-.674.966-1.011 2.128-1.011 3.486 0 1.504.394 2.748 1.182 3.733.884 1.02 2.099 1.53 3.644 1.53zm.164-1.217c-1.263 0-2.175-.382-2.738-1.145-.563-.763-.844-1.7-.844-2.813 0-1.39.328-2.441.984-3.155.656-.713 1.504-1.07 2.543-1.07 1.071 0 1.915.36 2.533 1.08.617.72.926 1.67.926 2.851 0 1.117-.268 2.105-.803 2.964-.536.859-1.403 1.288-2.601 1.288zm7.39 3.842v-3.74c.268.333.512.568.73.705.375.241.837.362 1.389.362.701 0 1.317-.216 1.845-.65.834-.683 1.251-1.822 1.251-3.417 0-1.18-.29-2.089-.868-2.724-.579-.636-1.283-.954-2.112-.954-.56 0-1.048.137-1.463.41-.292.182-.56.44-.807.773v-.971h-1.196v10.206h1.23zm1.954-3.746c-.847 0-1.43-.378-1.75-1.135-.168-.396-.252-.877-.252-1.442 0-.702.084-1.28.252-1.737.315-.852.898-1.278 1.75-1.278.848 0 1.431.403 1.75 1.21.17.42.253.92.253 1.504 0 .957-.19 1.676-.57 2.157-.38.48-.858.72-1.433.72zm7.554 1.087c.374 0 .709-.037 1.005-.11a2.867 2.867 0 001.388-.738c.223-.21.425-.48.605-.81.18-.33.286-.63.318-.899h-1.21a2.143 2.143 0 01-.465.855c-.383.423-.896.635-1.538.635-.688 0-1.197-.224-1.525-.673-.328-.449-.503-1.045-.526-1.788h5.366c0-.738-.036-1.276-.11-1.613a3.266 3.266 0 00-.491-1.292c-.256-.387-.627-.703-1.115-.947a3.34 3.34 0 00-1.51-.365c-1.03 0-1.864.367-2.502 1.1-.638.734-.957 1.689-.957 2.864 0 1.195.316 2.123.95 2.786.633.663 1.406.995 2.317.995zm2.154-4.505H108.1c.023-.629.226-1.147.609-1.555a1.872 1.872 0 011.421-.612c.789 0 1.352.296 1.689.889.182.319.298.745.349 1.278zM116.174 22v-3.835c0-.478.031-.846.093-1.104.061-.257.194-.507.4-.748.255-.301.551-.502.888-.602.187-.06.426-.089.718-.089.574 0 .97.228 1.19.684.132.273.198.633.198 1.08V22h1.25v-4.696c0-.739-.1-1.306-.3-1.702-.365-.725-1.067-1.087-2.106-1.087-.474 0-.904.093-1.292.28-.387.187-.754.494-1.1.923v-1.04h-1.17V22h1.231zm10.931 0c1.322 0 2.286-.392 2.892-1.176a2.8 2.8 0 00.608-1.757c0-.77-.226-1.374-.677-1.811-.255-.246-.64-.467-1.155-.663.35-.178.62-.374.807-.588.36-.406.54-.925.54-1.559 0-.542-.146-1.02-.438-1.435-.497-.702-1.333-1.053-2.509-1.053h-4.313V22h4.245zm-.437-5.797h-2.475v-3.11h2.427c.592 0 1.039.064 1.34.191.533.228.8.67.8 1.326 0 .652-.251 1.103-.753 1.354-.319.16-.765.24-1.34.24zm.458 4.635h-2.933v-3.534h2.7c.57 0 1.028.07 1.374.212.652.264.978.76.978 1.49 0 .433-.112.802-.335 1.107-.356.483-.95.725-1.784.725zm6.37 1.162v-6.036a64.19 64.19 0 00-.016-1.002 54.71 54.71 0 01-.018-1.124v-.308l2.885 8.47h1.347l2.864-8.47c0 .543-.004 1.05-.014 1.525-.009.474-.013.813-.013 1.018V22h1.292V11.958h-1.928l-2.864 8.49-2.885-8.49h-1.948V22h1.299zm14.452.26c1.353 0 2.429-.43 3.226-1.292.666-.716 1.058-1.602 1.176-2.66h-1.326c-.137.661-.344 1.195-.622 1.6-.524.77-1.288 1.155-2.29 1.155-1.09 0-1.908-.365-2.458-1.097-.549-.731-.823-1.685-.823-2.86 0-1.436.304-2.501.912-3.196.609-.695 1.407-1.043 2.396-1.043.811 0 1.441.19 1.89.568.45.378.749.89.9 1.538h1.325c-.077-.853-.467-1.614-1.168-2.284-.702-.67-1.689-1.004-2.96-1.004-1.49 0-2.66.515-3.507 1.544-.784.948-1.176 2.163-1.176 3.644 0 1.95.522 3.393 1.565 4.327.793.707 1.773 1.06 2.94 1.06z"/></g></svg> \ No newline at end of file
diff --git a/src/assets/styles/_obmc-sila.scss b/src/assets/styles/_obmc-sila.scss
new file mode 100644
index 00000000..ea2507f0
--- /dev/null
+++ b/src/assets/styles/_obmc-sila.scss
@@ -0,0 +1,6 @@
+// Vendor styles
+@import "./bootstrap";
+@import "~bootstrap-vue/src/index";
+
+// IBS BMC styles
+@import "./bmc/_sila";
diff --git a/src/assets/styles/bmc/_sila/_alert.scss b/src/assets/styles/bmc/_sila/_alert.scss
new file mode 100644
index 00000000..0e78ba64
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_alert.scss
@@ -0,0 +1,70 @@
+.alert {
+ display: flex;
+ padding: $spacer;
+ border-width: 0 0 0 3px;
+ color: gray("800");
+ margin-bottom: $spacer;
+
+ &.small {
+ padding: $spacer / 2;
+ font-size: 1rem;
+ }
+
+ .close {
+ font-weight: 300;
+ opacity: 1;
+ }
+
+ .alert-icon {
+ display: inline-flex;
+ align-items: flex-start;
+ margin-right: $spacer;
+ margin-bottom: $spacer;
+
+ @include media-breakpoint-up(sm) {
+ margin-bottom: 0;
+ }
+ }
+
+ .alert-content {
+ flex: 1 1 auto;
+ }
+
+ .alert-title {
+ margin-bottom: $spacer / 2;
+ }
+
+ .alert-msg {
+ p + p {
+ margin-bottom: $spacer;
+ }
+
+ p:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+
+ &.alert-info {
+ border-left-color: theme-color("info");
+ background-color: theme-color-light("info");
+ fill: theme-color("info");
+ }
+
+ &.alert-success {
+ border-left-color: theme-color("success");
+ background-color: theme-color-light("success");
+ fill: theme-color("success");
+ }
+
+ &.alert-danger {
+ border-left-color: theme-color("danger");
+ background-color: theme-color-light("danger");
+ fill: theme-color("danger");
+ }
+
+ &.alert-warning {
+ border-left-color: theme-color("warning");
+ background-color: theme-color-light("warning");
+ fill: theme-color("warning");
+ }
+ } \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_badge.scss b/src/assets/styles/bmc/_sila/_badge.scss
new file mode 100644
index 00000000..0b88b499
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_badge.scss
@@ -0,0 +1,21 @@
+.badge-pill {
+ // Need to explicitly set border-radius
+ // for pill variant because global $enable-rounded
+ // Bootstrap setting removes rounded pill style
+ border-radius: 10rem;
+ fill: currentColor;
+ font-weight: 400;
+ line-height: 1.5;
+ display: inline-flex;
+ .close {
+ font-size: 1em;
+ margin-left: $spacer/2;
+ font-weight: inherit;
+ color: inherit;
+ }
+}
+
+.badge-primary {
+ background-color: theme-color-light("info");
+ color: theme-color("info");
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_base.scss b/src/assets/styles/bmc/_sila/_base.scss
new file mode 100644
index 00000000..c11e046c
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_base.scss
@@ -0,0 +1,50 @@
+dt,
+legend,
+label {
+ color: gray("800");
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.4285;
+}
+
+h1,
+.h1 {
+ font-size: 2.625rem;
+ font-weight: 300;
+ line-height: 1.238;
+}
+
+h2,
+.h2 {
+ font-size: 2.25rem;
+ font-weight: 300;
+ line-height: 1.3333;
+}
+
+h3,
+.h3 {
+ font-size: 1.75rem;
+ font-weight: 400;
+ line-height: 1.2857;
+}
+
+h4,
+.h4 {
+ font-size: 1.25rem;
+ font-weight: 400;
+ line-height: 1.3;
+}
+
+h5,
+.h5 {
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1.375;
+}
+
+h6,
+.h6 {
+ font-size: 0.875rem;
+ font-weight: 500;
+ line-height: 1.2857;
+}
diff --git a/src/assets/styles/bmc/_sila/_bootstrap-grid.scss b/src/assets/styles/bmc/_sila/_bootstrap-grid.scss
new file mode 100644
index 00000000..7ad7c81b
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_bootstrap-grid.scss
@@ -0,0 +1,8 @@
+.container-xl {
+ // Fluid layout container class sets 100%
+ // width until xl breakpoint. Once a max-width
+ // is set, setting the left margin to 0 is needed
+ // so the content doesn't center align
+ // https://bootstrap-vue.org/docs/components/layout#fluid-width-container
+ margin-left: 0;
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_buttons.scss b/src/assets/styles/bmc/_sila/_buttons.scss
new file mode 100644
index 00000000..2a7b8169
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_buttons.scss
@@ -0,0 +1,82 @@
+.btn {
+ padding-top: $spacer / 2;
+ padding-right: $spacer;
+ padding-bottom: $spacer / 2;
+ padding-left: $spacer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-around;
+ svg {
+ margin-right: $spacer / 4;
+ }
+ &:disabled {
+ color: gray("600");
+ fill: currentColor;
+ box-shadow: none !important;
+ &:not(.btn-link) {
+ border-color: gray("400");
+ background-color: gray("400");
+ }
+ }
+}
+
+.btn-primary {
+ fill: currentColor;
+ &:focus,
+ &:not(:disabled):not(.disabled):active:focus {
+ border-color: $white;
+ box-shadow: inset 0 0 0 3px theme-color('primary'), inset 0 0 0 5px $white;
+ }
+}
+
+.btn-secondary {
+ fill: currentColor;
+ &:focus,
+ &:not(:disabled):not(.disabled):active:focus {
+ border-color: $white;
+ box-shadow: inset 0 0 0 3px theme-color('secondary'), inset 0 0 0 5px $white;
+ }
+}
+
+// Global style for all button link
+.btn-link {
+ font-weight: $headings-font-weight;
+ fill: theme-color("primary");
+ text-decoration: none !important;
+ &:hover {
+ background-color: gray("200");
+ color: theme-color("primary");
+ }
+ &:active {
+ background-color: gray("300");
+ }
+ &:focus {
+ box-shadow: inset 0 0 0 2px theme-color("primary");
+ color: theme-color("primary");
+ outline: none;
+ }
+ &:disabled {
+ box-shadow: $btn-focus-box-shadow;
+ }
+}
+
+// Icon only buttons
+.btn-icon-only svg {
+ margin-right: 0;
+}
+
+// Datepicker, clear search and Password toggle buttons
+.input-action-btn,
+.btn-datepicker {
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: $zindex-dropdown + 1;
+}
+
+// Contain input buttons within input
+.btn-datepicker .dropdown-toggle,
+.input-action-btn {
+ padding: 7px;
+ margin: 1px;
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_calendar.scss b/src/assets/styles/bmc/_sila/_calendar.scss
new file mode 100644
index 00000000..0307a6ce
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_calendar.scss
@@ -0,0 +1,17 @@
+.b-calendar-nav {
+ .btn {
+ &:hover {
+ background: none;
+ color: theme-color("dark");
+ }
+ }
+}
+
+.b-calendar-grid .btn {
+ display: inline-block;
+}
+
+// Date picker focus
+.b-calendar .b-calendar-grid {
+ padding: 6px 12px;
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_card.scss b/src/assets/styles/bmc/_sila/_card.scss
new file mode 100644
index 00000000..5f2a5962
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_card.scss
@@ -0,0 +1,5 @@
+.card {
+ .bg-success {
+ background-color: theme-color-light('success')!important;
+ }
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_dropdown.scss b/src/assets/styles/bmc/_sila/_dropdown.scss
new file mode 100644
index 00000000..969c4c68
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_dropdown.scss
@@ -0,0 +1,31 @@
+// Make calendar visible over the table
+.dropdown-menu {
+ z-index: $zindex-dropdown + 1;
+ padding: 0;
+}
+.dropdown-item {
+ padding-left: $spacer/4;
+ margin-top: -1 * $spacer/4;
+}
+.b-dropdown-form {
+ padding: $spacer/2;
+ .form-group {
+ margin-bottom: $spacer/2;
+ }
+}
+// Table filter dropdown clear button style
+.table-filter {
+ .dropdown-item {
+ &:hover {
+ background-color: gray("200");
+ }
+ &:active {
+ background-color: gray("300");
+ }
+ &:focus {
+ outline: none;
+ background-color: transparent;
+ box-shadow: inset 0 0 0 2px theme-color("primary");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_forms.scss b/src/assets/styles/bmc/_sila/_forms.scss
new file mode 100644
index 00000000..428a40c2
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_forms.scss
@@ -0,0 +1,132 @@
+// Helper text
+.form-text {
+ font-size: $form-label-font-size;
+ line-height: $form-line-height;
+ margin-top: -$spacer / 4;
+ margin-bottom: $spacer / 2;
+ color: gray("700")!important;
+}
+
+// Legend label
+.col-form-label {
+ color: gray("800");
+ font-size: $form-label-font-size;
+ line-height: $form-line-height;
+}
+
+.form-group {
+ margin-bottom: $spacer * 2;
+}
+
+.custom-select,
+.form-control,
+.input-group-text {
+ border-color: gray("500") !important;
+ background-color: gray("100");
+}
+
+.custom-select,
+.form-control {
+ &:active {
+ border: 1px solid $primary!important;
+ }
+ &:focus {
+ color: theme-color("dark");
+ background-color: gray("100");
+ box-shadow: inset 0 0 0 3px gray("100"), inset 0 0 0 5px $primary !important;
+ }
+ &:disabled {
+ background-color: gray("400");
+ color: gray("600");
+ }
+ &::placeholder {
+ color: gray("600");
+ }
+ &.is-invalid,
+ &:invalid {
+ border: 1px solid theme-color("danger") !important;
+ }
+}
+
+.custom-select,
+.custom-control-label,
+.form-control {
+ color: theme-color("dark") !important;
+ font-size: 1rem;
+}
+
+// Inverted form colors
+.form-background {
+ background-color: gray("100");
+ .custom-select,
+ .form-control {
+ background-color: $white;
+ &:focus {
+ background-color: $white;
+ }
+ &:disabled {
+ background-color: gray("400");
+ color: gray("600");
+ }
+ }
+}
+
+.invalid-feedback {
+ font-size: $form-label-font-size;
+ line-height: $form-line-height;
+}
+
+.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after,
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before,
+.custom-control-input:checked ~ .custom-control-label::before {
+ background-color: $black;
+ border-color: $black;
+ cursor: pointer;
+}
+
+.custom-control {
+ .custom-control-input[disabled=disabled] {
+ & + .custom-control-label {
+ // Disabled label for checkbox, radio,
+ // switch bootstrap form components
+ color: gray("600")!important;
+ }
+ }
+}
+
+.custom-control-input:focus ~ .custom-control-label::before{
+ box-shadow: 0 0 0 2px theme-color("primary");
+}
+
+.custom-control-label::after {
+ cursor: pointer;
+}
+
+.b-form-tag-remove {
+ // X button to remove tag
+ font-weight: normal;
+}
+
+.b-form-tags-button {
+ // Add button inside input field
+ white-space: nowrap;
+ margin-right: -$spacer;
+ &.btn-link-primary {
+ color: theme-color("primary");
+ fill: currentColor;
+ }
+}
+
+// Form validation icon
+ .form-control.is-invalid,
+ .form-control.is-valid {
+ background-position: right 1rem bottom 50%;
+ }
+
+// Form validation icon with datepicker or password toggle icon
+.form-control-with-button {
+ &.is-invalid,
+ &.is-valid {
+ background-position: right 3rem bottom 50%;
+ }
+}
diff --git a/src/assets/styles/bmc/_sila/_index.scss b/src/assets/styles/bmc/_sila/_index.scss
new file mode 100644
index 00000000..74594e35
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_index.scss
@@ -0,0 +1,18 @@
+// OpenBMC Global Style Overrides of out of the box
+// Bootstrap styles
+@import "./alert";
+@import "./badge";
+@import "./base";
+@import "./bootstrap-grid";
+@import "./buttons";
+@import "./calendar";
+@import "./card";
+@import "./dropdown";
+@import "./forms";
+@import "./kvm";
+@import "./modal";
+@import "./pagination";
+@import "./section-divider";
+@import "./sol";
+@import "./tables";
+@import "./toasts"; \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_kvm.scss b/src/assets/styles/bmc/_sila/_kvm.scss
new file mode 100644
index 00000000..a7223844
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_kvm.scss
@@ -0,0 +1,12 @@
+#terminal-kvm {
+ height: calc(100vh - 300px);
+ display: flex;
+ &.full-window {
+ height: calc(100vh - 80px);
+ }
+ div:nth-child(1) {
+ background: transparent !important;
+ display: block !important;
+ overflow: hidden !important;
+ }
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_modal.scss b/src/assets/styles/bmc/_sila/_modal.scss
new file mode 100644
index 00000000..e2fa0cd8
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_modal.scss
@@ -0,0 +1,12 @@
+.modal-header {
+ .close {
+ font-weight: normal;
+ color: theme-color("dark");
+ opacity: 1;
+ }
+ .modal-title {
+ font-size: 1.25rem;
+ font-weight: normal;
+ line-height: 1.3;
+ }
+}
diff --git a/src/assets/styles/bmc/_sila/_pagination.scss b/src/assets/styles/bmc/_sila/_pagination.scss
new file mode 100644
index 00000000..d38ce5d2
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_pagination.scss
@@ -0,0 +1,24 @@
+.table-pagination-select {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ select {
+ width: fit-content;
+ }
+ label {
+ margin-left: $spacer;
+ line-height: $spacer * 2;
+ }
+}
+
+.b-pagination {
+ @include media-breakpoint-up(sm) {
+ justify-content: flex-end;
+ }
+ .page-item.active button {
+ color: theme-color("dark");
+ background-color: color("white");
+ border-color: $border-color;
+ box-shadow: inset 0px -3px theme-color("primary");
+ }
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_section-divider.scss b/src/assets/styles/bmc/_sila/_section-divider.scss
new file mode 100644
index 00000000..620c9e56
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_section-divider.scss
@@ -0,0 +1,3 @@
+.section-divider {
+ border-bottom: 1px solid gray('400');
+ } \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_sol.scss b/src/assets/styles/bmc/_sila/_sol.scss
new file mode 100644
index 00000000..6987cf79
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_sol.scss
@@ -0,0 +1,3 @@
+#terminal .xterm .xterm-viewport {
+ overflow: auto;
+} \ No newline at end of file
diff --git a/src/assets/styles/bmc/_sila/_tables.scss b/src/assets/styles/bmc/_sila/_tables.scss
new file mode 100644
index 00000000..e8b5a832
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_tables.scss
@@ -0,0 +1,171 @@
+.table {
+ position: relative;
+ z-index: $zindex-dropdown;
+
+ td {
+ border-top: 1px solid gray("300");
+ border-bottom: 1px solid gray("300");
+ &:first-of-type {
+ border-left: 1px solid gray("300");
+ }
+ &:last-of-type {
+ border-right: 1px solid gray("300");
+ }
+ vertical-align: middle;
+
+ // Table action buttons
+ .btn-link {
+ width: 40px;
+ height: 40px;
+ padding: 5px !important;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+
+ // thead-light added for specificity
+ .thead-light th {
+ vertical-align: middle;
+ border-top: 1px solid gray("300");
+ border-bottom: 1px solid gray("300");
+ &:first-of-type {
+ border-left: 1px solid gray("300");
+ }
+ &:last-of-type {
+ border-right: 1px solid gray("300");
+ }
+ color: theme-color("dark");
+ &:focus {
+ outline: none;
+ }
+ }
+
+ .status-icon svg {
+ width: 1rem;
+ height: auto;
+ }
+
+ .b-table-has-details {
+ td {
+ border-bottom: none;
+ }
+ .table-row-expand svg {
+ transform: rotate(180deg);
+ }
+ }
+
+ .b-table-details {
+ background-color: theme-color("light");
+ td {
+ padding-left: calc(50px + (#{$table-cell-padding} * 2));
+ padding-right: calc(50px + (#{$table-cell-padding} * 2));
+ }
+ dl {
+ margin: 0;
+ }
+ dt {
+ float: left;
+ clear: left;
+ margin-right: $spacer / 2;
+ }
+ dd {
+ line-height: 1.2
+ }
+ }
+
+ .table-row-expand {
+ width: 50px;
+ .btn {
+ padding: 0;
+ width: 50px;
+ }
+ svg {
+ fill: theme-color("dark");
+ }
+ }
+ .b-table-sort-icon-left {
+ background-position: left calc(1.5rem / 2) center !important;
+ padding-left: calc(1.2rem + 0.65em) !important;
+ &:focus {
+ outline: none;
+ box-shadow: inset 0 0 0 2px theme-color('primary') !important;
+ }
+ &:hover {
+ background-color: theme-color-dark('light');
+ }
+ }
+}
+
+.b-table-sticky-header td {
+ border-top: none;
+}
+
+// Table stacked style for small screen only
+@include media-breakpoint-down(xs) {
+ .b-table-stacked-sm {
+ border: 1px solid gray("300");
+
+ tr {
+
+ &:not(:first-child) > td[aria-colindex='1'] {
+ border-top: 1px solid gray("300");
+ padding-top: 0.625rem;
+ }
+
+ &:not(.b-table-empty-row) {
+ position: relative; // Restrict background color to get zebra striping for the row
+
+ &::before,
+ &::after {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ z-index: -1;
+ }
+
+ &:before {
+ content: '';
+ background-color: gray("200");
+ width: 40%;
+ border-right: 1px solid gray("300");
+ }
+
+ &:after {
+ content: '';
+ right: 0;
+ width: 60%;
+ }
+
+ &:nth-child(even)::after {
+ background-color: gray("100"); // Zebra striping for the row
+ }
+ }
+
+ td {
+ border: 0;
+ padding: 0.75rem;
+ text-align: left !important;
+
+ &:last-of-type {
+ border-right: 0;
+ }
+ }
+ }
+ }
+
+ .table.b-table.b-table-stacked-sm > tbody > tr > [data-label] {
+ &::before {
+ text-align: left;
+ padding-left: $spacer /2;
+ }
+
+ > div {
+ padding-left: 1rem;
+ }
+ }
+
+ .table.b-table.b-table-stacked-sm > tbody > tr > :first-child {
+ border-top-width: 1px;
+ }
+}
diff --git a/src/assets/styles/bmc/_sila/_toasts.scss b/src/assets/styles/bmc/_sila/_toasts.scss
new file mode 100644
index 00000000..4e2ad7fa
--- /dev/null
+++ b/src/assets/styles/bmc/_sila/_toasts.scss
@@ -0,0 +1,61 @@
+.b-toaster {
+ top: 75px!important; // make sure toasts do not hide top header
+}
+
+// Toast component and status icon style
+.toast {
+ padding: $spacer/2 $spacer/2 $spacer/2 $spacer+2;
+ border-width: 0 0 0 3px;
+ box-shadow: $box-shadow;
+ .close {
+ font-weight: 300;
+ opacity: 1;
+ }
+}
+
+.toast-header {
+ display: flex;
+ align-items: flex-start;
+ background-color: inherit!important; //override specificity
+ border: none;
+ color: theme-color("dark")!important; //override specificity
+ padding-bottom: 0;
+}
+
+.toast-icon {
+ display: flex;
+ margin-right: 1rem;
+
+ svg {
+ margin-left: -2.5rem;
+ }
+
+ + .close {
+ line-height: .9;
+ }
+}
+
+.toast-body {
+ color: theme-color("dark");
+ padding-top: 0;
+}
+
+.b-toast-success .toast {
+ border-left-color: theme-color("success")!important;
+ background-color: theme-color-light("success")!important;
+}
+
+.b-toast-info .toast {
+ border-left-color: theme-color("info")!important;
+ background-color: theme-color-light("info")!important;
+}
+
+.b-toast-danger .toast {
+ border-left-color: theme-color("danger")!important;
+ background-color: theme-color-light("danger")!important;
+}
+
+.b-toast-warning .toast {
+ border-left-color: theme-color("warning")!important;
+ background-color: theme-color-light("warning")!important;
+} \ No newline at end of file
diff --git a/src/components/_sila/AppHeader/AppHeader.vue b/src/components/_sila/AppHeader/AppHeader.vue
new file mode 100644
index 00000000..84e4588f
--- /dev/null
+++ b/src/components/_sila/AppHeader/AppHeader.vue
@@ -0,0 +1,384 @@
+<template>
+ <div>
+ <header id="page-header">
+ <a
+ class="link-skip-nav btn btn-light"
+ href="#main-content"
+ @click="setFocus"
+ >
+ {{ $t('appHeader.skipToContent') }}
+ </a>
+
+ <b-navbar type="dark" :aria-label="$t('appHeader.applicationHeader')">
+ <!-- Left aligned nav items -->
+ <b-button
+ id="app-header-trigger"
+ class="nav-trigger"
+ aria-hidden="true"
+ type="button"
+ variant="link"
+ :class="{ open: isNavigationOpen }"
+ @click="toggleNavigation"
+ >
+ <icon-close
+ v-if="isNavigationOpen"
+ :title="$t('appHeader.titleHideNavigation')"
+ />
+ <icon-menu
+ v-if="!isNavigationOpen"
+ :title="$t('appHeader.titleShowNavigation')"
+ />
+ </b-button>
+ <b-navbar-nav>
+ <b-navbar-brand
+ class="mr-0"
+ to="/"
+ data-test-id="appHeader-container-overview"
+ >
+ <img
+ class="header-logo"
+ src="@/assets/images/logo-header.svg"
+ :alt="altLogo"
+ />
+ </b-navbar-brand>
+ <div v-if="isNavTagPresent" :key="routerKey" class="pl-2 nav-tags">
+ <span>|</span>
+ <span class="pl-3 asset-tag">{{ assetTag }}</span>
+ <span class="pl-3">{{ modelType }}</span>
+ <span class="pl-3">{{ serialNumber }}</span>
+ </div>
+ </b-navbar-nav>
+ <!-- Right aligned nav items -->
+ <b-navbar-nav class="ml-auto helper-menu">
+ <b-nav-item
+ to="/logs/event-logs"
+ data-test-id="appHeader-container-health"
+ >
+ <status-icon :status="healthStatusIcon" />
+ {{ $t('appHeader.health') }}
+ </b-nav-item>
+ <b-nav-item
+ to="/operations/server-power-operations"
+ data-test-id="appHeader-container-power"
+ >
+ <status-icon :status="serverStatusIcon" />
+ {{ $t('appHeader.power') }}
+ </b-nav-item>
+ <!-- Using LI elements instead of b-nav-item to support semantic button elements -->
+ <li class="nav-item">
+ <b-button
+ id="app-header-refresh"
+ variant="link"
+ data-test-id="appHeader-button-refresh"
+ @click="refresh"
+ >
+ <icon-renew :title="$t('appHeader.titleRefresh')" />
+ <span class="responsive-text">{{ $t('appHeader.refresh') }}</span>
+ </b-button>
+ </li>
+ <li class="nav-item">
+ <b-dropdown
+ id="app-header-user"
+ variant="link"
+ right
+ data-test-id="appHeader-container-user"
+ >
+ <template #button-content>
+ <icon-avatar :title="$t('appHeader.titleProfile')" />
+ <span class="responsive-text">{{ username }}</span>
+ </template>
+ <b-dropdown-item
+ to="/profile-settings"
+ data-test-id="appHeader-link-profile"
+ >{{ $t('appHeader.profileSettings') }}
+ </b-dropdown-item>
+ <b-dropdown-item
+ data-test-id="appHeader-link-logout"
+ @click="logout"
+ >
+ {{ $t('appHeader.logOut') }}
+ </b-dropdown-item>
+ </b-dropdown>
+ </li>
+ </b-navbar-nav>
+ </b-navbar>
+ </header>
+ <loading-bar />
+ </div>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconAvatar from '@carbon/icons-vue/es/user--avatar/20';
+import IconClose from '@carbon/icons-vue/es/close/20';
+import IconMenu from '@carbon/icons-vue/es/menu/20';
+import IconRenew from '@carbon/icons-vue/es/renew/20';
+import StatusIcon from '@/components/Global/StatusIcon';
+import LoadingBar from '@/components/Global/LoadingBar';
+
+export default {
+ name: 'AppHeader',
+ components: {
+ IconAvatar,
+ IconClose,
+ IconMenu,
+ IconRenew,
+ StatusIcon,
+ LoadingBar,
+ },
+ mixins: [BVToastMixin],
+ props: {
+ routerKey: {
+ type: Number,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ isNavigationOpen: false,
+ altLogo: process.env.VUE_APP_COMPANY_NAME || 'Built on OpenBMC',
+ };
+ },
+ computed: {
+ isNavTagPresent() {
+ return this.assetTag || this.modelType || this.serialNumber;
+ },
+ assetTag() {
+ return this.$store.getters['global/assetTag'];
+ },
+ modelType() {
+ return this.$store.getters['global/modelType'];
+ },
+ serialNumber() {
+ return this.$store.getters['global/serialNumber'];
+ },
+ isAuthorized() {
+ return this.$store.getters['global/isAuthorized'];
+ },
+ serverStatus() {
+ return this.$store.getters['global/serverStatus'];
+ },
+ healthStatus() {
+ return this.$store.getters['eventLog/healthStatus'];
+ },
+ serverStatusIcon() {
+ switch (this.serverStatus) {
+ case 'on':
+ return 'success';
+ case 'error':
+ return 'danger';
+ case 'diagnosticMode':
+ return 'warning';
+ case 'off':
+ default:
+ return 'secondary';
+ }
+ },
+ healthStatusIcon() {
+ switch (this.healthStatus) {
+ case 'OK':
+ return 'success';
+ case 'Warning':
+ return 'warning';
+ case 'Critical':
+ return 'danger';
+ default:
+ return 'secondary';
+ }
+ },
+ username() {
+ return this.$store.getters['global/username'];
+ },
+ },
+ watch: {
+ isAuthorized(value) {
+ if (value === false) {
+ this.errorToast(this.$t('global.toast.unAuthDescription'), {
+ title: this.$t('global.toast.unAuthTitle'),
+ });
+ }
+ },
+ },
+ created() {
+ // Reset auth state to check if user is authenticated based
+ // on available browser cookies
+ this.$store.dispatch('authentication/resetStoreState');
+ this.getSystemInfo();
+ this.getEvents();
+ },
+ mounted() {
+ this.$root.$on(
+ 'change-is-navigation-open',
+ (isNavigationOpen) => (this.isNavigationOpen = isNavigationOpen)
+ );
+ },
+ methods: {
+ getSystemInfo() {
+ this.$store.dispatch('global/getSystemInfo');
+ },
+ getEvents() {
+ this.$store.dispatch('eventLog/getEventLogData');
+ },
+ refresh() {
+ this.$emit('refresh');
+ },
+ logout() {
+ this.$store.dispatch('authentication/logout');
+ },
+ toggleNavigation() {
+ this.$root.$emit('toggle-navigation');
+ },
+ setFocus(event) {
+ event.preventDefault();
+ this.$root.$emit('skip-navigation');
+ },
+ },
+};
+</script>
+
+<style lang="scss">
+@mixin focus-box-shadow($padding-color: $navbar-color, $outline-color: $white) {
+ box-shadow: inset 0 0 0 3px $padding-color, inset 0 0 0 5px $outline-color;
+}
+.app-header {
+ .link-skip-nav {
+ position: absolute;
+ top: -60px;
+ left: 0.5rem;
+ z-index: $zindex-popover;
+ transition: $duration--moderate-01 $exit-easing--expressive;
+ &:focus {
+ top: 0.5rem;
+ transition-timing-function: $entrance-easing--expressive;
+ }
+ }
+ .navbar-text,
+ .nav-link,
+ .btn-link {
+ color: color('white') !important;
+ fill: currentColor;
+ padding: 0.68rem 1rem !important;
+
+ &:hover {
+ background-color: theme-color-level(light, 10);
+ }
+ &:active {
+ background-color: theme-color-level(light, 9);
+ }
+ &:focus {
+ @include focus-box-shadow;
+ outline: 0;
+ }
+ }
+
+ .nav-item {
+ fill: theme-color('light');
+ }
+
+ .navbar {
+ padding: 0;
+ background-color: $navbar-color;
+ @include media-breakpoint-up($responsive-layout-bp) {
+ height: $header-height;
+ }
+
+ .helper-menu {
+ @include media-breakpoint-down(sm) {
+ background-color: gray('800');
+ width: 100%;
+ justify-content: flex-end;
+
+ .nav-link,
+ .btn {
+ padding: $spacer / 1.125 $spacer / 2;
+ }
+
+ .nav-link:focus,
+ .btn:focus {
+ @include focus-box-shadow($gray-800);
+ }
+ }
+
+ .responsive-text {
+ @include media-breakpoint-down(xs) {
+ @include sr-only;
+ }
+ }
+ }
+ }
+
+ .navbar-nav {
+ @include media-breakpoint-up($responsive-layout-bp) {
+ padding: 0 $spacer;
+ }
+ align-items: center;
+
+ .navbar-brand,
+ .nav-link {
+ transition: $focus-transition;
+ }
+ .nav-tags {
+ color: theme-color-level(light, 3);
+ @include media-breakpoint-down(xs) {
+ @include sr-only;
+ }
+ .asset-tag {
+ @include media-breakpoint-down($responsive-layout-bp) {
+ @include sr-only;
+ }
+ }
+ }
+ }
+
+ .nav-trigger {
+ fill: theme-color('light');
+ width: $header-height;
+ height: $header-height;
+ transition: none;
+ display: inline-flex;
+ flex: 0 0 20px;
+ align-items: center;
+
+ svg {
+ margin: 0;
+ }
+
+ &:hover {
+ fill: theme-color('light');
+ background-color: theme-color-level(light, 10);
+ }
+
+ &.open {
+ background-color: gray('800');
+ }
+
+ @include media-breakpoint-up($responsive-layout-bp) {
+ display: none;
+ }
+ }
+
+ .dropdown-menu {
+ margin-top: 0;
+
+ @include media-breakpoint-only(md) {
+ margin-top: 4px;
+ }
+ }
+
+ .navbar-expand {
+ @include media-breakpoint-down(sm) {
+ flex-flow: wrap;
+ }
+ }
+}
+
+.navbar-brand {
+ padding: $spacer/2;
+ height: $header-height;
+ line-height: 1;
+ &:focus {
+ box-shadow: inset 0 0 0 3px $navbar-color, inset 0 0 0 5px color('white');
+ outline: 0;
+ }
+}
+</style>
diff --git a/src/components/_sila/AppHeader/index.js b/src/components/_sila/AppHeader/index.js
new file mode 100644
index 00000000..e180e80f
--- /dev/null
+++ b/src/components/_sila/AppHeader/index.js
@@ -0,0 +1,2 @@
+import AppHeader from './AppHeader';
+export default AppHeader;
diff --git a/src/components/_sila/AppNavigation/AppNavigation.vue b/src/components/_sila/AppNavigation/AppNavigation.vue
new file mode 100644
index 00000000..acfabe76
--- /dev/null
+++ b/src/components/_sila/AppNavigation/AppNavigation.vue
@@ -0,0 +1,255 @@
+<template>
+ <div>
+ <div class="nav-container" :class="{ open: isNavigationOpen }">
+ <nav ref="nav" :aria-label="$t('appNavigation.primaryNavigation')">
+ <b-nav vertical class="mb-4">
+ <template v-for="(navItem, index) in navigationItems">
+ <!-- Navigation items with no children -->
+ <b-nav-item
+ v-if="!navItem.children"
+ :key="index"
+ :to="navItem.route"
+ :data-test-id="`nav-item-${navItem.id}`"
+ >
+ <component :is="navItem.icon" />
+ {{ navItem.label }}
+ </b-nav-item>
+
+ <!-- Navigation items with children -->
+ <li v-else :key="index" class="nav-item">
+ <b-button
+ v-b-toggle="`${navItem.id}`"
+ variant="link"
+ :data-test-id="`nav-button-${navItem.id}`"
+ >
+ <component :is="navItem.icon" />
+ {{ navItem.label }}
+ <icon-expand class="icon-expand" />
+ </b-button>
+ <b-collapse :id="navItem.id" tag="ul" class="nav-item__nav">
+ <li class="nav-item">
+ <router-link
+ v-for="(subNavItem, i) of navItem.children"
+ :key="i"
+ :to="subNavItem.route"
+ :data-test-id="`nav-item-${subNavItem.id}`"
+ class="nav-link"
+ >
+ {{ subNavItem.label }}
+ </router-link>
+ </li>
+ </b-collapse>
+ </li>
+ </template>
+ </b-nav>
+ </nav>
+ </div>
+ <transition name="fade">
+ <div
+ v-if="isNavigationOpen"
+ id="nav-overlay"
+ class="nav-overlay"
+ @click="toggleIsOpen"
+ ></div>
+ </transition>
+ </div>
+</template>
+
+<script>
+//Do not change Mixin import.
+//Exact match alias set to support
+//dotenv customizations.
+import AppNavigationMixin from './AppNavigationMixin';
+
+export default {
+ name: 'AppNavigation',
+ mixins: [AppNavigationMixin],
+ data() {
+ return {
+ isNavigationOpen: false,
+ };
+ },
+ watch: {
+ $route: function () {
+ this.isNavigationOpen = false;
+ },
+ isNavigationOpen: function (isNavigationOpen) {
+ this.$root.$emit('change-is-navigation-open', isNavigationOpen);
+ },
+ },
+ mounted() {
+ this.$root.$on('toggle-navigation', () => this.toggleIsOpen());
+ },
+ methods: {
+ toggleIsOpen() {
+ this.isNavigationOpen = !this.isNavigationOpen;
+ },
+ },
+};
+</script>
+
+<style scoped lang="scss">
+svg {
+ fill: currentColor;
+ height: 1.2rem;
+ width: 1.2rem;
+ margin-left: 0 !important; //!important overriding button specificity
+ vertical-align: text-bottom;
+ &:not(.icon-expand) {
+ margin-right: $spacer;
+ }
+}
+
+.nav {
+ padding-top: $spacer / 4;
+ @include media-breakpoint-up($responsive-layout-bp) {
+ padding-top: $spacer;
+ }
+}
+
+.nav-item__nav {
+ list-style: none;
+ padding-left: 0;
+ margin-left: 0;
+
+ .nav-item {
+ outline: none;
+ }
+
+ .nav-link {
+ padding-left: $spacer * 4;
+ outline: none;
+
+ &:not(.nav-link--current) {
+ font-weight: normal;
+ }
+ }
+}
+
+.btn-link {
+ display: inline-block;
+ width: 100%;
+ text-align: left;
+ text-decoration: none !important;
+ border-radius: 0;
+
+ &.collapsed {
+ .icon-expand {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+.icon-expand {
+ float: right;
+ margin-top: $spacer / 4;
+}
+
+.btn-link,
+.nav-link {
+ position: relative;
+ font-weight: $headings-font-weight;
+ padding-left: $spacer; // defining consistent padding for links and buttons
+ padding-right: $spacer;
+ color: theme-color('secondary');
+
+ &:hover {
+ background-color: theme-color-level(dark, -10.5);
+ color: theme-color('dark');
+ }
+
+ &:focus {
+ background-color: theme-color-level(light, 0);
+ box-shadow: inset 0 0 0 2px theme-color('primary');
+ color: theme-color('dark');
+ outline: 0;
+ }
+
+ &:active {
+ background-color: theme-color('secondary');
+ color: $white;
+ }
+}
+
+.nav-link--current {
+ font-weight: $headings-font-weight;
+ background-color: theme-color('secondary');
+ color: theme-color('light');
+ cursor: default;
+ box-shadow: none;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 4px;
+ background-color: theme-color('primary');
+ }
+
+ &:hover,
+ &:focus {
+ background-color: theme-color('secondary');
+ color: theme-color('light');
+ }
+}
+
+.nav-container {
+ position: fixed;
+ width: $navigation-width;
+ top: $header-height;
+ bottom: 0;
+ left: 0;
+ z-index: $zindex-fixed;
+ overflow-y: auto;
+ background-color: theme-color('light');
+ transform: translateX(-$navigation-width);
+ transition: transform $exit-easing--productive $duration--moderate-02;
+ border-right: 1px solid theme-color-level('light', 2.85);
+
+ @include media-breakpoint-down(md) {
+ z-index: $zindex-fixed + 2;
+ }
+
+ &.open,
+ &:focus-within {
+ transform: translateX(0);
+ transition-timing-function: $entrance-easing--productive;
+ }
+
+ @include media-breakpoint-up($responsive-layout-bp) {
+ transition-duration: $duration--fast-01;
+ transform: translateX(0);
+ }
+}
+
+.nav-overlay {
+ position: fixed;
+ top: $header-height;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: $zindex-fixed + 1;
+ background-color: $black;
+ opacity: 0.5;
+
+ &.fade-enter-active {
+ transition: opacity $duration--moderate-02 $entrance-easing--productive;
+ }
+
+ &.fade-leave-active {
+ transition: opacity $duration--fast-02 $exit-easing--productive;
+ }
+
+ &.fade-enter, // Remove this vue2 based only class when switching to vue3
+ &.fade-enter-from, // This is vue3 based only class modified from 'fade-enter'
+ &.fade-leave-to {
+ opacity: 0;
+ }
+
+ @include media-breakpoint-up($responsive-layout-bp) {
+ display: none;
+ }
+}
+</style>
diff --git a/src/components/_sila/AppNavigation/AppNavigationMixin.js b/src/components/_sila/AppNavigation/AppNavigationMixin.js
new file mode 100644
index 00000000..bbbbb1ee
--- /dev/null
+++ b/src/components/_sila/AppNavigation/AppNavigationMixin.js
@@ -0,0 +1,182 @@
+import IconDashboard from '@carbon/icons-vue/es/dashboard/16';
+import IconTextLinkAnalysis from '@carbon/icons-vue/es/text-link--analysis/16';
+import IconDataCheck from '@carbon/icons-vue/es/data--check/16';
+import IconSettingsAdjust from '@carbon/icons-vue/es/settings--adjust/16';
+import IconSettings from '@carbon/icons-vue/es/settings/16';
+import IconSecurity from '@carbon/icons-vue/es/security/16';
+import IconChevronUp from '@carbon/icons-vue/es/chevron--up/16';
+import IconDataBase from '@carbon/icons-vue/es/data--base--alt/16';
+
+const AppNavigationMixin = {
+ components: {
+ iconOverview: IconDashboard,
+ iconLogs: IconTextLinkAnalysis,
+ iconHealth: IconDataCheck,
+ iconControl: IconSettingsAdjust,
+ iconSettings: IconSettings,
+ iconSecurityAndAccess: IconSecurity,
+ iconExpand: IconChevronUp,
+ iconResourceManagement: IconDataBase,
+ },
+ data() {
+ return {
+ navigationItems: [
+ {
+ id: 'overview',
+ label: this.$t('appNavigation.overview'),
+ route: '/',
+ icon: 'iconOverview',
+ },
+ {
+ id: 'logs',
+ label: this.$t('appNavigation.logs'),
+ icon: 'iconLogs',
+ children: [
+ {
+ id: 'event-logs',
+ label: this.$t('appNavigation.eventLogs'),
+ route: '/logs/event-logs',
+ },
+ {
+ id: 'post-code-logs',
+ label: this.$t('appNavigation.postCodeLogs'),
+ route: '/logs/post-code-logs',
+ },
+ ],
+ },
+ {
+ id: 'hardware-status',
+ label: this.$t('appNavigation.hardwareStatus'),
+ icon: 'iconHealth',
+ children: [
+ {
+ id: 'inventory',
+ label: this.$t('appNavigation.inventory'),
+ route: '/hardware-status/inventory',
+ },
+ {
+ id: 'sensors',
+ label: this.$t('appNavigation.sensors'),
+ route: '/hardware-status/sensors',
+ },
+ ],
+ },
+ {
+ id: 'operations',
+ label: this.$t('appNavigation.operations'),
+ icon: 'iconControl',
+ children: [
+ {
+ id: 'factory-reset',
+ label: this.$t('appNavigation.factoryReset'),
+ route: '/operations/factory-reset',
+ },
+ {
+ id: 'kvm',
+ label: this.$t('appNavigation.kvm'),
+ route: '/operations/kvm',
+ },
+ {
+ id: 'key-clear',
+ label: this.$t('appNavigation.keyClear'),
+ route: '/operations/key-clear',
+ },
+ {
+ id: 'firmware',
+ label: this.$t('appNavigation.firmware'),
+ route: '/operations/firmware',
+ },
+ {
+ id: 'reboot-bmc',
+ label: this.$t('appNavigation.rebootBmc'),
+ route: '/operations/reboot-bmc',
+ },
+ {
+ id: 'serial-over-lan',
+ label: this.$t('appNavigation.serialOverLan'),
+ route: '/operations/serial-over-lan',
+ },
+ {
+ id: 'server-power-operations',
+ label: this.$t('appNavigation.serverPowerOperations'),
+ route: '/operations/server-power-operations',
+ },
+ {
+ id: 'virtual-media',
+ label: this.$t('appNavigation.virtualMedia'),
+ route: '/operations/virtual-media',
+ },
+ ],
+ },
+ {
+ id: 'settings',
+ label: this.$t('appNavigation.settings'),
+ icon: 'iconSettings',
+ children: [
+ {
+ id: 'date-time',
+ label: this.$t('appNavigation.dateTime'),
+ route: '/settings/date-time',
+ },
+ {
+ id: 'network',
+ label: this.$t('appNavigation.network'),
+ route: '/settings/network',
+ },
+ {
+ id: 'power-restore-policy',
+ label: this.$t('appNavigation.powerRestorePolicy'),
+ route: '/settings/power-restore-policy',
+ },
+ ],
+ },
+ {
+ id: 'security-and-access',
+ label: this.$t('appNavigation.securityAndAccess'),
+ icon: 'iconSecurityAndAccess',
+ children: [
+ {
+ id: 'sessions',
+ label: this.$t('appNavigation.sessions'),
+ route: '/security-and-access/sessions',
+ },
+ {
+ id: 'ldap',
+ label: this.$t('appNavigation.ldap'),
+ route: '/security-and-access/ldap',
+ },
+ {
+ id: 'user-management',
+ label: this.$t('appNavigation.userManagement'),
+ route: '/security-and-access/user-management',
+ },
+ {
+ id: 'policies',
+ label: this.$t('appNavigation.policies'),
+ route: '/security-and-access/policies',
+ },
+ {
+ id: 'certificates',
+ label: this.$t('appNavigation.certificates'),
+ route: '/security-and-access/certificates',
+ },
+ ],
+ },
+ {
+ id: 'resource-management',
+ label: this.$t('appNavigation.resourceManagement'),
+ icon: 'iconResourceManagement',
+ children: [
+ {
+ id: 'power',
+ label: this.$t('appNavigation.power'),
+ route: '/resource-management/power',
+ },
+ ],
+ },
+ ],
+ };
+ },
+};
+
+export default AppNavigationMixin;
diff --git a/src/components/_sila/AppNavigation/index.js b/src/components/_sila/AppNavigation/index.js
new file mode 100644
index 00000000..88fe8eb6
--- /dev/null
+++ b/src/components/_sila/AppNavigation/index.js
@@ -0,0 +1,2 @@
+import AppNavigation from './AppNavigation';
+export default AppNavigation;
diff --git a/src/components/_sila/Global/Alert.vue b/src/components/_sila/Global/Alert.vue
new file mode 100644
index 00000000..e8de9e27
--- /dev/null
+++ b/src/components/_sila/Global/Alert.vue
@@ -0,0 +1,47 @@
+<template>
+ <b-alert :show="show" :variant="variant" :class="{ small }">
+ <div
+ v-if="
+ variant == 'info' ||
+ variant == 'success' ||
+ variant == 'warning' ||
+ variant == 'danger'
+ "
+ class="alert-icon"
+ >
+ <status-icon :status="variant" />
+ </div>
+ <div class="alert-content">
+ <div class="alert-msg">
+ <slot />
+ </div>
+ </div>
+ <div class="alert-action">
+ <slot name="action"></slot>
+ </div>
+ </b-alert>
+</template>
+
+<script>
+import StatusIcon from '@/components/Global/StatusIcon';
+import { BAlert } from 'bootstrap-vue';
+
+export default {
+ name: 'Alert',
+ components: {
+ BAlert: BAlert,
+ StatusIcon: StatusIcon,
+ },
+ props: {
+ show: {
+ type: Boolean,
+ default: true,
+ },
+ variant: {
+ type: String,
+ default: '',
+ },
+ small: Boolean,
+ },
+};
+</script>
diff --git a/src/components/_sila/Global/ButtonBackToTop.vue b/src/components/_sila/Global/ButtonBackToTop.vue
new file mode 100644
index 00000000..9160c7b7
--- /dev/null
+++ b/src/components/_sila/Global/ButtonBackToTop.vue
@@ -0,0 +1,68 @@
+<template>
+ <b-button
+ id="scrollToTopBtn"
+ class="btn-top btn-icon-only"
+ :class="{ 'show-btn': showButton }"
+ variant="secondary"
+ :title="$t('global.ariaLabel.scrollToTop')"
+ @click="scrollToTop"
+ >
+ <icon-up-to-top />
+ <span class="sr-only">{{ $t('global.ariaLabel.scrollToTop') }}</span>
+ </b-button>
+</template>
+
+<script>
+import UpToTop24 from '@carbon/icons-vue/es/up-to-top/24';
+
+import { debounce } from 'lodash';
+
+export default {
+ name: 'BackToTop',
+ components: { IconUpToTop: UpToTop24 },
+ data() {
+ return {
+ showButton: false,
+ };
+ },
+ created() {
+ window.addEventListener('scroll', debounce(this.handleScroll, 200));
+ },
+ methods: {
+ handleScroll() {
+ document.documentElement.scrollTop > 500
+ ? (this.showButton = true)
+ : (this.showButton = false);
+ },
+ scrollToTop() {
+ document.documentElement.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.btn-top {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+
+ box-shadow: $box-shadow;
+ visibility: hidden;
+ opacity: 0;
+ transition: $transition-base;
+ z-index: $zindex-fixed;
+
+ @media (min-width: 1600px) {
+ left: 1485px;
+ right: auto;
+ }
+}
+.show-btn {
+ visibility: visible;
+ opacity: 1;
+}
+</style>
diff --git a/src/components/_sila/Global/FormFile.vue b/src/components/_sila/Global/FormFile.vue
new file mode 100644
index 00000000..cf713acf
--- /dev/null
+++ b/src/components/_sila/Global/FormFile.vue
@@ -0,0 +1,119 @@
+<template>
+ <div class="custom-form-file-container">
+ <label>
+ <b-form-file
+ :id="id"
+ v-model="file"
+ :accept="accept"
+ :disabled="disabled"
+ :state="state"
+ plain
+ @input="$emit('input', file)"
+ >
+ </b-form-file>
+ <span
+ class="add-file-btn btn"
+ :class="{
+ disabled,
+ 'btn-secondary': isSecondary,
+ 'btn-primary': !isSecondary,
+ }"
+ >
+ {{ $t('global.fileUpload.browseText') }}
+ </span>
+ <slot name="invalid"></slot>
+ </label>
+ <div v-if="file" class="clear-selected-file px-3 py-2 mt-2">
+ {{ file ? file.name : '' }}
+ <b-button
+ variant="light"
+ class="px-2 ml-auto"
+ :disabled="disabled"
+ @click="file = null"
+ ><icon-close :title="$t('global.fileUpload.clearSelectedFile')" /><span
+ class="sr-only"
+ >{{ $t('global.fileUpload.clearSelectedFile') }}</span
+ >
+ </b-button>
+ </div>
+ </div>
+</template>
+
+<script>
+import { BFormFile } from 'bootstrap-vue';
+import IconClose from '@carbon/icons-vue/es/close/20';
+
+export default {
+ name: 'FormFile',
+ components: { BFormFile, IconClose },
+ props: {
+ id: {
+ type: String,
+ default: '',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ accept: {
+ type: String,
+ default: '',
+ },
+ state: {
+ type: Boolean,
+ default: true,
+ },
+ variant: {
+ type: String,
+ default: 'secondary',
+ },
+ },
+ data() {
+ return {
+ file: null,
+ };
+ },
+ computed: {
+ isSecondary() {
+ return this.variant === 'secondary';
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.form-control-file {
+ opacity: 0;
+ height: 0;
+ &:focus + span {
+ box-shadow: inset 0 0 0 3px theme-color('primary'), inset 0 0 0 5px $white;
+ }
+}
+
+// Get mouse pointer on complete element
+.add-file-btn {
+ position: relative;
+ &.disabled {
+ border-color: gray('400');
+ background-color: gray('400');
+ color: gray('600');
+ box-shadow: none !important;
+ }
+}
+
+.clear-selected-file {
+ display: flex;
+ align-items: center;
+ background-color: theme-color('light');
+ .btn {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+
+ &:focus {
+ box-shadow: inset 0 0 0 2px theme-color('primary');
+ }
+ }
+}
+</style>
diff --git a/src/components/_sila/Global/InfoTooltip.vue b/src/components/_sila/Global/InfoTooltip.vue
new file mode 100644
index 00000000..c91109d1
--- /dev/null
+++ b/src/components/_sila/Global/InfoTooltip.vue
@@ -0,0 +1,35 @@
+<template>
+ <b-button
+ v-b-tooltip
+ variant="link"
+ class="btn-tooltip btn-icon-only"
+ :title="title"
+ >
+ <icon-tooltip />
+ <span class="sr-only">{{ $t('global.ariaLabel.tooltip') }}</span>
+ </b-button>
+</template>
+
+<script>
+import IconTooltip from '@carbon/icons-vue/es/information/16';
+
+export default {
+ components: { IconTooltip },
+ props: {
+ title: {
+ type: String,
+ default: '',
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.btn-tooltip {
+ padding: 0;
+ line-height: 1em;
+ svg {
+ vertical-align: baseline;
+ }
+}
+</style>
diff --git a/src/components/_sila/Global/InputPasswordToggle.vue b/src/components/_sila/Global/InputPasswordToggle.vue
new file mode 100644
index 00000000..d2c0d4a6
--- /dev/null
+++ b/src/components/_sila/Global/InputPasswordToggle.vue
@@ -0,0 +1,54 @@
+<template>
+ <div class="input-password-toggle-container">
+ <slot></slot>
+ <b-button
+ :title="togglePasswordLabel"
+ variant="link"
+ class="input-action-btn btn-icon-only"
+ :class="{ isVisible: isVisible }"
+ @click="toggleVisibility"
+ >
+ <icon-view-off v-if="isVisible" />
+ <icon-view v-else />
+ <span class="sr-only">{{ togglePasswordLabel }}</span>
+ </b-button>
+ </div>
+</template>
+
+<script>
+import IconView from '@carbon/icons-vue/es/view/20';
+import IconViewOff from '@carbon/icons-vue/es/view--off/20';
+
+export default {
+ name: 'InputPasswordToggle',
+ components: { IconView, IconViewOff },
+ data() {
+ return {
+ isVisible: false,
+ togglePasswordLabel: this.$t('global.ariaLabel.showPassword'),
+ };
+ },
+ methods: {
+ toggleVisibility() {
+ const firstChild = this.$children[0];
+ const inputEl = firstChild ? firstChild.$el : null;
+
+ this.isVisible = !this.isVisible;
+
+ if (inputEl && inputEl.nodeName === 'INPUT') {
+ inputEl.type = this.isVisible ? 'text' : 'password';
+ }
+
+ this.isVisible
+ ? (this.togglePasswordLabel = this.$t('global.ariaLabel.hidePassword'))
+ : (this.togglePasswordLabel = this.$t('global.ariaLabel.showPassword'));
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.input-password-toggle-container {
+ position: relative;
+}
+</style>
diff --git a/src/components/_sila/Global/LoadingBar.vue b/src/components/_sila/Global/LoadingBar.vue
new file mode 100644
index 00000000..0e9551b5
--- /dev/null
+++ b/src/components/_sila/Global/LoadingBar.vue
@@ -0,0 +1,93 @@
+<template>
+ <transition name="fade">
+ <b-progress v-if="!isLoadingComplete">
+ <b-progress-bar
+ striped
+ animated
+ :value="loadingIndicatorValue"
+ :aria-label="$t('global.ariaLabel.progressBar')"
+ />
+ </b-progress>
+ </transition>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ loadingIndicatorValue: 0,
+ isLoadingComplete: false,
+ loadingIntervalId: null,
+ timeoutId: null,
+ };
+ },
+ created() {
+ this.$root.$on('loader-start', () => {
+ this.startLoadingInterval();
+ });
+ this.$root.$on('loader-end', () => {
+ this.endLoadingInterval();
+ });
+ this.$root.$on('loader-hide', () => {
+ this.hideLoadingBar();
+ });
+ },
+ methods: {
+ startLoadingInterval() {
+ this.clearLoadingInterval();
+ this.clearTimeout();
+ this.loadingIndicatorValue = 0;
+ this.isLoadingComplete = false;
+ this.loadingIntervalId = setInterval(() => {
+ this.loadingIndicatorValue += 1;
+ if (this.loadingIndicatorValue > 100) this.clearLoadingInterval();
+ }, 100);
+ },
+ endLoadingInterval() {
+ this.clearLoadingInterval();
+ this.clearTimeout();
+ this.loadingIndicatorValue = 100;
+ this.timeoutId = setTimeout(() => {
+ // Let animation complete before hiding
+ // the loading bar
+ this.isLoadingComplete = true;
+ }, 1000);
+ },
+ hideLoadingBar() {
+ this.clearLoadingInterval();
+ this.clearTimeout();
+ this.loadingIndicatorValue = 0;
+ this.isLoadingComplete = true;
+ },
+ clearLoadingInterval() {
+ if (this.loadingIntervalId) clearInterval(this.loadingIntervalId);
+ this.loadingIntervalId = null;
+ },
+ clearTimeout() {
+ if (this.timeoutId) clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.progress {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -0.4rem;
+ opacity: 1;
+ transition: opacity $duration--moderate-01 $standard-easing--productive;
+ height: 0.4rem;
+
+ &.fade-enter, // Remove this vue2 based only class when switching to vue3
+ &.fade-enter-from, // This is vue3 based only class modified from 'fade-enter'
+ &.fade-leave-to {
+ opacity: 0;
+ }
+}
+.progress-bar {
+ background-color: $loading-color;
+}
+</style>
diff --git a/src/components/_sila/Global/PageContainer.vue b/src/components/_sila/Global/PageContainer.vue
new file mode 100644
index 00000000..ab4adb63
--- /dev/null
+++ b/src/components/_sila/Global/PageContainer.vue
@@ -0,0 +1,37 @@
+<template>
+ <main id="main-content" class="page-container">
+ <slot />
+ </main>
+</template>
+
+<script>
+import JumpLinkMixin from '@/components/Mixins/JumpLinkMixin';
+export default {
+ name: 'PageContainer',
+ mixins: [JumpLinkMixin],
+ created() {
+ this.$root.$on('skip-navigation', () => {
+ this.setFocus(this.$el);
+ });
+ },
+};
+</script>
+<style lang="scss" scoped>
+main {
+ width: 100%;
+ height: 100%;
+ padding-top: $spacer * 1.5;
+ padding-bottom: $spacer * 3;
+ padding-left: $spacer;
+ padding-right: $spacer;
+
+ &:focus-visible {
+ box-shadow: inset 0 0 0 2px theme-color('primary');
+ outline: none;
+ }
+
+ @include media-breakpoint-up($responsive-layout-bp) {
+ padding-left: $spacer * 2;
+ }
+}
+</style>
diff --git a/src/components/_sila/Global/PageSection.vue b/src/components/_sila/Global/PageSection.vue
new file mode 100644
index 00000000..dd39ddd5
--- /dev/null
+++ b/src/components/_sila/Global/PageSection.vue
@@ -0,0 +1,29 @@
+<template>
+ <div class="page-section">
+ <h2 v-if="sectionTitle">{{ sectionTitle }}</h2>
+ <slot />
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'PageSection',
+ props: {
+ sectionTitle: {
+ type: String,
+ default: '',
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.page-section {
+ margin-bottom: $spacer * 4;
+}
+
+h2 {
+ @include font-size($h3-font-size);
+ margin-bottom: $spacer;
+}
+</style>
diff --git a/src/components/_sila/Global/PageTitle.vue b/src/components/_sila/Global/PageTitle.vue
new file mode 100644
index 00000000..45c75edb
--- /dev/null
+++ b/src/components/_sila/Global/PageTitle.vue
@@ -0,0 +1,32 @@
+<template>
+ <div class="page-title">
+ <h1>{{ title }}</h1>
+ <p v-if="description">{{ description }}</p>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'PageTitle',
+ props: {
+ description: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ title: this.$route.meta.title,
+ };
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.page-title {
+ margin-bottom: $spacer * 2;
+}
+p {
+ max-width: 72ch;
+}
+</style>
diff --git a/src/components/_sila/Global/Search.vue b/src/components/_sila/Global/Search.vue
new file mode 100644
index 00000000..ac8f9bfb
--- /dev/null
+++ b/src/components/_sila/Global/Search.vue
@@ -0,0 +1,83 @@
+<template>
+ <div class="search-global">
+ <b-form-group
+ :label="$t('global.form.search')"
+ :label-for="`searchInput-${_uid}`"
+ label-class="invisible"
+ class="mb-2"
+ >
+ <b-input-group size="md" class="align-items-center">
+ <b-input-group-prepend>
+ <icon-search class="search-icon" />
+ </b-input-group-prepend>
+ <b-form-input
+ :id="`searchInput-${_uid}`"
+ ref="searchInput"
+ v-model="filter"
+ class="search-input"
+ type="text"
+ :aria-label="$t('global.form.search')"
+ :placeholder="placeholder"
+ @input="onChangeInput"
+ >
+ </b-form-input>
+ <b-button
+ v-if="filter"
+ variant="link"
+ class="btn-icon-only input-action-btn"
+ :title="$t('global.ariaLabel.clearSearch')"
+ @click="onClearSearch"
+ >
+ <icon-close />
+ <span class="sr-only">{{ $t('global.ariaLabel.clearSearch') }}</span>
+ </b-button>
+ </b-input-group>
+ </b-form-group>
+ </div>
+</template>
+
+<script>
+import IconSearch from '@carbon/icons-vue/es/search/16';
+import IconClose from '@carbon/icons-vue/es/close/20';
+
+export default {
+ name: 'Search',
+ components: { IconSearch, IconClose },
+ props: {
+ placeholder: {
+ type: String,
+ default: function () {
+ return this.$t('global.form.search');
+ },
+ },
+ },
+ data() {
+ return {
+ filter: null,
+ };
+ },
+ methods: {
+ onChangeInput() {
+ this.$emit('change-search', this.filter);
+ },
+ onClearSearch() {
+ this.filter = '';
+ this.$emit('clear-search');
+ this.$refs.searchInput.focus();
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.search-input {
+ padding-left: ($spacer * 2);
+}
+.search-icon {
+ position: absolute;
+ left: 10px;
+ top: 12px;
+ z-index: 4;
+ stroke: gray('400');
+}
+</style>
diff --git a/src/components/_sila/Global/StatusIcon.vue b/src/components/_sila/Global/StatusIcon.vue
new file mode 100644
index 00000000..4552633e
--- /dev/null
+++ b/src/components/_sila/Global/StatusIcon.vue
@@ -0,0 +1,61 @@
+<template>
+ <span :class="['status-icon', status]">
+ <icon-info v-if="status === 'info'" />
+ <icon-success v-else-if="status === 'success'" />
+ <icon-warning v-else-if="status === 'warning'" />
+ <icon-danger v-else-if="status === 'danger'" />
+ <icon-secondary v-else />
+ </span>
+</template>
+
+<script>
+import IconInfo from '@carbon/icons-vue/es/information--filled/20';
+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';
+import IconMisuse from '@carbon/icons-vue/es/misuse/20';
+
+export default {
+ name: 'StatusIcon',
+ components: {
+ IconInfo: IconInfo,
+ iconSuccess: IconCheckmark,
+ iconDanger: IconMisuse,
+ iconSecondary: IconError,
+ iconWarning: IconWarning,
+ },
+ props: {
+ status: {
+ type: String,
+ default: '',
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.status-icon {
+ vertical-align: text-bottom;
+
+ &.info {
+ color: theme-color('info');
+ }
+ &.success {
+ color: theme-color('success');
+ }
+ &.danger {
+ color: theme-color('danger');
+ }
+ &.secondary {
+ color: gray('600');
+ transform: rotate(-45deg);
+ }
+ &.warning {
+ color: theme-color('warning');
+ }
+
+ svg {
+ fill: currentColor;
+ }
+}
+</style>
diff --git a/src/components/_sila/Global/TableCellCount.vue b/src/components/_sila/Global/TableCellCount.vue
new file mode 100644
index 00000000..acb4d443
--- /dev/null
+++ b/src/components/_sila/Global/TableCellCount.vue
@@ -0,0 +1,35 @@
+<template>
+ <div class="mt-2">
+ <p v-if="!filterActive">
+ {{ $t('global.table.items', { count: totalNumberOfCells }) }}
+ </p>
+ <p v-else>
+ {{
+ $t('global.table.selectedItems', {
+ count: totalNumberOfCells,
+ filterCount: filteredItemsCount,
+ })
+ }}
+ </p>
+ </div>
+</template>
+
+<script>
+export default {
+ props: {
+ filteredItemsCount: {
+ type: Number,
+ required: true,
+ },
+ totalNumberOfCells: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ filterActive() {
+ return this.filteredItemsCount !== this.totalNumberOfCells;
+ },
+ },
+};
+</script>
diff --git a/src/components/_sila/Global/TableDateFilter.vue b/src/components/_sila/Global/TableDateFilter.vue
new file mode 100644
index 00000000..aa10cb5c
--- /dev/null
+++ b/src/components/_sila/Global/TableDateFilter.vue
@@ -0,0 +1,165 @@
+<template>
+ <b-row class="mb-2">
+ <b-col class="d-sm-flex">
+ <b-form-group
+ :label="$t('global.table.fromDate')"
+ label-for="input-from-date"
+ class="mr-3 my-0 w-100"
+ >
+ <b-input-group>
+ <b-form-input
+ id="input-from-date"
+ v-model="fromDate"
+ placeholder="YYYY-MM-DD"
+ :state="getValidationState($v.fromDate)"
+ class="form-control-with-button mb-3 mb-md-0"
+ @blur="$v.fromDate.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.fromDate.pattern">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ <template v-if="!$v.fromDate.maxDate">
+ {{ $t('global.form.dateMustBeBefore', { date: toDate }) }}
+ </template>
+ </b-form-invalid-feedback>
+ <b-form-datepicker
+ v-model="fromDate"
+ class="btn-datepicker btn-icon-only"
+ button-only
+ right
+ :max="toDate"
+ :hide-header="true"
+ :locale="locale"
+ :label-help="
+ $t('global.calendar.useCursorKeysToNavigateCalendarDates')
+ "
+ :title="$t('global.calendar.selectDate')"
+ button-variant="link"
+ aria-controls="input-from-date"
+ >
+ <template #button-content>
+ <icon-calendar />
+ <span class="sr-only">
+ {{ $t('global.calendar.selectDate') }}
+ </span>
+ </template>
+ </b-form-datepicker>
+ </b-input-group>
+ </b-form-group>
+ <b-form-group
+ :label="$t('global.table.toDate')"
+ label-for="input-to-date"
+ class="my-0 w-100"
+ >
+ <b-input-group>
+ <b-form-input
+ id="input-to-date"
+ v-model="toDate"
+ placeholder="YYYY-MM-DD"
+ :state="getValidationState($v.toDate)"
+ class="form-control-with-button"
+ @blur="$v.toDate.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.toDate.pattern">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ <template v-if="!$v.toDate.minDate">
+ {{ $t('global.form.dateMustBeAfter', { date: fromDate }) }}
+ </template>
+ </b-form-invalid-feedback>
+ <b-form-datepicker
+ v-model="toDate"
+ class="btn-datepicker btn-icon-only"
+ button-only
+ right
+ :min="fromDate"
+ :hide-header="true"
+ :locale="locale"
+ :label-help="
+ $t('global.calendar.useCursorKeysToNavigateCalendarDates')
+ "
+ :title="$t('global.calendar.selectDate')"
+ button-variant="link"
+ aria-controls="input-to-date"
+ >
+ <template #button-content>
+ <icon-calendar />
+ <span class="sr-only">
+ {{ $t('global.calendar.selectDate') }}
+ </span>
+ </template>
+ </b-form-datepicker>
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ </b-row>
+</template>
+
+<script>
+import IconCalendar from '@carbon/icons-vue/es/calendar/20';
+import { helpers } from 'vuelidate/lib/validators';
+
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
+
+export default {
+ components: { IconCalendar },
+ mixins: [VuelidateMixin],
+ data() {
+ return {
+ fromDate: '',
+ toDate: '',
+ offsetToDate: '',
+ locale: this.$store.getters['global/languagePreference'],
+ };
+ },
+ validations() {
+ return {
+ fromDate: {
+ pattern: helpers.regex('pattern', isoDateRegex),
+ maxDate: (value) => {
+ if (!this.toDate) return true;
+ const date = new Date(value);
+ const maxDate = new Date(this.toDate);
+ if (date.getTime() > maxDate.getTime()) return false;
+ return true;
+ },
+ },
+ toDate: {
+ pattern: helpers.regex('pattern', isoDateRegex),
+ minDate: (value) => {
+ if (!this.fromDate) return true;
+ const date = new Date(value);
+ const minDate = new Date(this.fromDate);
+ if (date.getTime() < minDate.getTime()) return false;
+ return true;
+ },
+ },
+ };
+ },
+ watch: {
+ fromDate() {
+ this.emitChange();
+ },
+ toDate(newVal) {
+ // Offset the end date to end of day to make sure all
+ // entries from selected end date are included in filter
+ this.offsetToDate = new Date(newVal).setUTCHours(23, 59, 59, 999);
+ this.emitChange();
+ },
+ },
+ methods: {
+ emitChange() {
+ if (this.$v.$invalid) return;
+ this.$v.$reset(); //reset to re-validate on blur
+ this.$emit('change', {
+ fromDate: this.fromDate ? new Date(this.fromDate) : null,
+ toDate: this.toDate ? new Date(this.offsetToDate) : null,
+ });
+ },
+ },
+};
+</script>
diff --git a/src/components/_sila/Global/TableFilter.vue b/src/components/_sila/Global/TableFilter.vue
new file mode 100644
index 00000000..7c66bea6
--- /dev/null
+++ b/src/components/_sila/Global/TableFilter.vue
@@ -0,0 +1,114 @@
+<template>
+ <div class="table-filter d-inline-block">
+ <p class="d-inline-block mb-0">
+ <b-badge v-for="(tag, index) in tags" :key="index" pill>
+ {{ tag }}
+ <b-button-close
+ :disabled="dropdownVisible"
+ :aria-hidden="true"
+ @click="removeTag(tag)"
+ />
+ </b-badge>
+ </p>
+ <b-dropdown
+ variant="link"
+ no-caret
+ right
+ data-test-id="tableFilter-dropdown-options"
+ @hide="dropdownVisible = false"
+ @show="dropdownVisible = true"
+ >
+ <template #button-content>
+ <icon-filter />
+ {{ $t('global.action.filter') }}
+ </template>
+ <b-dropdown-form>
+ <b-form-group
+ v-for="(filter, index) of filters"
+ :key="index"
+ :label="filter.label"
+ >
+ <b-form-checkbox-group v-model="tags">
+ <b-form-checkbox
+ v-for="value in filter.values"
+ :key="value"
+ :value="value"
+ :data-test-id="`tableFilter-checkbox-${value}`"
+ >
+ <b-dropdown-item>
+ {{ value }}
+ </b-dropdown-item>
+ </b-form-checkbox>
+ </b-form-checkbox-group>
+ </b-form-group>
+ </b-dropdown-form>
+ <b-dropdown-item-button
+ variant="primary"
+ data-test-id="tableFilter-button-clearAll"
+ @click="clearAllTags"
+ >
+ {{ $t('global.action.clearAll') }}
+ </b-dropdown-item-button>
+ </b-dropdown>
+ </div>
+</template>
+
+<script>
+import IconFilter from '@carbon/icons-vue/es/settings--adjust/20';
+
+export default {
+ name: 'TableFilter',
+ components: { IconFilter },
+ props: {
+ filters: {
+ type: Array,
+ default: () => [],
+ validator: (prop) => {
+ return prop.every(
+ (filter) => 'label' in filter && 'values' in filter && 'key' in filter
+ );
+ },
+ },
+ },
+ data() {
+ return {
+ dropdownVisible: false,
+ tags: [],
+ };
+ },
+ watch: {
+ tags: {
+ handler() {
+ this.emitChange();
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ removeTag(removedTag) {
+ this.tags = this.tags.filter((tag) => tag !== removedTag);
+ },
+ clearAllTags() {
+ this.tags = [];
+ },
+ emitChange() {
+ const activeFilters = this.filters.map(({ key, values }) => {
+ const activeValues = values.filter(
+ (value) => this.tags.indexOf(value) !== -1
+ );
+ return {
+ key,
+ values: activeValues,
+ };
+ });
+ this.$emit('filter-change', { activeFilters });
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.badge {
+ margin-right: $spacer / 2;
+}
+</style>
diff --git a/src/components/_sila/Global/TableRowAction.vue b/src/components/_sila/Global/TableRowAction.vue
new file mode 100644
index 00000000..549f1b52
--- /dev/null
+++ b/src/components/_sila/Global/TableRowAction.vue
@@ -0,0 +1,112 @@
+<template>
+ <span>
+ <b-link
+ v-if="value === 'export'"
+ class="align-bottom btn-icon-only py-0 btn-link"
+ :download="download"
+ :href="href"
+ :title="title"
+ >
+ <slot name="icon">
+ {{ $t('global.action.export') }}
+ </slot>
+ <span v-if="btnIconOnly" class="sr-only">{{ title }}</span>
+ </b-link>
+ <b-link
+ v-else-if="
+ value === 'download' && downloadInNewTab && downloadLocation !== ''
+ "
+ class="align-bottom btn-icon-only py-0 btn-link"
+ target="_blank"
+ :href="downloadLocation"
+ :title="title"
+ >
+ <slot name="icon" />
+ <span class="sr-only">
+ {{ $t('global.action.download') }}
+ </span>
+ </b-link>
+ <b-link
+ v-else-if="value === 'download' && downloadLocation !== ''"
+ class="align-bottom btn-icon-only py-0 btn-link"
+ :download="exportName"
+ :href="downloadLocation"
+ :title="title"
+ >
+ <slot name="icon" />
+ <span class="sr-only">
+ {{ $t('global.action.download') }}
+ </span>
+ </b-link>
+ <b-button
+ v-else-if="showButton"
+ variant="link"
+ :class="{ 'btn-icon-only': btnIconOnly }"
+ :disabled="!enabled"
+ :title="btnIconOnly ? title : !title"
+ @click="$emit('click-table-action', value)"
+ >
+ <slot name="icon">
+ {{ title }}
+ </slot>
+ <span v-if="btnIconOnly" class="sr-only">{{ title }}</span>
+ </b-button>
+ </span>
+</template>
+
+<script>
+import { omit } from 'lodash';
+
+export default {
+ name: 'TableRowAction',
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ enabled: {
+ type: Boolean,
+ default: true,
+ },
+ title: {
+ type: String,
+ default: null,
+ },
+ rowData: {
+ type: Object,
+ default: () => {},
+ },
+ exportName: {
+ type: String,
+ default: 'export',
+ },
+ downloadLocation: {
+ type: String,
+ default: '',
+ },
+ btnIconOnly: {
+ type: Boolean,
+ default: true,
+ },
+ downloadInNewTab: {
+ type: Boolean,
+ default: false,
+ },
+ showButton: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ computed: {
+ dataForExport() {
+ return JSON.stringify(omit(this.rowData, 'actions'));
+ },
+ download() {
+ return `${this.exportName}.json`;
+ },
+ href() {
+ return `data:text/json;charset=utf-8,${this.dataForExport}`;
+ },
+ },
+};
+</script>
diff --git a/src/components/_sila/Global/TableToolbar.vue b/src/components/_sila/Global/TableToolbar.vue
new file mode 100644
index 00000000..5235feae
--- /dev/null
+++ b/src/components/_sila/Global/TableToolbar.vue
@@ -0,0 +1,130 @@
+<template>
+ <transition name="slide">
+ <div v-if="isToolbarActive" class="toolbar-container">
+ <div class="toolbar-content">
+ <p class="toolbar-selected">
+ {{ selectedItemsCount }} {{ $t('global.action.selected') }}
+ </p>
+ <div class="toolbar-actions d-flex">
+ <slot name="toolbar-buttons"></slot>
+ <b-button
+ v-for="(action, index) in actions"
+ :key="index"
+ :data-test-id="`table-button-${action.value}Selected`"
+ variant="primary"
+ class="d-block"
+ @click="$emit('batch-action', action.value)"
+ >
+ {{ action.label }}
+ </b-button>
+ <b-button
+ variant="secondary"
+ class="d-block"
+ @click="$emit('clear-selected')"
+ >
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ </div>
+ </div>
+ </div>
+ </transition>
+</template>
+
+<script>
+export default {
+ name: 'TableToolbar',
+ props: {
+ selectedItemsCount: {
+ type: Number,
+ required: true,
+ },
+ actions: {
+ type: Array,
+ default: () => [],
+ validator: (prop) => {
+ return prop.every((action) => {
+ return (
+ Object.prototype.hasOwnProperty.call(action, 'value') &&
+ Object.prototype.hasOwnProperty.call(action, 'label')
+ );
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ isToolbarActive: false,
+ };
+ },
+ watch: {
+ selectedItemsCount: function (selectedItemsCount) {
+ if (selectedItemsCount > 0) {
+ this.isToolbarActive = true;
+ } else {
+ this.isToolbarActive = false;
+ }
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+$toolbar-height: 46px;
+
+.toolbar-container {
+ width: 100%;
+ position: relative;
+ z-index: $zindex-dropdown + 1;
+}
+
+.toolbar-content {
+ height: $toolbar-height;
+ background-color: theme-color('primary');
+ color: $white;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: -$toolbar-height;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.toolbar-selected {
+ line-height: $toolbar-height;
+ margin: 0;
+ padding: 0 $spacer;
+}
+
+// Using v-deep to style export slot child-element
+// depricated and vue-js 3
+.toolbar-actions ::v-deep .btn {
+ position: relative;
+ &:after {
+ content: '';
+ position: absolute;
+ left: 0;
+ height: 1.5rem;
+ width: 1px;
+ background: rgba($white, 0.6);
+ }
+ &:last-child,
+ &:first-child {
+ &:after {
+ width: 0;
+ }
+ }
+}
+
+.slide-enter-active {
+ transition: transform $duration--moderate-02 $entrance-easing--productive;
+}
+.slide-leave-active {
+ transition: transform $duration--moderate-02 $exit-easing--productive;
+}
+.slide-enter, // Remove this vue2 based only class when switching to vue3
+.slide-enter-from, // This is vue3 based only class modified from 'slide-enter'
+.slide-leave-to {
+ transform: translateY($toolbar-height);
+}
+</style>
diff --git a/src/components/_sila/Global/TableToolbarExport.vue b/src/components/_sila/Global/TableToolbarExport.vue
new file mode 100644
index 00000000..69646ea6
--- /dev/null
+++ b/src/components/_sila/Global/TableToolbarExport.vue
@@ -0,0 +1,36 @@
+<template>
+ <b-button
+ class="d-flex align-items-center"
+ variant="primary"
+ :download="download"
+ :href="href"
+ >
+ {{ $t('global.action.export') }}
+ </b-button>
+</template>
+
+<script>
+export default {
+ props: {
+ data: {
+ type: Array,
+ default: () => [],
+ },
+ fileName: {
+ type: String,
+ default: 'data',
+ },
+ },
+ computed: {
+ dataForExport() {
+ return JSON.stringify(this.data);
+ },
+ download() {
+ return `${this.fileName}.json`;
+ },
+ href() {
+ return `data:text/json;charset=utf-8,${this.dataForExport}`;
+ },
+ },
+};
+</script>
diff --git a/src/components/_sila/Mixins/BVPaginationMixin.js b/src/components/_sila/Mixins/BVPaginationMixin.js
new file mode 100644
index 00000000..4ccf6f2c
--- /dev/null
+++ b/src/components/_sila/Mixins/BVPaginationMixin.js
@@ -0,0 +1,34 @@
+import i18n from '@/i18n';
+export const currentPage = 1;
+export const perPage = 20;
+export const itemsPerPageOptions = [
+ {
+ value: 10,
+ text: '10',
+ },
+ {
+ value: 20,
+ text: '20',
+ },
+ {
+ value: 30,
+ text: '30',
+ },
+ {
+ value: 40,
+ text: '40',
+ },
+ {
+ value: 0,
+ text: i18n.t('global.table.viewAll'),
+ },
+];
+const BVPaginationMixin = {
+ methods: {
+ getTotalRowCount(count) {
+ return this.perPage === 0 ? 0 : count;
+ },
+ },
+};
+
+export default BVPaginationMixin;
diff --git a/src/components/_sila/Mixins/BVTableSelectableMixin.js b/src/components/_sila/Mixins/BVTableSelectableMixin.js
new file mode 100644
index 00000000..b4f0b953
--- /dev/null
+++ b/src/components/_sila/Mixins/BVTableSelectableMixin.js
@@ -0,0 +1,41 @@
+export const selectedRows = [];
+export const tableHeaderCheckboxModel = false;
+export const tableHeaderCheckboxIndeterminate = false;
+
+const BVTableSelectableMixin = {
+ methods: {
+ clearSelectedRows(tableRef) {
+ if (tableRef) tableRef.clearSelected();
+ },
+ toggleSelectRow(tableRef, rowIndex) {
+ if (tableRef && rowIndex !== undefined) {
+ tableRef.isRowSelected(rowIndex)
+ ? tableRef.unselectRow(rowIndex)
+ : tableRef.selectRow(rowIndex);
+ }
+ },
+ onRowSelected(selectedRows, totalRowsCount) {
+ if (selectedRows && totalRowsCount !== undefined) {
+ this.selectedRows = selectedRows;
+ if (selectedRows.length === 0) {
+ this.tableHeaderCheckboxIndeterminate = false;
+ this.tableHeaderCheckboxModel = false;
+ } else if (selectedRows.length === totalRowsCount) {
+ this.tableHeaderCheckboxIndeterminate = false;
+ this.tableHeaderCheckboxModel = true;
+ } else {
+ this.tableHeaderCheckboxIndeterminate = true;
+ this.tableHeaderCheckboxModel = true;
+ }
+ }
+ },
+ onChangeHeaderCheckbox(tableRef) {
+ if (tableRef) {
+ if (this.tableHeaderCheckboxModel) tableRef.selectAllRows();
+ else tableRef.clearSelected();
+ }
+ },
+ },
+};
+
+export default BVTableSelectableMixin;
diff --git a/src/components/_sila/Mixins/BVToastMixin.js b/src/components/_sila/Mixins/BVToastMixin.js
new file mode 100644
index 00000000..a04ef438
--- /dev/null
+++ b/src/components/_sila/Mixins/BVToastMixin.js
@@ -0,0 +1,115 @@
+import StatusIcon from '../Global/StatusIcon';
+
+const BVToastMixin = {
+ components: {
+ StatusIcon,
+ },
+ methods: {
+ $_BVToastMixin_createTitle(title, status) {
+ const statusIcon = this.$createElement('StatusIcon', {
+ props: { status },
+ });
+ const titleWithIcon = this.$createElement(
+ 'strong',
+ { class: 'toast-icon' },
+ [statusIcon, title]
+ );
+ return titleWithIcon;
+ },
+ $_BVToastMixin_createBody(messageBody) {
+ if (Array.isArray(messageBody)) {
+ return messageBody.map((message) =>
+ this.$createElement('p', { class: 'mb-0' }, message)
+ );
+ } else {
+ return [this.$createElement('p', { class: 'mb-0' }, messageBody)];
+ }
+ },
+ $_BVToastMixin_createTimestamp() {
+ const timestamp = this.$options.filters.formatTime(new Date());
+ return this.$createElement('p', { class: 'mt-3 mb-0' }, timestamp);
+ },
+ $_BVToastMixin_createRefreshAction() {
+ return this.$createElement(
+ 'BLink',
+ {
+ class: 'd-inline-block mt-3',
+ on: {
+ click: () => {
+ this.$root.$emit('refresh-application');
+ },
+ },
+ },
+ this.$t('global.action.refresh')
+ );
+ },
+ $_BVToastMixin_initToast(body, title, variant) {
+ this.$root.$bvToast.toast(body, {
+ title,
+ variant,
+ autoHideDelay: 10000, //auto hide in milliseconds
+ noAutoHide: variant !== 'success',
+ isStatus: true,
+ solid: true,
+ });
+ },
+ successToast(
+ message,
+ {
+ title: t = this.$t('global.status.success'),
+ timestamp,
+ refreshAction,
+ } = {}
+ ) {
+ const body = this.$_BVToastMixin_createBody(message);
+ const title = this.$_BVToastMixin_createTitle(t, 'success');
+ if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
+ if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+ this.$_BVToastMixin_initToast(body, title, 'success');
+ },
+ errorToast(
+ message,
+ {
+ title: t = this.$t('global.status.error'),
+ timestamp,
+ refreshAction,
+ } = {}
+ ) {
+ const body = this.$_BVToastMixin_createBody(message);
+ const title = this.$_BVToastMixin_createTitle(t, 'danger');
+ if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
+ if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+ this.$_BVToastMixin_initToast(body, title, 'danger');
+ },
+ warningToast(
+ message,
+ {
+ title: t = this.$t('global.status.warning'),
+ timestamp,
+ refreshAction,
+ } = {}
+ ) {
+ const body = this.$_BVToastMixin_createBody(message);
+ const title = this.$_BVToastMixin_createTitle(t, 'warning');
+ if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
+ if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+ this.$_BVToastMixin_initToast(body, title, 'warning');
+ },
+ infoToast(
+ message,
+ {
+ title: t = this.$t('global.status.informational'),
+ timestamp,
+ refreshAction,
+ } = {}
+ ) {
+ const body = this.$_BVToastMixin_createBody(message);
+ const title = this.$_BVToastMixin_createTitle(t, 'info');
+ if (refreshAction) body.push(this.$_BVToastMixin_createRefreshAction());
+ if (timestamp) body.push(this.$_BVToastMixin_createTimestamp());
+ this.$_BVToastMixin_initToast(body, title, 'info');
+ },
+ },
+};
+
+export default BVToastMixin;
diff --git a/src/components/_sila/Mixins/DataFormatterMixin.js b/src/components/_sila/Mixins/DataFormatterMixin.js
new file mode 100644
index 00000000..5ce79327
--- /dev/null
+++ b/src/components/_sila/Mixins/DataFormatterMixin.js
@@ -0,0 +1,30 @@
+const DataFormatterMixin = {
+ methods: {
+ dataFormatter(value) {
+ if (value === undefined || value === null || value === '') {
+ return '--';
+ } else if (typeof value === 'number') {
+ return parseFloat(value.toFixed(3));
+ } else {
+ return value;
+ }
+ },
+ statusIcon(status) {
+ switch (status) {
+ case 'OK':
+ return 'success';
+ case 'Warning':
+ return 'warning';
+ case 'Critical':
+ return 'danger';
+ default:
+ return '';
+ }
+ },
+ dataFormatterArray(value) {
+ return value.join(', ');
+ },
+ },
+};
+
+export default DataFormatterMixin;
diff --git a/src/components/_sila/Mixins/JumpLinkMixin.js b/src/components/_sila/Mixins/JumpLinkMixin.js
new file mode 100644
index 00000000..b038527b
--- /dev/null
+++ b/src/components/_sila/Mixins/JumpLinkMixin.js
@@ -0,0 +1,27 @@
+const JumpLinkMixin = {
+ methods: {
+ setFocus(element) {
+ element.setAttribute('tabindex', '-1');
+ element.focus();
+ // Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk
+ element.removeAttribute('tabindex');
+ },
+ scrollToOffset(event) {
+ // Select element to scroll to
+ const ref = event.target.getAttribute('data-ref');
+ const element = this.$refs[ref].$el;
+
+ // Set focus and tabindex on selected element
+ this.setFocus(element);
+
+ // Set scroll offset below header
+ const offset = element.offsetTop - 50;
+ window.scroll({
+ top: offset,
+ behavior: 'smooth',
+ });
+ },
+ },
+};
+
+export default JumpLinkMixin;
diff --git a/src/components/_sila/Mixins/LoadingBarMixin.js b/src/components/_sila/Mixins/LoadingBarMixin.js
new file mode 100644
index 00000000..d1152703
--- /dev/null
+++ b/src/components/_sila/Mixins/LoadingBarMixin.js
@@ -0,0 +1,19 @@
+export const loading = true;
+
+const LoadingBarMixin = {
+ methods: {
+ startLoader() {
+ this.$root.$emit('loader-start');
+ this.loading = true;
+ },
+ endLoader() {
+ this.$root.$emit('loader-end');
+ this.loading = false;
+ },
+ hideLoader() {
+ this.$root.$emit('loader-hide');
+ },
+ },
+};
+
+export default LoadingBarMixin;
diff --git a/src/components/_sila/Mixins/LocalTimezoneLabelMixin.js b/src/components/_sila/Mixins/LocalTimezoneLabelMixin.js
new file mode 100644
index 00000000..6b4141c6
--- /dev/null
+++ b/src/components/_sila/Mixins/LocalTimezoneLabelMixin.js
@@ -0,0 +1,14 @@
+import { format } from 'date-fns-tz';
+
+const LocalTimezoneLabelMixin = {
+ methods: {
+ localOffset() {
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ const shortTz = this.$options.filters.shortTimeZone(new Date());
+ const pattern = `'${shortTz}' O`;
+ return format(new Date(), pattern, { timezone }).replace('GMT', 'UTC');
+ },
+ },
+};
+
+export default LocalTimezoneLabelMixin;
diff --git a/src/components/_sila/Mixins/SearchFilterMixin.js b/src/components/_sila/Mixins/SearchFilterMixin.js
new file mode 100644
index 00000000..a4819e26
--- /dev/null
+++ b/src/components/_sila/Mixins/SearchFilterMixin.js
@@ -0,0 +1,14 @@
+export const searchFilter = null;
+
+const SearchFilterMixin = {
+ methods: {
+ onChangeSearchInput(searchValue) {
+ this.searchFilter = searchValue;
+ },
+ onClearSearchInput() {
+ this.searchFilter = null;
+ },
+ },
+};
+
+export default SearchFilterMixin;
diff --git a/src/components/_sila/Mixins/TableFilterMixin.js b/src/components/_sila/Mixins/TableFilterMixin.js
new file mode 100644
index 00000000..7a2cc540
--- /dev/null
+++ b/src/components/_sila/Mixins/TableFilterMixin.js
@@ -0,0 +1,58 @@
+import { includes } from 'lodash';
+
+const TableFilterMixin = {
+ methods: {
+ getFilteredTableData(tableData = [], filters = []) {
+ const filterItems = filters.reduce((arr, filter) => {
+ return [...arr, ...filter.values];
+ }, []);
+ // If no filters are active, then return all table data
+ if (filterItems.length === 0) return tableData;
+
+ // Check if row property value is included in list of
+ // active filters
+ return tableData.filter((row) => {
+ let returnRow = false;
+ for (const { key, values } of filters) {
+ const rowProperty = row[key];
+ if (rowProperty && includes(values, rowProperty)) {
+ returnRow = true;
+ break;
+ }
+ }
+ return returnRow;
+ });
+ },
+ getFilteredTableDataByDate(
+ tableData = [],
+ startDate,
+ endDate,
+ propertyKey = 'date'
+ ) {
+ if (!startDate && !endDate) return tableData;
+ let startDateInMs = startDate ? startDate.getTime() : 0;
+ let endDateInMs = endDate ? endDate.getTime() : Number.POSITIVE_INFINITY;
+
+ const isUtcDisplay = this.$store.getters['global/isUtcDisplay'];
+
+ //Offset preference selected
+ if (!isUtcDisplay) {
+ startDateInMs = startDate
+ ? startDate.getTime() + startDate.getTimezoneOffset() * 60000
+ : 0;
+ endDateInMs = endDate
+ ? endDate.getTime() + endDate.getTimezoneOffset() * 60000
+ : Number.POSITIVE_INFINITY;
+ }
+
+ return tableData.filter((row) => {
+ const date = row[propertyKey];
+ if (!(date instanceof Date)) return;
+ const dateInMs = date.getTime();
+ if (dateInMs >= startDateInMs && dateInMs <= endDateInMs) return row;
+ });
+ },
+ },
+};
+
+export default TableFilterMixin;
diff --git a/src/components/_sila/Mixins/TableRowExpandMixin.js b/src/components/_sila/Mixins/TableRowExpandMixin.js
new file mode 100644
index 00000000..7f815a46
--- /dev/null
+++ b/src/components/_sila/Mixins/TableRowExpandMixin.js
@@ -0,0 +1,15 @@
+import i18n from '@/i18n';
+export const expandRowLabel = i18n.t('global.table.expandTableRow');
+
+const TableRowExpandMixin = {
+ methods: {
+ toggleRowDetails(row) {
+ row.toggleDetails();
+ row.detailsShowing
+ ? (this.expandRowLabel = this.$t('global.table.expandTableRow'))
+ : (this.expandRowLabel = this.$t('global.table.collapseTableRow'));
+ },
+ },
+};
+
+export default TableRowExpandMixin;
diff --git a/src/components/_sila/Mixins/TableSortMixin.js b/src/components/_sila/Mixins/TableSortMixin.js
new file mode 100644
index 00000000..c0997350
--- /dev/null
+++ b/src/components/_sila/Mixins/TableSortMixin.js
@@ -0,0 +1,11 @@
+const STATUS = ['OK', 'Warning', 'Critical'];
+
+const TableSortMixin = {
+ methods: {
+ sortStatus(a, b, key) {
+ return STATUS.indexOf(a[key]) - STATUS.indexOf(b[key]);
+ },
+ },
+};
+
+export default TableSortMixin;
diff --git a/src/components/_sila/Mixins/VuelidateMixin.js b/src/components/_sila/Mixins/VuelidateMixin.js
new file mode 100644
index 00000000..fec85251
--- /dev/null
+++ b/src/components/_sila/Mixins/VuelidateMixin.js
@@ -0,0 +1,10 @@
+const VuelidateMixin = {
+ methods: {
+ getValidationState(model) {
+ const { $dirty, $error } = model;
+ return $dirty ? !$error : null;
+ },
+ },
+};
+
+export default VuelidateMixin;
diff --git a/src/env/assets/styles/_sila.scss b/src/env/assets/styles/_sila.scss
new file mode 100644
index 00000000..884d62b5
--- /dev/null
+++ b/src/env/assets/styles/_sila.scss
@@ -0,0 +1,92 @@
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-ExtraLight.woff2') format('woff2');
+ font-weight: 200;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-ExtraLightItalic.woff2') format('woff2');
+ font-weight: 200;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-Light.woff2') format('woff2');
+ font-weight: 300;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-Regular.woff2') format('woff2');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-Italic.woff2') format('woff2');
+ font-weight: 400;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-Medium.woff2') format('woff2');
+ font-weight: 500;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-Bold.woff2') format('woff2');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'Inter';
+ src: url('~@/env/assets/fonts/Inter/Inter-BoldItalic.woff2') format('woff2');
+ font-weight: 700;
+ font-style: italic;
+}
+
+// IBS uses Inter https://github.com/rsms/inter
+
+$font-family-base: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+$dark: #2c405a;
+
+$blue: #0070ff;
+$red: #e11717;
+$green: #34b233;
+$yellow: #f5bd1f;
+
+$primary: $red;
+$danger: $red;
+$success: $green;
+$warning: $yellow;
+
+$dark-hover: #3c506a;
+
+$red-hover: #FC2A2A;
+$red-active: #df2323;
+$red-disabled: #E17171;
+$red-click: #C71414;
+$red-shadow: #e1171780;
+$red-light-background: #e117170d;
+
+$gray-2: #fbfbfc;
+$gray-5: #1a3e5b0d;
+$gray-5-hover: #1427351a;
+$gray-10: #1a3e5b1a;
+$gray-20: #1a3e5b33;
+$red-40:#e1171766;
+
+$surface-secondary: #F3F4F5;
+$on-surface-secondary: #040a0f99;
+$on-surface-tretiatry: #040A0F4D;
+
+$text-primary: #0C1C29;
+$text-secondary: #0C1C29E5;
+$text-tretiatry: #0c1c2999;
+$text-quaternary: #0c1c294d;
+
+$login-page-description-color: #0c1c2999;
+
+$border-radius: 8px;
+
+$loading-color: #c11d1d;
+$navbar-color: $dark;
+
diff --git a/src/env/components/AppNavigation/sila.js b/src/env/components/AppNavigation/sila.js
new file mode 100644
index 00000000..bbbbb1ee
--- /dev/null
+++ b/src/env/components/AppNavigation/sila.js
@@ -0,0 +1,182 @@
+import IconDashboard from '@carbon/icons-vue/es/dashboard/16';
+import IconTextLinkAnalysis from '@carbon/icons-vue/es/text-link--analysis/16';
+import IconDataCheck from '@carbon/icons-vue/es/data--check/16';
+import IconSettingsAdjust from '@carbon/icons-vue/es/settings--adjust/16';
+import IconSettings from '@carbon/icons-vue/es/settings/16';
+import IconSecurity from '@carbon/icons-vue/es/security/16';
+import IconChevronUp from '@carbon/icons-vue/es/chevron--up/16';
+import IconDataBase from '@carbon/icons-vue/es/data--base--alt/16';
+
+const AppNavigationMixin = {
+ components: {
+ iconOverview: IconDashboard,
+ iconLogs: IconTextLinkAnalysis,
+ iconHealth: IconDataCheck,
+ iconControl: IconSettingsAdjust,
+ iconSettings: IconSettings,
+ iconSecurityAndAccess: IconSecurity,
+ iconExpand: IconChevronUp,
+ iconResourceManagement: IconDataBase,
+ },
+ data() {
+ return {
+ navigationItems: [
+ {
+ id: 'overview',
+ label: this.$t('appNavigation.overview'),
+ route: '/',
+ icon: 'iconOverview',
+ },
+ {
+ id: 'logs',
+ label: this.$t('appNavigation.logs'),
+ icon: 'iconLogs',
+ children: [
+ {
+ id: 'event-logs',
+ label: this.$t('appNavigation.eventLogs'),
+ route: '/logs/event-logs',
+ },
+ {
+ id: 'post-code-logs',
+ label: this.$t('appNavigation.postCodeLogs'),
+ route: '/logs/post-code-logs',
+ },
+ ],
+ },
+ {
+ id: 'hardware-status',
+ label: this.$t('appNavigation.hardwareStatus'),
+ icon: 'iconHealth',
+ children: [
+ {
+ id: 'inventory',
+ label: this.$t('appNavigation.inventory'),
+ route: '/hardware-status/inventory',
+ },
+ {
+ id: 'sensors',
+ label: this.$t('appNavigation.sensors'),
+ route: '/hardware-status/sensors',
+ },
+ ],
+ },
+ {
+ id: 'operations',
+ label: this.$t('appNavigation.operations'),
+ icon: 'iconControl',
+ children: [
+ {
+ id: 'factory-reset',
+ label: this.$t('appNavigation.factoryReset'),
+ route: '/operations/factory-reset',
+ },
+ {
+ id: 'kvm',
+ label: this.$t('appNavigation.kvm'),
+ route: '/operations/kvm',
+ },
+ {
+ id: 'key-clear',
+ label: this.$t('appNavigation.keyClear'),
+ route: '/operations/key-clear',
+ },
+ {
+ id: 'firmware',
+ label: this.$t('appNavigation.firmware'),
+ route: '/operations/firmware',
+ },
+ {
+ id: 'reboot-bmc',
+ label: this.$t('appNavigation.rebootBmc'),
+ route: '/operations/reboot-bmc',
+ },
+ {
+ id: 'serial-over-lan',
+ label: this.$t('appNavigation.serialOverLan'),
+ route: '/operations/serial-over-lan',
+ },
+ {
+ id: 'server-power-operations',
+ label: this.$t('appNavigation.serverPowerOperations'),
+ route: '/operations/server-power-operations',
+ },
+ {
+ id: 'virtual-media',
+ label: this.$t('appNavigation.virtualMedia'),
+ route: '/operations/virtual-media',
+ },
+ ],
+ },
+ {
+ id: 'settings',
+ label: this.$t('appNavigation.settings'),
+ icon: 'iconSettings',
+ children: [
+ {
+ id: 'date-time',
+ label: this.$t('appNavigation.dateTime'),
+ route: '/settings/date-time',
+ },
+ {
+ id: 'network',
+ label: this.$t('appNavigation.network'),
+ route: '/settings/network',
+ },
+ {
+ id: 'power-restore-policy',
+ label: this.$t('appNavigation.powerRestorePolicy'),
+ route: '/settings/power-restore-policy',
+ },
+ ],
+ },
+ {
+ id: 'security-and-access',
+ label: this.$t('appNavigation.securityAndAccess'),
+ icon: 'iconSecurityAndAccess',
+ children: [
+ {
+ id: 'sessions',
+ label: this.$t('appNavigation.sessions'),
+ route: '/security-and-access/sessions',
+ },
+ {
+ id: 'ldap',
+ label: this.$t('appNavigation.ldap'),
+ route: '/security-and-access/ldap',
+ },
+ {
+ id: 'user-management',
+ label: this.$t('appNavigation.userManagement'),
+ route: '/security-and-access/user-management',
+ },
+ {
+ id: 'policies',
+ label: this.$t('appNavigation.policies'),
+ route: '/security-and-access/policies',
+ },
+ {
+ id: 'certificates',
+ label: this.$t('appNavigation.certificates'),
+ route: '/security-and-access/certificates',
+ },
+ ],
+ },
+ {
+ id: 'resource-management',
+ label: this.$t('appNavigation.resourceManagement'),
+ icon: 'iconResourceManagement',
+ children: [
+ {
+ id: 'power',
+ label: this.$t('appNavigation.power'),
+ route: '/resource-management/power',
+ },
+ ],
+ },
+ ],
+ };
+ },
+};
+
+export default AppNavigationMixin;
diff --git a/src/env/router/sila.js b/src/env/router/sila.js
new file mode 100644
index 00000000..39590cfa
--- /dev/null
+++ b/src/env/router/sila.js
@@ -0,0 +1,286 @@
+import AppLayout from '@/layouts/_sila/AppLayout.vue';
+import ChangePassword from '@/views/_sila/ChangePassword';
+import Sessions from '@/views/_sila/SecurityAndAccess/Sessions';
+import ConsoleLayout from '@/layouts/_sila/ConsoleLayout.vue';
+import DateTime from '@/views/_sila/Settings/DateTime';
+import EventLogs from '@/views/_sila/Logs/EventLogs';
+import Firmware from '@/views/_sila/Operations/Firmware';
+import FactoryReset from '@/views/_sila/Operations/FactoryReset';
+import Inventory from '@/views/_sila/HardwareStatus/Inventory';
+import Kvm from '@/views/_sila/Operations/Kvm';
+import KvmConsole from '@/views/_sila/Operations/Kvm/KvmConsole';
+import Ldap from '@/views/_sila/SecurityAndAccess/Ldap';
+import UserManagement from '@/views/_sila/SecurityAndAccess/UserManagement';
+import Login from '@/views/_sila/Login';
+import LoginLayout from '@/layouts/_sila/LoginLayout';
+import Network from '@/views/_sila/Settings/Network';
+import Overview from '@/views/_sila/Overview';
+import PageNotFound from '@/views/_sila/PageNotFound';
+import PostCodeLogs from '@/views/_sila/Logs/PostCodeLogs';
+import PowerRestorePolicy from '@/views/_sila/Settings/PowerRestorePolicy';
+import ProfileSettings from '@/views/_sila/ProfileSettings';
+import RebootBmc from '@/views/_sila/Operations/RebootBmc';
+import Policies from '@/views/_sila/SecurityAndAccess/Policies';
+import Sensors from '@/views/_sila/HardwareStatus/Sensors';
+import SerialOverLan from '@/views/_sila/Operations/SerialOverLan';
+import SerialOverLanConsole from '@/views/_sila/Operations/SerialOverLan/SerialOverLanConsole';
+import ServerPowerOperations from '@/views/_sila/Operations/ServerPowerOperations';
+import KeyClear from '@/views/_sila/Operations/KeyClear';
+import Certificates from '@/views/_sila/SecurityAndAccess/Certificates';
+import VirtualMedia from '@/views/_sila/Operations/VirtualMedia';
+import Power from '@/views/_sila/ResourceManagement/Power';
+import i18n from '@/i18n';
+
+const routes = [
+ {
+ path: '/login',
+ component: LoginLayout,
+ children: [
+ {
+ path: '',
+ name: 'login',
+ component: Login,
+ meta: {
+ title: i18n.t('appPageTitle.login'),
+ },
+ },
+ {
+ path: '/change-password',
+ name: 'change-password',
+ component: ChangePassword,
+ meta: {
+ title: i18n.t('appPageTitle.changePassword'),
+ requiresAuth: true,
+ },
+ },
+ ],
+ },
+ {
+ path: '/console',
+ component: ConsoleLayout,
+ meta: {
+ requiresAuth: true,
+ },
+ children: [
+ {
+ path: 'serial-over-lan-console',
+ name: 'serial-over-lan-console',
+ component: SerialOverLanConsole,
+ meta: {
+ title: i18n.t('appPageTitle.serialOverLan'),
+ },
+ },
+ {
+ path: 'kvm',
+ name: 'kvm-console',
+ component: KvmConsole,
+ meta: {
+ title: i18n.t('appPageTitle.kvm'),
+ },
+ },
+ ],
+ },
+ {
+ path: '/',
+ meta: {
+ requiresAuth: true,
+ },
+ component: AppLayout,
+ children: [
+ {
+ path: '',
+ name: 'overview',
+ component: Overview,
+ meta: {
+ title: i18n.t('appPageTitle.overview'),
+ },
+ },
+ {
+ path: '/profile-settings',
+ name: 'profile-settings',
+ component: ProfileSettings,
+ meta: {
+ title: i18n.t('appPageTitle.profileSettings'),
+ },
+ },
+ {
+ path: '/logs/event-logs',
+ name: 'event-logs',
+ component: EventLogs,
+ meta: {
+ title: i18n.t('appPageTitle.eventLogs'),
+ },
+ },
+ {
+ path: '/logs/post-code-logs',
+ name: 'post-code-logs',
+ component: PostCodeLogs,
+ meta: {
+ title: i18n.t('appPageTitle.postCodeLogs'),
+ },
+ },
+ {
+ path: '/hardware-status/inventory',
+ name: 'inventory',
+ component: Inventory,
+ meta: {
+ title: i18n.t('appPageTitle.inventory'),
+ },
+ },
+ {
+ path: '/hardware-status/sensors',
+ name: 'sensors',
+ component: Sensors,
+ meta: {
+ title: i18n.t('appPageTitle.sensors'),
+ },
+ },
+ {
+ path: '/security-and-access/sessions',
+ name: 'sessions',
+ component: Sessions,
+ meta: {
+ title: i18n.t('appPageTitle.sessions'),
+ },
+ },
+ {
+ path: '/security-and-access/ldap',
+ name: 'ldap',
+ component: Ldap,
+ meta: {
+ title: i18n.t('appPageTitle.ldap'),
+ },
+ },
+ {
+ path: '/security-and-access/user-management',
+ name: 'user-management',
+ component: UserManagement,
+ meta: {
+ title: i18n.t('appPageTitle.userManagement'),
+ },
+ },
+ {
+ path: '/security-and-access/policies',
+ name: 'policies',
+ component: Policies,
+ meta: {
+ title: i18n.t('appPageTitle.policies'),
+ },
+ },
+ {
+ path: '/security-and-access/certificates',
+ name: 'certificates',
+ component: Certificates,
+ meta: {
+ title: i18n.t('appPageTitle.certificates'),
+ },
+ },
+ {
+ path: '/settings/date-time',
+ name: 'date-time',
+ component: DateTime,
+ meta: {
+ title: i18n.t('appPageTitle.dateTime'),
+ },
+ },
+ {
+ path: '/operations/kvm',
+ name: 'kvm',
+ component: Kvm,
+ meta: {
+ title: i18n.t('appPageTitle.kvm'),
+ },
+ },
+ {
+ path: '/operations/firmware',
+ name: 'firmware',
+ component: Firmware,
+ meta: {
+ title: i18n.t('appPageTitle.firmware'),
+ },
+ },
+ {
+ path: '/settings/network',
+ name: 'network',
+ component: Network,
+ meta: {
+ title: i18n.t('appPageTitle.network'),
+ },
+ },
+ {
+ path: '/settings/power-restore-policy',
+ name: 'power-restore-policy',
+ component: PowerRestorePolicy,
+ meta: {
+ title: i18n.t('appPageTitle.powerRestorePolicy'),
+ },
+ },
+ {
+ path: '/resource-management/power',
+ name: 'power',
+ component: Power,
+ meta: {
+ title: i18n.t('appPageTitle.power'),
+ },
+ },
+ {
+ path: '/operations/factory-reset',
+ name: 'factory-reset',
+ component: FactoryReset,
+ meta: {
+ title: i18n.t('appPageTitle.factoryReset'),
+ },
+ },
+ {
+ path: '/operations/key-clear',
+ name: 'key-clear',
+ component: KeyClear,
+ meta: {
+ title: i18n.t('appPageTitle.keyClear'),
+ },
+ },
+ {
+ path: '/operations/reboot-bmc',
+ name: 'reboot-bmc',
+ component: RebootBmc,
+ meta: {
+ title: i18n.t('appPageTitle.rebootBmc'),
+ },
+ },
+ {
+ path: '/operations/serial-over-lan',
+ name: 'serial-over-lan',
+ component: SerialOverLan,
+ meta: {
+ title: i18n.t('appPageTitle.serialOverLan'),
+ },
+ },
+ {
+ path: '/operations/server-power-operations',
+ name: 'server-power-operations',
+ component: ServerPowerOperations,
+ meta: {
+ title: i18n.t('appPageTitle.serverPowerOperations'),
+ },
+ },
+ {
+ path: '/operations/virtual-media',
+ name: 'virtual-media',
+ component: VirtualMedia,
+ meta: {
+ title: i18n.t('appPageTitle.virtualMedia'),
+ },
+ },
+ {
+ path: '*',
+ name: 'page-not-found',
+ component: PageNotFound,
+ meta: {
+ title: i18n.t('appPageTitle.pageNotFound'),
+ },
+ },
+ ],
+ },
+];
+
+export default routes;
diff --git a/src/env/store/sila.js b/src/env/store/sila.js
new file mode 100644
index 00000000..d0834c3b
--- /dev/null
+++ b/src/env/store/sila.js
@@ -0,0 +1,10 @@
+import store from '@/store';
+import KeyClearStore from '@/store/modules/Operations/KeyClearStore';
+
+store.registerModule('key-clear', KeyClearStore);
+
+// Use store.registerModule() to register env specific
+// store modules
+// https://vuex.vuejs.org/api/#registermodule
+
+export default store;
diff --git a/src/layouts/_sila/AppLayout.vue b/src/layouts/_sila/AppLayout.vue
new file mode 100644
index 00000000..0b78e5b1
--- /dev/null
+++ b/src/layouts/_sila/AppLayout.vue
@@ -0,0 +1,91 @@
+<template>
+ <div class="app-container">
+ <app-header
+ ref="focusTarget"
+ class="app-header"
+ :router-key="routerKey"
+ @refresh="refresh"
+ />
+ <app-navigation class="app-navigation" />
+ <page-container class="app-content">
+ <router-view ref="routerView" :key="routerKey" />
+ <!-- Scroll to top button -->
+ <button-back-to-top />
+ </page-container>
+ </div>
+</template>
+
+<script>
+import AppHeader from '@/components/AppHeader';
+import AppNavigation from '@/components/AppNavigation';
+import PageContainer from '@/components/Global/PageContainer';
+import ButtonBackToTop from '@/components/Global/ButtonBackToTop';
+import JumpLinkMixin from '@/components/Mixins/JumpLinkMixin';
+
+export default {
+ name: 'App',
+ components: {
+ AppHeader,
+ AppNavigation,
+ PageContainer,
+ ButtonBackToTop,
+ },
+ mixins: [JumpLinkMixin],
+ data() {
+ return {
+ routerKey: 0,
+ };
+ },
+ watch: {
+ $route: function () {
+ this.$nextTick(function () {
+ this.setFocus(this.$refs.focusTarget.$el);
+ });
+ },
+ },
+ mounted() {
+ this.$root.$on('refresh-application', () => this.refresh());
+ },
+ methods: {
+ refresh() {
+ // Changing the component :key value will trigger
+ // a component re-rendering and 'refresh' the view
+ this.routerKey += 1;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+ display: grid;
+ grid-template-columns: 100%;
+ grid-template-rows: auto;
+ grid-template-areas:
+ 'header'
+ 'content';
+
+ @include media-breakpoint-up($responsive-layout-bp) {
+ grid-template-columns: $navigation-width 1fr;
+ grid-template-areas:
+ 'header header'
+ 'navigation content';
+ }
+}
+
+.app-header {
+ grid-area: header;
+ position: sticky;
+ top: 0;
+ z-index: $zindex-fixed + 1;
+}
+
+.app-navigation {
+ grid-area: navigation;
+}
+
+.app-content {
+ grid-area: content;
+ background-color: $white;
+}
+</style>
diff --git a/src/layouts/_sila/ConsoleLayout.vue b/src/layouts/_sila/ConsoleLayout.vue
new file mode 100644
index 00000000..9f8175bf
--- /dev/null
+++ b/src/layouts/_sila/ConsoleLayout.vue
@@ -0,0 +1,9 @@
+<template>
+ <router-view />
+</template>
+
+<script>
+export default {
+ name: 'Console',
+};
+</script>
diff --git a/src/layouts/_sila/LoginLayout.vue b/src/layouts/_sila/LoginLayout.vue
new file mode 100644
index 00000000..cdff2040
--- /dev/null
+++ b/src/layouts/_sila/LoginLayout.vue
@@ -0,0 +1,112 @@
+<template>
+ <main>
+ <div class="login-container">
+ <div class="login-main">
+ <div>
+ <div class="login-brand mb-5">
+ <img
+ width="90px"
+ src="@/assets/images/login-company-logo.svg"
+ :alt="altLogo"
+ />
+ </div>
+ <h1 v-if="customizableGuiName" class="h3 mb-5">
+ {{ customizableGuiName }}
+ </h1>
+ <router-view class="login=form form-background" />
+ </div>
+ </div>
+ <div class="login-aside">
+ <div class="login-aside__logo-brand">
+ <!-- Add Secondary brand logo if needed -->
+ </div>
+ <div class="login-aside__logo-bmc">
+ <img
+ height="60px"
+ src="@/assets/images/built-on-openbmc-logo.svg"
+ alt="Built on OpenBMC"
+ />
+ </div>
+ </div>
+ </div>
+ </main>
+</template>
+
+<script>
+export default {
+ name: 'LoginLayout',
+ data() {
+ return {
+ altLogo: process.env.VUE_APP_COMPANY_NAME || 'OpenBMC',
+ customizableGuiName: process.env.VUE_APP_GUI_NAME || '',
+ };
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.login-container {
+ background: gray('100');
+ display: flex;
+ flex-direction: column;
+ gap: $spacer * 2;
+ max-width: 1400px;
+ min-width: 320px;
+ min-height: 100vh;
+ justify-content: space-around;
+
+ @include media-breakpoint-up('md') {
+ background: $white;
+ flex-direction: row;
+ }
+}
+
+.login-main {
+ min-height: 50vh;
+ padding: $spacer * 3;
+
+ @include media-breakpoint-up('md') {
+ background: gray('100');
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 75%;
+ min-height: 100vh;
+ justify-content: center;
+ align-items: center;
+ }
+}
+
+.login-form {
+ @include media-breakpoint-up('md') {
+ max-width: 360px;
+ }
+}
+
+.login-aside {
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-end;
+ gap: $spacer * 1.5;
+ margin-right: $spacer * 3;
+ margin-bottom: $spacer;
+
+ @include media-breakpoint-up('md') {
+ min-height: 100vh;
+ padding-bottom: $spacer;
+ flex: 1 1 25%;
+ margin-bottom: 0;
+ }
+}
+
+.login-aside__logo-brand:not(:empty) {
+ &::after {
+ content: '';
+ display: inline-block;
+ height: 2.5rem;
+ width: 2px;
+ background-color: gray('200');
+ margin-left: $spacer * 1.5;
+ vertical-align: middle;
+ }
+}
+</style>
diff --git a/src/views/_sila/ChangePassword/ChangePassword.vue b/src/views/_sila/ChangePassword/ChangePassword.vue
new file mode 100644
index 00000000..2440ace1
--- /dev/null
+++ b/src/views/_sila/ChangePassword/ChangePassword.vue
@@ -0,0 +1,134 @@
+<template>
+ <div class="change-password-container">
+ <alert variant="danger" class="mb-4">
+ <p v-if="changePasswordError">
+ {{ $t('pageChangePassword.changePasswordError') }}
+ </p>
+ <p v-else>{{ $t('pageChangePassword.changePasswordAlertMessage') }}</p>
+ </alert>
+ <div class="change-password__form-container">
+ <dl>
+ <dt>{{ $t('pageChangePassword.username') }}</dt>
+ <dd>{{ username }}</dd>
+ </dl>
+ <b-form novalidate @submit.prevent="changePassword">
+ <b-form-group
+ label-for="password"
+ :label="$t('pageChangePassword.newPassword')"
+ >
+ <input-password-toggle>
+ <b-form-input
+ id="password"
+ v-model="form.password"
+ autofocus="autofocus"
+ type="password"
+ :state="getValidationState($v.form.password)"
+ class="form-control-with-button"
+ @change="$v.form.password.$touch()"
+ >
+ </b-form-input>
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.password.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ <b-form-group
+ label-for="password-confirm"
+ :label="$t('pageChangePassword.confirmNewPassword')"
+ >
+ <input-password-toggle>
+ <b-form-input
+ id="password-confirm"
+ v-model="form.passwordConfirm"
+ type="password"
+ :state="getValidationState($v.form.passwordConfirm)"
+ class="form-control-with-button"
+ @change="$v.form.passwordConfirm.$touch()"
+ >
+ </b-form-input>
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.passwordConfirm.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-else-if="!$v.form.passwordConfirm.sameAsPassword">
+ {{ $t('global.form.passwordsDoNotMatch') }}
+ </template>
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ <div class="text-right">
+ <b-button type="button" variant="link" @click="goBack">
+ {{ $t('pageChangePassword.goBack') }}
+ </b-button>
+ <b-button type="submit" variant="primary">
+ {{ $t('pageChangePassword.changePassword') }}
+ </b-button>
+ </div>
+ </b-form>
+ </div>
+ </div>
+</template>
+
+<script>
+import { required, sameAs } from 'vuelidate/lib/validators';
+import Alert from '@/components/Global/Alert';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin';
+import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+export default {
+ name: 'ChangePassword',
+ components: { Alert, InputPasswordToggle },
+ mixins: [VuelidateMixin, BVToastMixin],
+ data() {
+ return {
+ form: {
+ password: null,
+ passwordConfirm: null,
+ },
+ username: this.$store.getters['global/username'],
+ changePasswordError: false,
+ };
+ },
+ validations() {
+ return {
+ form: {
+ password: { required },
+ passwordConfirm: {
+ required,
+ sameAsPassword: sameAs('password'),
+ },
+ },
+ };
+ },
+ methods: {
+ goBack() {
+ // Remove session created if navigating back to the Login page
+ this.$store.dispatch('authentication/logout');
+ },
+ changePassword() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ let data = {
+ originalUsername: this.username,
+ password: this.form.password,
+ };
+
+ this.$store
+ .dispatch('userManagement/updateUser', data)
+ .then(() => this.$router.push('/'))
+ .catch(() => (this.changePasswordError = true));
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.change-password__form-container {
+ @include media-breakpoint-up('md') {
+ max-width: 360px;
+ }
+}
+</style>
diff --git a/src/views/_sila/ChangePassword/index.js b/src/views/_sila/ChangePassword/index.js
new file mode 100644
index 00000000..9de0af42
--- /dev/null
+++ b/src/views/_sila/ChangePassword/index.js
@@ -0,0 +1,2 @@
+import ChangePassword from './ChangePassword.vue';
+export default ChangePassword;
diff --git a/src/views/_sila/HardwareStatus/Inventory/Inventory.vue b/src/views/_sila/HardwareStatus/Inventory/Inventory.vue
new file mode 100644
index 00000000..fcdbf8d2
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/Inventory.vue
@@ -0,0 +1,196 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+
+ <!-- Service indicators -->
+ <service-indicator />
+
+ <!-- Quicklinks section -->
+ <page-section :section-title="$t('pageInventory.quicklinkTitle')">
+ <b-row class="w-75">
+ <b-col v-for="column in quicklinkColumns" :key="column.id" xl="4">
+ <div v-for="item in column" :key="item.id">
+ <b-link
+ :href="item.href"
+ :data-ref="item.dataRef"
+ @click.prevent="scrollToOffset"
+ >
+ <jump-link /> {{ item.linkText }}
+ </b-link>
+ </div>
+ </b-col>
+ </b-row>
+ </page-section>
+
+ <!-- System table -->
+ <table-system ref="system" />
+
+ <!-- BMC manager table -->
+ <table-bmc-manager ref="bmc" />
+
+ <!-- Chassis table -->
+ <table-chassis ref="chassis" />
+
+ <!-- DIMM slot table -->
+ <table-dimm-slot ref="dimms" />
+
+ <!-- Fans table -->
+ <table-fans ref="fans" />
+
+ <!-- Power supplies table -->
+ <table-power-supplies ref="powerSupply" />
+
+ <!-- Processors table -->
+ <table-processors ref="processors" />
+
+ <!-- Assembly table -->
+ <table-assembly ref="assembly" />
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import ServiceIndicator from './InventoryServiceIndicator';
+import TableSystem from './InventoryTableSystem';
+import TablePowerSupplies from './InventoryTablePowerSupplies';
+import TableDimmSlot from './InventoryTableDimmSlot';
+import TableFans from './InventoryTableFans';
+import TableBmcManager from './InventoryTableBmcManager';
+import TableChassis from './InventoryTableChassis';
+import TableProcessors from './InventoryTableProcessors';
+import TableAssembly from './InventoryTableAssembly';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import PageSection from '@/components/Global/PageSection';
+import JumpLink16 from '@carbon/icons-vue/es/jump-link/16';
+import JumpLinkMixin from '@/components/Mixins/JumpLinkMixin';
+import { chunk } from 'lodash';
+
+export default {
+ components: {
+ PageTitle,
+ ServiceIndicator,
+ TableDimmSlot,
+ TablePowerSupplies,
+ TableSystem,
+ TableFans,
+ TableBmcManager,
+ TableChassis,
+ TableProcessors,
+ TableAssembly,
+ PageSection,
+ JumpLink: JumpLink16,
+ },
+ mixins: [LoadingBarMixin, JumpLinkMixin],
+ beforeRouteLeave(to, from, next) {
+ // Hide loader if user navigates away from page
+ // before requests complete
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ links: [
+ {
+ id: 'system',
+ dataRef: 'system',
+ href: '#system',
+ linkText: this.$t('pageInventory.system'),
+ },
+ {
+ id: 'bmc',
+ dataRef: 'bmc',
+ href: '#bmc',
+ linkText: this.$t('pageInventory.bmcManager'),
+ },
+ {
+ id: 'chassis',
+ dataRef: 'chassis',
+ href: '#chassis',
+ linkText: this.$t('pageInventory.chassis'),
+ },
+ {
+ id: 'dimms',
+ dataRef: 'dimms',
+ href: '#dimms',
+ linkText: this.$t('pageInventory.dimmSlot'),
+ },
+ {
+ id: 'fans',
+ dataRef: 'fans',
+ href: '#fans',
+ linkText: this.$t('pageInventory.fans'),
+ },
+ {
+ id: 'powerSupply',
+ dataRef: 'powerSupply',
+ href: '#powerSupply',
+ linkText: this.$t('pageInventory.powerSupplies'),
+ },
+ {
+ id: 'processors',
+ dataRef: 'processors',
+ href: '#processors',
+ linkText: this.$t('pageInventory.processors'),
+ },
+ {
+ id: 'assembly',
+ dataRef: 'assembly',
+ href: '#assembly',
+ linkText: this.$t('pageInventory.assemblies'),
+ },
+ ],
+ };
+ },
+ computed: {
+ quicklinkColumns() {
+ // Chunk links array to 3 array's to display 3 items per column
+ return chunk(this.links, 3);
+ },
+ },
+ created() {
+ this.startLoader();
+ const bmcManagerTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-bmc-manager-complete', () => resolve());
+ });
+ const chassisTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-chassis-complete', () => resolve());
+ });
+ const dimmSlotTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-dimm-slot-complete', () => resolve());
+ });
+ const fansTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-fans-complete', () => resolve());
+ });
+ const powerSuppliesTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-power-supplies-complete', () =>
+ resolve()
+ );
+ });
+ const processorsTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-processors-complete', () => resolve());
+ });
+ const serviceIndicatorPromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-service-complete', () => resolve());
+ });
+ const systemTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-system-complete', () => resolve());
+ });
+ const assemblyTablePromise = new Promise((resolve) => {
+ this.$root.$on('hardware-status-assembly-complete', () => resolve());
+ });
+ // Combine all child component Promises to indicate
+ // when page data load complete
+ Promise.all([
+ bmcManagerTablePromise,
+ chassisTablePromise,
+ dimmSlotTablePromise,
+ fansTablePromise,
+ powerSuppliesTablePromise,
+ processorsTablePromise,
+ serviceIndicatorPromise,
+ systemTablePromise,
+ assemblyTablePromise,
+ ]).finally(() => this.endLoader());
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryServiceIndicator.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryServiceIndicator.vue
new file mode 100644
index 00000000..01f4a446
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryServiceIndicator.vue
@@ -0,0 +1,76 @@
+<template>
+ <page-section
+ :section-title="$t('pageInventory.systemIndicator.sectionTitle')"
+ >
+ <div class="form-background pl-4 pt-4 pb-1">
+ <b-row>
+ <b-col sm="6" md="3">
+ <dl>
+ <dt>{{ $t('pageInventory.systemIndicator.powerStatus') }}</dt>
+ <dd>
+ {{ $t(powerStatus) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" md="3">
+ <dl>
+ <dt>
+ {{ $t('pageInventory.systemIndicator.identifyLed') }}
+ </dt>
+ <dd>
+ <b-form-checkbox
+ id="identifyLedSwitchService"
+ v-model="systems.locationIndicatorActive"
+ data-test-id="inventoryService-toggle-identifyLed"
+ switch
+ @change="toggleIdentifyLedSwitch"
+ >
+ <span v-if="systems.locationIndicatorActive">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else>{{ $t('global.status.off') }}</span>
+ </b-form-checkbox>
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </div>
+ </page-section>
+</template>
+<script>
+import PageSection from '@/components/Global/PageSection';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+export default {
+ components: { PageSection },
+ mixins: [BVToastMixin],
+ computed: {
+ systems() {
+ let systemData = this.$store.getters['system/systems'][0];
+ return systemData ? systemData : {};
+ },
+ serverStatus() {
+ return this.$store.getters['global/serverStatus'];
+ },
+ powerStatus() {
+ if (this.serverStatus === 'unreachable') {
+ return `global.status.off`;
+ }
+ return `global.status.${this.serverStatus}`;
+ },
+ },
+ created() {
+ this.$store.dispatch('system/getSystem').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-service-complete');
+ });
+ },
+ methods: {
+ toggleIdentifyLedSwitch(state) {
+ this.$store
+ .dispatch('system/changeIdentifyLedState', state)
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableAssembly.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableAssembly.vue
new file mode 100644
index 00000000..b4010bfe
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableAssembly.vue
@@ -0,0 +1,153 @@
+<template>
+ <page-section :section-title="$t('pageInventory.assemblies')">
+ <b-table
+ sort-icon-left
+ no-sort-reset
+ hover
+ responsive="md"
+ :items="items"
+ :fields="fields"
+ show-empty
+ :empty-text="$t('global.table.emptyMessage')"
+ :busy="isBusy"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandAssembly"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Toggle identify LED -->
+ <template #cell(identifyLed)="row">
+ <b-form-checkbox
+ v-if="hasIdentifyLed(row.item.identifyLed)"
+ v-model="row.item.identifyLed"
+ name="switch"
+ switch
+ @change="toggleIdentifyLedValue(row.item)"
+ >
+ <span v-if="row.item.identifyLed">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else> {{ $t('global.status.off') }} </span>
+ </b-form-checkbox>
+ <div v-else>--</div>
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col class="mt-2" sm="6" xl="6">
+ <!-- Nmae -->
+ <dt>{{ $t('pageInventory.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ <!-- Serial number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ </b-col>
+ <b-col class="mt-2" sm="6" xl="6">
+ <!-- Model-->
+ <dt>Model</dt>
+ <dd>{{ dataFormatter(item.model) }}</dd>
+ <!-- Spare Part Number -->
+ <dt>Spare Part Number</dt>
+ <dd>{{ dataFormatter(item.sparePartNumber) }}</dd>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ components: { IconChevron, PageSection },
+ mixins: [BVToastMixin, TableRowExpandMixin, DataFormatterMixin],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ },
+ {
+ key: 'name',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'partNumber',
+ label: this.$t('pageInventory.table.partNumber'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'identifyLed',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ assemblies() {
+ return this.$store.getters['assemblies/assemblies'];
+ },
+ items() {
+ if (this.assemblies) {
+ return this.assemblies;
+ } else {
+ return [];
+ }
+ },
+ },
+ created() {
+ this.$store.dispatch('assemblies/getAssemblyInfo').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-assembly-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ toggleIdentifyLedValue(row) {
+ this.$store
+ .dispatch('assemblies/updateIdentifyLedValue', {
+ uri: row.uri,
+ memberId: row.id,
+ identifyLed: row.identifyLed,
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ hasIdentifyLed(identifyLed) {
+ return typeof identifyLed === 'boolean';
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableBmcManager.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableBmcManager.vue
new file mode 100644
index 00000000..48b914f4
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableBmcManager.vue
@@ -0,0 +1,245 @@
+<template>
+ <page-section :section-title="$t('pageInventory.bmcManager')">
+ <b-table
+ responsive="md"
+ hover
+ :items="items"
+ :fields="fields"
+ show-empty
+ :empty-text="$t('global.table.emptyMessage')"
+ :busy="isBusy"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandBmc"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+
+ <!-- Toggle identify LED -->
+ <template #cell(identifyLed)="row">
+ <b-form-checkbox
+ v-if="hasIdentifyLed(row.item.identifyLed)"
+ v-model="row.item.identifyLed"
+ name="switch"
+ switch
+ @change="toggleIdentifyLedValue(row.item)"
+ >
+ <span v-if="row.item.identifyLed">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else> {{ $t('global.status.off') }} </span>
+ </b-form-checkbox>
+ <div v-else>--</div>
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Name -->
+ <dt>{{ $t('pageInventory.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ <!-- Part number -->
+ <dt>{{ $t('pageInventory.table.partNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.partNumber) }}</dd>
+ <!-- Serial number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ <!-- Spare part number -->
+ <dt>{{ $t('pageInventory.table.sparePartNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.sparePartNumber) }}</dd>
+ <!-- Model -->
+ <dt>{{ $t('pageInventory.table.model') }}:</dt>
+ <dd>{{ dataFormatter(item.model) }}</dd>
+ <!-- UUID -->
+ <dt>{{ $t('pageInventory.table.uuid') }}:</dt>
+ <dd>{{ dataFormatter(item.uuid) }}</dd>
+ <!-- Service entry point UUID -->
+ <dt>{{ $t('pageInventory.table.serviceEntryPointUuid') }}:</dt>
+ <dd>{{ dataFormatter(item.serviceEntryPointUuid) }}</dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ <!-- Power state -->
+ <dt>{{ $t('pageInventory.table.power') }}:</dt>
+ <dd>{{ dataFormatter(item.powerState) }}</dd>
+ <!-- Health rollup -->
+ <dt>{{ $t('pageInventory.table.healthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.healthRollup) }}</dd>
+ <!-- BMC date and time -->
+ <dt>{{ $t('pageInventory.table.bmcDateTime') }}:</dt>
+ <dd>
+ {{ item.dateTime | formatDate }}
+ {{ item.dateTime | formatTime }}
+ </dd>
+ <!-- Reset date and time -->
+ <dt>{{ $t('pageInventory.table.lastResetTime') }}:</dt>
+ <dd>
+ {{ item.lastResetTime | formatDate }}
+ {{ item.lastResetTime | formatTime }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <div class="section-divider mb-3 mt-3"></div>
+ <b-row>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Manufacturer -->
+ <dt>{{ $t('pageInventory.table.manufacturer') }}:</dt>
+ <dd>{{ dataFormatter(item.manufacturer) }}</dd>
+ <!-- Description -->
+ <dt>{{ $t('pageInventory.table.description') }}:</dt>
+ <dd>{{ dataFormatter(item.description) }}</dd>
+ <!-- Manager type -->
+ <dt>{{ $t('pageInventory.table.managerType') }}:</dt>
+ <dd>{{ dataFormatter(item.managerType) }}</dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-2" sm="6" xl="6">
+ <!-- Firmware Version -->
+ <dl>
+ <dt>{{ $t('pageInventory.table.firmwareVersion') }}:</dt>
+ <dd>{{ item.firmwareVersion }}</dd>
+ </dl>
+ <!-- Graphical console -->
+ <p class="mt-1 mb-2 h6 float-none m-0">
+ {{ $t('pageInventory.table.graphicalConsole') }}
+ </p>
+ <dl class="ml-4">
+ <dt>{{ $t('pageInventory.table.connectTypesSupported') }}:</dt>
+ <dd>
+ {{ dataFormatterArray(item.graphicalConsoleConnectTypes) }}
+ </dd>
+ <dt>{{ $t('pageInventory.table.maxConcurrentSessions') }}:</dt>
+ <dd>
+ {{ dataFormatter(item.graphicalConsoleMaxSessions) }}
+ </dd>
+ <dt>{{ $t('pageInventory.table.serviceEnabled') }}:</dt>
+ <dd>
+ {{ dataFormatter(item.graphicalConsoleEnabled) }}
+ </dd>
+ </dl>
+ <!-- Serial console -->
+ <p class="mt-1 mb-2 h6 float-none m-0">
+ {{ $t('pageInventory.table.serialConsole') }}
+ </p>
+ <dl class="ml-4">
+ <dt>{{ $t('pageInventory.table.connectTypesSupported') }}:</dt>
+ <dd>
+ {{ dataFormatterArray(item.serialConsoleConnectTypes) }}
+ </dd>
+ <dt>{{ $t('pageInventory.table.maxConcurrentSessions') }}:</dt>
+ <dd>{{ dataFormatter(item.serialConsoleMaxSessions) }}</dd>
+ <dt>{{ $t('pageInventory.table.serviceEnabled') }}:</dt>
+ <dd>{{ dataFormatter(item.serialConsoleEnabled) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+import StatusIcon from '@/components/Global/StatusIcon';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon },
+ mixins: [BVToastMixin, TableRowExpandMixin, DataFormatterMixin],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'identifyLed',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ bmc() {
+ return this.$store.getters['bmc/bmc'];
+ },
+ items() {
+ if (this.bmc) {
+ return [this.bmc];
+ } else {
+ return [];
+ }
+ },
+ },
+ created() {
+ this.$store.dispatch('bmc/getBmcInfo').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-bmc-manager-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ toggleIdentifyLedValue(row) {
+ this.$store
+ .dispatch('bmc/updateIdentifyLedValue', {
+ uri: row.uri,
+ identifyLed: row.identifyLed,
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ // TO DO: remove hasIdentifyLed method once the following story is merged:
+ // https://gerrit.openbmc-project.xyz/c/openbmc/bmcweb/+/43179
+ hasIdentifyLed(identifyLed) {
+ return typeof identifyLed === 'boolean';
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableChassis.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableChassis.vue
new file mode 100644
index 00000000..b49cec7f
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableChassis.vue
@@ -0,0 +1,191 @@
+<template>
+ <page-section :section-title="$t('pageInventory.chassis')">
+ <b-table
+ responsive="md"
+ hover
+ :items="chassis"
+ :fields="fields"
+ show-empty
+ :empty-text="$t('global.table.emptyMessage')"
+ :busy="isBusy"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandChassis"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+ <!-- Toggle identify LED -->
+ <template #cell(identifyLed)="row">
+ <b-form-checkbox
+ v-if="hasIdentifyLed(row.item.identifyLed)"
+ v-model="row.item.identifyLed"
+ name="switch"
+ switch
+ @change="toggleIdentifyLedValue(row.item)"
+ >
+ <span v-if="row.item.identifyLed">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else> {{ $t('global.status.off') }} </span>
+ </b-form-checkbox>
+ <div v-else>--</div>
+ </template>
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Name -->
+ <dt>{{ $t('pageInventory.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ <!-- Part number -->
+ <dt>{{ $t('pageInventory.table.partNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.partNumber) }}</dd>
+ <!-- Serial Number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ <!-- Model -->
+ <dt>{{ $t('pageInventory.table.model') }}:</dt>
+ <dd class="mb-2">
+ {{ dataFormatter(item.model) }}
+ </dd>
+ <!-- Asset tag -->
+ <dt>{{ $t('pageInventory.table.assetTag') }}:</dt>
+ <dd class="mb-2">
+ {{ dataFormatter(item.assetTag) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ <!-- Power state -->
+ <dt>{{ $t('pageInventory.table.power') }}:</dt>
+ <dd>{{ dataFormatter(item.power) }}</dd>
+ <!-- Health rollup -->
+ <dt>{{ $t('pageInventory.table.healthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.healthRollup) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <div class="section-divider mb-3 mt-3"></div>
+ <b-row>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Manufacturer -->
+ <dt>{{ $t('pageInventory.table.manufacturer') }}:</dt>
+ <dd>{{ dataFormatter(item.manufacturer) }}</dd>
+ <!-- Chassis Type -->
+ <dt>{{ $t('pageInventory.table.chassisType') }}:</dt>
+ <dd>{{ dataFormatter(item.chassisType) }}</dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Min power -->
+ <dt>{{ $t('pageInventory.table.minPowerWatts') }}:</dt>
+ <dd>{{ dataFormatter(item.minPowerWatts) }}</dd>
+ <!-- Max power -->
+ <dt>{{ $t('pageInventory.table.maxPowerWatts') }}:</dt>
+ <dd>{{ dataFormatter(item.maxPowerWatts) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import StatusIcon from '@/components/Global/StatusIcon';
+
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon },
+ mixins: [BVToastMixin, TableRowExpandMixin, DataFormatterMixin],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'identifyLed',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ chassis() {
+ return this.$store.getters['chassis/chassis'];
+ },
+ },
+ created() {
+ this.$store.dispatch('chassis/getChassisInfo').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-chassis-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ toggleIdentifyLedValue(row) {
+ this.$store
+ .dispatch('chassis/updateIdentifyLedValue', {
+ uri: row.uri,
+ identifyLed: row.identifyLed,
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ // TO DO: Remove this method when the LocationIndicatorActive is added from backend.
+ hasIdentifyLed(identifyLed) {
+ return typeof identifyLed === 'boolean';
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableDimmSlot.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableDimmSlot.vue
new file mode 100644
index 00000000..65994810
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableDimmSlot.vue
@@ -0,0 +1,255 @@
+<template>
+ <page-section :section-title="$t('pageInventory.dimmSlot')">
+ <b-row class="align-items-end">
+ <b-col sm="6" md="5" xl="4">
+ <search
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ </b-col>
+ <b-col sm="6" md="3" xl="2">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="dimms.length"
+ ></table-cell-count>
+ </b-col>
+ </b-row>
+ <b-table
+ sort-icon-left
+ no-sort-reset
+ hover
+ sort-by="health"
+ responsive="md"
+ show-empty
+ :items="dimms"
+ :fields="fields"
+ :sort-desc="true"
+ :sort-compare="sortCompare"
+ :filter="searchFilter"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandDimms"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+ <!-- Toggle identify LED -->
+ <template #cell(identifyLed)="row">
+ <b-form-checkbox
+ v-model="row.item.identifyLed"
+ name="switch"
+ switch
+ @change="toggleIdentifyLedValue(row.item)"
+ >
+ <span v-if="row.item.identifyLed">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else> {{ $t('global.status.off') }} </span>
+ </b-form-checkbox>
+ </template>
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col sm="6" xl="6">
+ <dl>
+ <!-- Part Number -->
+ <dt>{{ $t('pageInventory.table.partNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.partNumber) }}</dd>
+ </dl>
+ <dl>
+ <!-- Serial Number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ </dl>
+ <dl>
+ <!-- Spare Part Number -->
+ <dt>{{ $t('pageInventory.table.sparePartNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.sparePartNumber) }}</dd>
+ </dl>
+ <dl>
+ <!-- Model -->
+ <dt>{{ $t('pageInventory.table.model') }}:</dt>
+ <dd>{{ dataFormatter(item.model) }}</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" xl="6">
+ <dl>
+ <!-- Memory Size in kb -->
+ <dt>{{ $t('pageInventory.table.memorySize') }}:</dt>
+ <dd>{{ dataFormatter(item.memorySize) }} KB</dd>
+ </dl>
+ <dl>
+ <!-- Status-->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ </dl>
+ <dl>
+ <!-- Enabled-->
+ <dt>{{ $t('pageInventory.table.enabled') }}:</dt>
+ <dd>{{ dataFormatter(item.enabled) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <div class="section-divider mb-3 mt-3"></div>
+ <b-row>
+ <b-col sm="6" xl="6">
+ <dl>
+ <!-- Description -->
+ <dt>{{ $t('pageInventory.table.description') }}:</dt>
+ <dd>{{ dataFormatter(item.description) }}</dd>
+ </dl>
+ <dl>
+ <!-- Memory Type -->
+ <dt>{{ $t('pageInventory.table.memoryType') }}:</dt>
+ <dd>{{ dataFormatter(item.memoryType) }}</dd>
+ </dl>
+ <dl>
+ <!-- Base Module Type -->
+ <dt>{{ $t('pageInventory.table.baseModuleType') }}:</dt>
+ <dd>{{ dataFormatter(item.baseModuleType) }}</dd>
+ </dl>
+ <dl>
+ <!-- Capacity MiB -->
+ <dt>{{ $t('pageInventory.table.capacityMiB') }}:</dt>
+ <dd>{{ dataFormatter(item.capacityMiB) }}</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" xl="6">
+ <dl>
+ <!-- Bus Width Bits -->
+ <dt>{{ $t('pageInventory.table.busWidthBits') }}:</dt>
+ <dd>{{ dataFormatter(item.busWidthBits) }}</dd>
+ </dl>
+ <dl>
+ <!-- Data Width Bits -->
+ <dt>{{ $t('pageInventory.table.dataWidthBits') }}:</dt>
+ <dd>{{ dataFormatter(item.dataWidthBits) }}</dd>
+ </dl>
+ <dl>
+ <!-- Operating Speed Mhz -->
+ <dt>{{ $t('pageInventory.table.operatingSpeedMhz') }}:</dt>
+ <dd>{{ dataFormatter(item.operatingSpeedMhz) }} MHz</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+
+import StatusIcon from '@/components/Global/StatusIcon';
+import TableCellCount from '@/components/Global/TableCellCount';
+
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import Search from '@/components/Global/Search';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon, Search, TableCellCount },
+ mixins: [
+ TableRowExpandMixin,
+ DataFormatterMixin,
+ TableSortMixin,
+ SearchFilterMixin,
+ ],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'identifyLed',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.dimms.length;
+ },
+ dimms() {
+ return this.$store.getters['memory/dimms'];
+ },
+ },
+ created() {
+ this.$store.dispatch('memory/getDimms').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-dimm-slot-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ sortCompare(a, b, key) {
+ if (key === 'health') {
+ return this.sortStatus(a, b, key);
+ }
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ toggleIdentifyLedValue(row) {
+ this.$store
+ .dispatch('memory/updateIdentifyLedValue', {
+ uri: row.uri,
+ identifyLed: row.identifyLed,
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableFans.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableFans.vue
new file mode 100644
index 00000000..fe788c53
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableFans.vue
@@ -0,0 +1,190 @@
+<template>
+ <page-section :section-title="$t('pageInventory.fans')">
+ <b-row class="align-items-end">
+ <b-col sm="6" md="5" xl="4">
+ <search
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ </b-col>
+ <b-col sm="6" md="3" xl="2">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="fans.length"
+ ></table-cell-count>
+ </b-col>
+ </b-row>
+ <b-table
+ sort-icon-left
+ no-sort-reset
+ hover
+ responsive="md"
+ sort-by="health"
+ show-empty
+ :items="fans"
+ :fields="fields"
+ :sort-desc="true"
+ :sort-compare="sortCompare"
+ :filter="searchFilter"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandFans"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col sm="6" xl="4">
+ <dl>
+ <!-- Name -->
+ <dt>{{ $t('pageInventory.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ </dl>
+ <dl>
+ <!-- Serial number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ </dl>
+ <dl>
+ <!-- Part number -->
+ <dt>{{ $t('pageInventory.table.partNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.partNumber) }}</dd>
+ </dl>
+ <dl>
+ <!-- Fan speed -->
+ <dt>{{ $t('pageInventory.table.fanSpeed') }}:</dt>
+ <dd>{{ dataFormatter(item.speed) }}</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <dl>
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ </dl>
+ <dl>
+ <!-- Health Rollup state -->
+ <dt>{{ $t('pageInventory.table.statusHealthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.healthRollup) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+import TableCellCount from '@/components/Global/TableCellCount';
+
+import StatusIcon from '@/components/Global/StatusIcon';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import Search from '@/components/Global/Search';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon, Search, TableCellCount },
+ mixins: [
+ TableRowExpandMixin,
+ DataFormatterMixin,
+ TableSortMixin,
+ SearchFilterMixin,
+ ],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ sortable: false,
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'partNumber',
+ label: this.$t('pageInventory.table.partNumber'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'serialNumber',
+ label: this.$t('pageInventory.table.serialNumber'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.fans.length;
+ },
+ fans() {
+ return this.$store.getters['fan/fans'];
+ },
+ },
+ created() {
+ this.$store.dispatch('fan/getFanInfo').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-fans-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ sortCompare(a, b, key) {
+ if (key === 'health') {
+ return this.sortStatus(a, b, key);
+ }
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue
new file mode 100644
index 00000000..aed7871a
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue
@@ -0,0 +1,208 @@
+<template>
+ <page-section :section-title="$t('pageInventory.powerSupplies')">
+ <b-row class="align-items-end">
+ <b-col sm="6" md="5" xl="4">
+ <search
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ </b-col>
+ <b-col sm="6" md="3" xl="2">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="powerSupplies.length"
+ ></table-cell-count>
+ </b-col>
+ </b-row>
+ <b-table
+ sort-icon-left
+ no-sort-reset
+ hover
+ responsive="md"
+ sort-by="health"
+ show-empty
+ :items="powerSupplies"
+ :fields="fields"
+ :sort-desc="true"
+ :sort-compare="sortCompare"
+ :filter="searchFilter"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandPowerSupplies"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col sm="6" xl="4">
+ <dl>
+ <!-- Name -->
+ <dt>{{ $t('pageInventory.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ <!-- Part number -->
+ <dt>{{ $t('pageInventory.table.partNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.partNumber) }}</dd>
+ <!-- Serial number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ <!-- Spare part number -->
+ <dt>{{ $t('pageInventory.table.sparePartNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.sparePartNumber) }}</dd>
+ <!-- Model -->
+ <dt>{{ $t('pageInventory.table.model') }}:</dt>
+ <dd>{{ dataFormatter(item.model) }}</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <dl>
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ <!-- Status Health rollup state -->
+ <dt>{{ $t('pageInventory.table.statusHealthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.statusHealth) }}</dd>
+ <!-- Efficiency percent -->
+ <dt>{{ $t('pageInventory.table.efficiencyPercent') }}:</dt>
+ <dd>{{ dataFormatter(item.efficiencyPercent) }}</dd>
+ <!-- Power input watts -->
+ <dt>{{ $t('pageInventory.table.powerInputWatts') }}:</dt>
+ <dd>{{ dataFormatter(item.powerInputWatts) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <div class="section-divider mb-3 mt-3"></div>
+ <b-row>
+ <b-col sm="6" xl="4">
+ <dl>
+ <!-- Manufacturer -->
+ <dt>{{ $t('pageInventory.table.manufacturer') }}:</dt>
+ <dd>{{ dataFormatter(item.manufacturer) }}</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <dl>
+ <!-- Firmware version -->
+ <dt>{{ $t('pageInventory.table.firmwareVersion') }}:</dt>
+ <dd>{{ dataFormatter(item.firmwareVersion) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+
+import StatusIcon from '@/components/Global/StatusIcon';
+import TableCellCount from '@/components/Global/TableCellCount';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import Search from '@/components/Global/Search';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon, Search, TableCellCount },
+ mixins: [
+ TableRowExpandMixin,
+ DataFormatterMixin,
+ TableSortMixin,
+ SearchFilterMixin,
+ ],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ sortable: false,
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'identifyLed',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.powerSupplies.length;
+ },
+ powerSupplies() {
+ return this.$store.getters['powerSupply/powerSupplies'];
+ },
+ },
+ created() {
+ this.$store.dispatch('powerSupply/getAllPowerSupplies').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-power-supplies-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ sortCompare(a, b, key) {
+ if (key === 'health') {
+ return this.sortStatus(a, b, key);
+ }
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableProcessors.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableProcessors.vue
new file mode 100644
index 00000000..7d5dd700
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableProcessors.vue
@@ -0,0 +1,251 @@
+<template>
+ <page-section :section-title="$t('pageInventory.processors')">
+ <!-- Search -->
+ <b-row class="align-items-end">
+ <b-col sm="6" md="5" xl="4">
+ <search
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ </b-col>
+ <b-col sm="6" md="3" xl="2">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="processors.length"
+ ></table-cell-count>
+ </b-col>
+ </b-row>
+ <b-table
+ sort-icon-left
+ no-sort-reset
+ hover
+ responsive="md"
+ show-empty
+ :items="processors"
+ :fields="fields"
+ :sort-desc="true"
+ :filter="searchFilter"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ >
+ <!-- Expand button -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandProcessors"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+
+ <!-- Toggle identify LED -->
+ <template #cell(identifyLed)="row">
+ <b-form-checkbox
+ v-if="hasIdentifyLed(row.item.identifyLed)"
+ v-model="row.item.identifyLed"
+ name="switch"
+ switch
+ @change="toggleIdentifyLedValue(row.item)"
+ >
+ <span v-if="row.item.identifyLed">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else> {{ $t('global.status.off') }} </span>
+ </b-form-checkbox>
+ <div v-else>--</div>
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Name -->
+ <dt>{{ $t('pageInventory.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ <!-- Part Number -->
+ <dt>{{ $t('pageInventory.table.partNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.partNumber) }}</dd>
+ <!-- Serial Number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ <!-- Spare Part Number -->
+ <dt>{{ $t('pageInventory.table.sparePartNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.sparePartNumber) }}</dd>
+ <!-- Model -->
+ <dt>{{ $t('pageInventory.table.model') }}:</dt>
+ <dd>{{ dataFormatter(item.model) }}</dd>
+ <!-- Asset Tag -->
+ <dt>{{ $t('pageInventory.table.assetTag') }}:</dt>
+ <dd>{{ dataFormatter(item.assetTag) }}</dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-2" sm="6" xl="6">
+ <dl>
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ <!-- Health Rollup -->
+ <dt>{{ $t('pageInventory.table.healthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.healthRollup) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <div class="section-divider mb-3 mt-3"></div>
+ <b-row>
+ <b-col class="mt-1" sm="6" xl="6">
+ <dl>
+ <!-- Manufacturer -->
+ <dt>{{ $t('pageInventory.table.manufacturer') }}:</dt>
+ <dd>{{ dataFormatter(item.manufacturer) }}</dd>
+ <!-- Processor Type -->
+ <dt>{{ $t('pageInventory.table.processorType') }}:</dt>
+ <dd>{{ dataFormatter(item.processorType) }}</dd>
+ <!-- Processor Architecture -->
+ <dt>{{ $t('pageInventory.table.processorArchitecture') }}:</dt>
+ <dd>{{ dataFormatter(item.processorArchitecture) }}</dd>
+ <!-- Instruction Set -->
+ <dt>{{ $t('pageInventory.table.instructionSet') }}:</dt>
+ <dd>{{ dataFormatter(item.instructionSet) }}</dd>
+ <!-- Version -->
+ <dt>{{ $t('pageInventory.table.version') }}:</dt>
+ <dd>{{ dataFormatter(item.version) }}</dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-1" sm="6" xl="6">
+ <dl>
+ <!-- Min Speed MHz -->
+ <dt>{{ $t('pageInventory.table.minSpeedMHz') }}:</dt>
+ <dd>{{ dataFormatter(item.minSpeedMHz) }}</dd>
+ <!-- Max Speed MHz -->
+ <dt>{{ $t('pageInventory.table.maxSpeedMHz') }}:</dt>
+ <dd>{{ dataFormatter(item.maxSpeedMHz) }}</dd>
+ <!-- Total Cores -->
+ <dt>{{ $t('pageInventory.table.totalCores') }}:</dt>
+ <dd>{{ dataFormatter(item.totalCores) }}</dd>
+ <!-- Total Threads -->
+ <dt>{{ $t('pageInventory.table.totalThreads') }}:</dt>
+ <dd>{{ dataFormatter(item.totalThreads) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+import StatusIcon from '@/components/Global/StatusIcon';
+import TableCellCount from '@/components/Global/TableCellCount';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import Search from '@/components/Global/Search';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon, Search, TableCellCount },
+ mixins: [
+ BVToastMixin,
+ TableRowExpandMixin,
+ DataFormatterMixin,
+ TableSortMixin,
+ SearchFilterMixin,
+ ],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ sortable: false,
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ sortable: true,
+ },
+ {
+ key: 'identifyLed',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ sortable: false,
+ },
+ ],
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.processors.length;
+ },
+ processors() {
+ return this.$store.getters['processors/processors'];
+ },
+ },
+ created() {
+ this.$store.dispatch('processors/getProcessorsInfo').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-processors-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ toggleIdentifyLedValue(row) {
+ this.$store
+ .dispatch('processors/updateIdentifyLedValue', {
+ uri: row.uri,
+ identifyLed: row.identifyLed,
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ // TO DO: remove hasIdentifyLed when the following is merged:
+ // https://gerrit.openbmc-project.xyz/c/openbmc/bmcweb/+/37045
+ hasIdentifyLed(identifyLed) {
+ return typeof identifyLed === 'boolean';
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/InventoryTableSystem.vue b/src/views/_sila/HardwareStatus/Inventory/InventoryTableSystem.vue
new file mode 100644
index 00000000..cf2cf020
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/InventoryTableSystem.vue
@@ -0,0 +1,224 @@
+<template>
+ <page-section :section-title="$t('pageInventory.system')">
+ <b-table
+ responsive="md"
+ hover
+ show-empty
+ :items="systems"
+ :fields="fields"
+ :empty-text="$t('global.table.emptyMessage')"
+ :busy="isBusy"
+ >
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ data-test-id="hardwareStatus-button-expandSystem"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ <span class="sr-only">{{ expandRowLabel }}</span>
+ </b-button>
+ </template>
+
+ <!-- Health -->
+ <template #cell(health)="{ value }">
+ <status-icon :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+
+ <template #cell(locationIndicatorActive)="{ item }">
+ <b-form-checkbox
+ id="identifyLedSwitchSystem"
+ v-model="item.locationIndicatorActive"
+ data-test-id="inventorySystem-toggle-identifyLed"
+ switch
+ @change="toggleIdentifyLedSwitch"
+ >
+ <span v-if="item.locationIndicatorActive">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else>{{ $t('global.status.off') }}</span>
+ </b-form-checkbox>
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col class="mt-2" sm="6">
+ <dl>
+ <!-- Serial number -->
+ <dt>{{ $t('pageInventory.table.serialNumber') }}:</dt>
+ <dd>{{ dataFormatter(item.serialNumber) }}</dd>
+ <!-- Model -->
+ <dt>{{ $t('pageInventory.table.model') }}:</dt>
+ <dd>{{ dataFormatter(item.model) }}</dd>
+ <!-- Asset tag -->
+ <dt>{{ $t('pageInventory.table.assetTag') }}:</dt>
+ <dd class="mb-2">
+ {{ dataFormatter(item.assetTag) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col class="mt-2" sm="6">
+ <dl>
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.statusState) }}</dd>
+ <!-- Power state -->
+ <dt>{{ $t('pageInventory.table.power') }}:</dt>
+ <dd>{{ dataFormatter(item.powerState) }}</dd>
+ <!-- Health rollup -->
+ <dt>{{ $t('pageInventory.table.healthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.healthRollup) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <div class="section-divider mb-3 mt-3"></div>
+ <b-row>
+ <b-col class="mt-1" sm="6">
+ <dl>
+ <!-- Manufacturer -->
+ <dt>{{ $t('pageInventory.table.manufacturer') }}:</dt>
+ <dd>{{ dataFormatter(item.manufacturer) }}</dd>
+ <!-- Description -->
+ <dt>{{ $t('pageInventory.table.description') }}:</dt>
+ <dd>{{ dataFormatter(item.description) }}</dd>
+ <!-- Sub Model -->
+ <dt>{{ $t('pageInventory.table.subModel') }}:</dt>
+ <dd>
+ {{ dataFormatter(item.subModel) }}
+ </dd>
+ <!-- System Type -->
+ <dt>{{ $t('pageInventory.table.systemType') }}:</dt>
+ <dd>
+ {{ dataFormatter(item.systemType) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col sm="6">
+ <!-- Memory Summary -->
+ <p class="mt-1 mb-2 h6 float-none m-0">
+ {{ $t('pageInventory.table.memorySummary') }}
+ </p>
+ <dl class="ml-4">
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.memorySummaryState) }}</dd>
+ <!-- Health -->
+ <dt>{{ $t('pageInventory.table.health') }}:</dt>
+ <dd>{{ dataFormatter(item.memorySummaryHealth) }}</dd>
+ <!-- Health Roll -->
+ <dt>{{ $t('pageInventory.table.healthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.memorySummaryHealthRollup) }}</dd>
+ <!-- Total system memory -->
+ <dt>{{ $t('pageInventory.table.totalSystemMemoryGiB') }}:</dt>
+ <dd>{{ dataFormatter(item.totalSystemMemoryGiB) }}GB</dd>
+ </dl>
+ <!-- Processor Summary -->
+ <p class="mt-1 mb-2 h6 float-none m-0">
+ {{ $t('pageInventory.table.processorSummary') }}
+ </p>
+ <dl class="ml-4">
+ <!-- Status state -->
+ <dt>{{ $t('pageInventory.table.statusState') }}:</dt>
+ <dd>{{ dataFormatter(item.processorSummaryState) }}</dd>
+ <!-- Health -->
+ <dt>{{ $t('pageInventory.table.health') }}:</dt>
+ <dd>{{ dataFormatter(item.processorSummaryHealth) }}</dd>
+ <!-- Health Rollup -->
+ <dt>{{ $t('pageInventory.table.healthRollup') }}:</dt>
+ <dd>{{ dataFormatter(item.processorSummaryHealthRoll) }}</dd>
+ <!-- Count -->
+ <dt>{{ $t('pageInventory.table.count') }}:</dt>
+ <dd>{{ dataFormatter(item.processorSummaryCount) }}</dd>
+ <!-- Core Count -->
+ <dt>{{ $t('pageInventory.table.coreCount') }}:</dt>
+ <dd>{{ dataFormatter(item.processorSummaryCoreCount) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import PageSection from '@/components/Global/PageSection';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+
+import StatusIcon from '@/components/Global/StatusIcon';
+
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ components: { IconChevron, PageSection, StatusIcon },
+ mixins: [BVToastMixin, TableRowExpandMixin, DataFormatterMixin],
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ },
+ {
+ key: 'id',
+ label: this.$t('pageInventory.table.id'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'hardwareType',
+ label: this.$t('pageInventory.table.hardwareType'),
+ formatter: this.dataFormatter,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'health',
+ label: this.$t('pageInventory.table.health'),
+ formatter: this.dataFormatter,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'locationNumber',
+ label: this.$t('pageInventory.table.locationNumber'),
+ formatter: this.dataFormatter,
+ },
+ {
+ key: 'locationIndicatorActive',
+ label: this.$t('pageInventory.table.identifyLed'),
+ formatter: this.dataFormatter,
+ },
+ ],
+ expandRowLabel: expandRowLabel,
+ };
+ },
+ computed: {
+ systems() {
+ return this.$store.getters['system/systems'];
+ },
+ },
+ created() {
+ this.$store.dispatch('system/getSystem').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('hardware-status-system-complete');
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ toggleIdentifyLedSwitch(state) {
+ this.$store
+ .dispatch('system/changeIdentifyLedState', state)
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Inventory/index.js b/src/views/_sila/HardwareStatus/Inventory/index.js
new file mode 100644
index 00000000..c9fde8d2
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Inventory/index.js
@@ -0,0 +1,2 @@
+import Inventory from './Inventory.vue';
+export default Inventory;
diff --git a/src/views/_sila/HardwareStatus/Sensors/Sensors.vue b/src/views/_sila/HardwareStatus/Sensors/Sensors.vue
new file mode 100644
index 00000000..6329d9d8
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Sensors/Sensors.vue
@@ -0,0 +1,256 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row class="align-items-end">
+ <b-col sm="6" md="5" xl="4">
+ <search
+ :placeholder="$t('pageSensors.searchForSensors')"
+ data-test-id="sensors-input-searchForSensors"
+ @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="allSensors.length"
+ ></table-cell-count>
+ </b-col>
+ <b-col sm="3" md="4" xl="6" class="text-right">
+ <table-filter :filters="tableFilters" @filter-change="onFilterChange" />
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="12">
+ <table-toolbar
+ ref="toolbar"
+ :selected-items-count="selectedRows.length"
+ @clear-selected="clearSelectedRows($refs.table)"
+ >
+ <template #toolbar-buttons>
+ <table-toolbar-export
+ :data="selectedRows"
+ :file-name="exportFileNameByDate()"
+ />
+ </template>
+ </table-toolbar>
+ <b-table
+ ref="table"
+ responsive="md"
+ selectable
+ no-select-on-click
+ sort-icon-left
+ hover
+ no-sort-reset
+ sticky-header="75vh"
+ sort-by="status"
+ show-empty
+ :no-border-collapse="true"
+ :items="filteredSensors"
+ :fields="fields"
+ :sort-desc="true"
+ :sort-compare="sortCompare"
+ :filter="searchFilter"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ @row-selected="onRowSelected($event, filteredSensors.length)"
+ >
+ <!-- Checkbox column -->
+ <template #head(checkbox)>
+ <b-form-checkbox
+ v-model="tableHeaderCheckboxModel"
+ :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"
+ @change="toggleSelectRow($refs.table, row.index)"
+ >
+ <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+ </b-form-checkbox>
+ </template>
+
+ <template #cell(status)="{ value }">
+ <status-icon :status="statusIcon(value)" /> {{ value }}
+ </template>
+ <template #cell(currentValue)="data">
+ {{ data.value }} {{ data.item.units }}
+ </template>
+ <template #cell(lowerCaution)="data">
+ {{ data.value }} {{ data.item.units }}
+ </template>
+ <template #cell(upperCaution)="data">
+ {{ data.value }} {{ data.item.units }}
+ </template>
+ <template #cell(lowerCritical)="data">
+ {{ data.value }} {{ data.item.units }}
+ </template>
+ <template #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 Search from '@/components/Global/Search';
+import StatusIcon from '@/components/Global/StatusIcon';
+import TableFilter from '@/components/Global/TableFilter';
+import TableToolbar from '@/components/Global/TableToolbar';
+import TableToolbarExport from '@/components/Global/TableToolbarExport';
+import TableCellCount from '@/components/Global/TableCellCount';
+
+import BVTableSelectableMixin, {
+ selectedRows,
+ tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import TableFilterMixin from '@/components/Mixins/TableFilterMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+
+export default {
+ name: 'Sensors',
+ components: {
+ PageTitle,
+ Search,
+ StatusIcon,
+ TableCellCount,
+ TableFilter,
+ TableToolbar,
+ TableToolbarExport,
+ },
+ mixins: [
+ TableFilterMixin,
+ BVTableSelectableMixin,
+ LoadingBarMixin,
+ DataFormatterMixin,
+ TableSortMixin,
+ SearchFilterMixin,
+ ],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'checkbox',
+ sortable: false,
+ label: '',
+ },
+ {
+ key: 'name',
+ sortable: true,
+ label: this.$t('pageSensors.table.name'),
+ },
+ {
+ key: 'status',
+ sortable: true,
+ label: this.$t('pageSensors.table.status'),
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'lowerCritical',
+ formatter: this.dataFormatter,
+ label: this.$t('pageSensors.table.lowerCritical'),
+ },
+ {
+ key: 'lowerCaution',
+ formatter: this.dataFormatter,
+ label: this.$t('pageSensors.table.lowerWarning'),
+ },
+
+ {
+ key: 'currentValue',
+ formatter: this.dataFormatter,
+ label: this.$t('pageSensors.table.currentValue'),
+ },
+ {
+ key: 'upperCaution',
+ formatter: this.dataFormatter,
+ label: this.$t('pageSensors.table.upperWarning'),
+ },
+ {
+ key: 'upperCritical',
+ formatter: this.dataFormatter,
+ label: this.$t('pageSensors.table.upperCritical'),
+ },
+ ],
+ tableFilters: [
+ {
+ key: 'status',
+ label: this.$t('pageSensors.table.status'),
+ values: ['OK', 'Warning', 'Critical'],
+ },
+ ],
+ activeFilters: [],
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ selectedRows: selectedRows,
+ tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+ };
+ },
+ computed: {
+ allSensors() {
+ return this.$store.getters['sensors/sensors'];
+ },
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.filteredSensors.length;
+ },
+ filteredSensors() {
+ return this.getFilteredTableData(this.allSensors, this.activeFilters);
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store.dispatch('sensors/getAllSensors').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ sortCompare(a, b, key) {
+ if (key === 'status') {
+ return this.sortStatus(a, b, key);
+ }
+ },
+ onFilterChange({ activeFilters }) {
+ this.activeFilters = activeFilters;
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ onChangeSearchInput(event) {
+ this.searchFilter = event;
+ },
+ exportFileNameByDate() {
+ // Create export file name based on date
+ let date = new Date();
+ date =
+ date.toISOString().slice(0, 10) +
+ '_' +
+ date.toString().split(':').join('-').split(' ')[4];
+ return this.$t('pageSensors.exportFilePrefix') + date;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/HardwareStatus/Sensors/index.js b/src/views/_sila/HardwareStatus/Sensors/index.js
new file mode 100644
index 00000000..fc71b611
--- /dev/null
+++ b/src/views/_sila/HardwareStatus/Sensors/index.js
@@ -0,0 +1,2 @@
+import Sensors from './Sensors.vue';
+export default Sensors;
diff --git a/src/views/_sila/Login/Login.vue b/src/views/_sila/Login/Login.vue
new file mode 100644
index 00000000..8d96573a
--- /dev/null
+++ b/src/views/_sila/Login/Login.vue
@@ -0,0 +1,146 @@
+<template>
+ <b-form class="login-form" novalidate @submit.prevent="login">
+ <alert class="login-error mb-4" :show="authError" variant="danger">
+ <p id="login-error-alert">
+ {{ $t('pageLogin.alert.message') }}
+ </p>
+ </alert>
+ <b-form-group label-for="language" :label="$t('pageLogin.language')">
+ <b-form-select
+ id="language"
+ v-model="$i18n.locale"
+ :options="languages"
+ data-test-id="login-select-language"
+ ></b-form-select>
+ </b-form-group>
+ <b-form-group label-for="username" :label="$t('pageLogin.username')">
+ <b-form-input
+ id="username"
+ v-model="userInfo.username"
+ aria-describedby="login-error-alert username-required"
+ :state="getValidationState($v.userInfo.username)"
+ type="text"
+ autofocus="autofocus"
+ data-test-id="login-input-username"
+ @input="$v.userInfo.username.$touch()"
+ >
+ </b-form-input>
+ <b-form-invalid-feedback id="username-required" role="alert">
+ <template v-if="!$v.userInfo.username.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ <div class="login-form__section mb-3">
+ <label for="password">{{ $t('pageLogin.password') }}</label>
+ <input-password-toggle>
+ <b-form-input
+ id="password"
+ v-model="userInfo.password"
+ aria-describedby="login-error-alert password-required"
+ :state="getValidationState($v.userInfo.password)"
+ type="password"
+ data-test-id="login-input-password"
+ class="form-control-with-button"
+ @input="$v.userInfo.password.$touch()"
+ >
+ </b-form-input>
+ </input-password-toggle>
+ <b-form-invalid-feedback id="password-required" role="alert">
+ <template v-if="!$v.userInfo.password.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </div>
+ <b-button
+ class="mt-3"
+ type="submit"
+ variant="primary"
+ data-test-id="login-button-submit"
+ :disabled="disableSubmitButton"
+ >{{ $t('pageLogin.logIn') }}</b-button
+ >
+ </b-form>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import i18n from '@/i18n';
+import Alert from '@/components/Global/Alert';
+import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+
+export default {
+ name: 'Login',
+ components: { Alert, InputPasswordToggle },
+ mixins: [VuelidateMixin],
+ data() {
+ return {
+ userInfo: {
+ username: null,
+ password: null,
+ },
+ disableSubmitButton: false,
+ languages: [
+ {
+ value: 'en-US',
+ text: 'English',
+ },
+ {
+ value: 'es',
+ text: 'Español',
+ },
+ {
+ value: 'ru-RU',
+ text: 'Русский',
+ },
+ ],
+ };
+ },
+ computed: {
+ authError() {
+ return this.$store.getters['authentication/authError'];
+ },
+ },
+ validations: {
+ userInfo: {
+ username: {
+ required,
+ },
+ password: {
+ required,
+ },
+ },
+ },
+ methods: {
+ login: function () {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.disableSubmitButton = true;
+ const username = this.userInfo.username;
+ const password = this.userInfo.password;
+ this.$store
+ .dispatch('authentication/login', { username, password })
+ .then(() => {
+ localStorage.setItem('storedLanguage', i18n.locale);
+ localStorage.setItem('storedUsername', username);
+ this.$store.commit('global/setUsername', username);
+ this.$store.commit('global/setLanguagePreference', i18n.locale);
+ return this.$store.dispatch(
+ 'authentication/checkPasswordChangeRequired',
+ username
+ );
+ })
+ .then((passwordChangeRequired) => {
+ if (passwordChangeRequired) {
+ this.$router.push('/change-password');
+ } else {
+ this.$router.push('/');
+ }
+ })
+ .catch((error) => console.log(error))
+ .finally(() => (this.disableSubmitButton = false));
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Login/index.js b/src/views/_sila/Login/index.js
new file mode 100644
index 00000000..8fe0250d
--- /dev/null
+++ b/src/views/_sila/Login/index.js
@@ -0,0 +1,2 @@
+import Login from './Login.vue';
+export default Login;
diff --git a/src/views/_sila/Logs/Dumps/Dumps.vue b/src/views/_sila/Logs/Dumps/Dumps.vue
new file mode 100644
index 00000000..81c9de04
--- /dev/null
+++ b/src/views/_sila/Logs/Dumps/Dumps.vue
@@ -0,0 +1,404 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row>
+ <b-col sm="6" lg="5" xl="4">
+ <page-section :section-title="$t('pageDumps.initiateDump')">
+ <dumps-form />
+ </page-section>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="10">
+ <page-section :section-title="$t('pageDumps.dumpsAvailableOnBmc')">
+ <b-row class="align-items-start">
+ <b-col sm="8" xl="6" class="d-sm-flex align-items-end">
+ <search
+ :placeholder="$t('pageDumps.table.searchDumps')"
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ <div class="ml-sm-4">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="allDumps.length"
+ ></table-cell-count>
+ </div>
+ </b-col>
+ <b-col sm="8" md="7" xl="6">
+ <table-date-filter @change="onChangeDateTimeFilter" />
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col class="text-right">
+ <table-filter
+ :filters="tableFilters"
+ @filter-change="onFilterChange"
+ />
+ </b-col>
+ </b-row>
+ <table-toolbar
+ :selected-items-count="selectedRows.length"
+ :actions="batchActions"
+ @clear-selected="clearSelectedRows($refs.table)"
+ @batch-action="onTableBatchAction"
+ />
+ <b-table
+ ref="table"
+ show-empty
+ hover
+ sort-icon-left
+ no-sort-reset
+ sort-desc
+ selectable
+ no-select-on-click
+ responsive="md"
+ sort-by="dateTime"
+ :fields="fields"
+ :items="filteredDumps"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :filter="searchFilter"
+ :busy="isBusy"
+ @filtered="onChangeSearchFilter"
+ @row-selected="onRowSelected($event, filteredTableItems.length)"
+ >
+ <!-- Checkbox column -->
+ <template #head(checkbox)>
+ <b-form-checkbox
+ v-model="tableHeaderCheckboxModel"
+ :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"
+ @change="toggleSelectRow($refs.table, row.index)"
+ >
+ <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+ </b-form-checkbox>
+ </template>
+
+ <!-- Date and Time column -->
+ <template #cell(dateTime)="{ value }">
+ <p class="mb-0">{{ value | formatDate }}</p>
+ <p class="mb-0">{{ value | formatTime }}</p>
+ </template>
+
+ <!-- Size column -->
+ <template #cell(size)="{ value }">
+ {{ convertBytesToMegabytes(value) }} MB
+ </template>
+
+ <!-- Actions column -->
+ <template #cell(actions)="row">
+ <table-row-action
+ v-for="(action, index) in row.item.actions"
+ :key="index"
+ :value="action.value"
+ :title="action.title"
+ :download-location="row.item.data"
+ :export-name="exportFileName(row)"
+ @click-table-action="onTableRowAction($event, row.item)"
+ >
+ <template #icon>
+ <icon-download v-if="action.value === 'download'" />
+ <icon-delete v-if="action.value === 'delete'" />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </page-section>
+ </b-col>
+ </b-row>
+ <!-- Table pagination -->
+ <b-row>
+ <b-col sm="6" xl="5">
+ <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" xl="5">
+ <b-pagination
+ v-model="currentPage"
+ first-number
+ last-number
+ :per-page="perPage"
+ :total-rows="getTotalRowCount()"
+ aria-controls="table-dump-entries"
+ />
+ </b-col>
+ </b-row>
+ </b-container>
+</template>
+
+<script>
+import IconDelete from '@carbon/icons-vue/es/trash-can/20';
+import IconDownload from '@carbon/icons-vue/es/download/20';
+import DumpsForm from './DumpsForm';
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+import Search from '@/components/Global/Search';
+import TableCellCount from '@/components/Global/TableCellCount';
+import TableDateFilter from '@/components/Global/TableDateFilter';
+import TableRowAction from '@/components/Global/TableRowAction';
+import TableToolbar from '@/components/Global/TableToolbar';
+import BVTableSelectableMixin, {
+ selectedRows,
+ tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import BVPaginationMixin, {
+ currentPage,
+ perPage,
+ itemsPerPageOptions,
+} from '@/components/Mixins/BVPaginationMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+import TableFilter from '@/components/Global/TableFilter';
+import TableFilterMixin from '@/components/Mixins/TableFilterMixin';
+
+export default {
+ components: {
+ DumpsForm,
+ IconDelete,
+ IconDownload,
+ PageSection,
+ PageTitle,
+ Search,
+ TableCellCount,
+ TableDateFilter,
+ TableRowAction,
+ TableToolbar,
+ TableFilter,
+ },
+ mixins: [
+ BVTableSelectableMixin,
+ BVToastMixin,
+ BVPaginationMixin,
+ LoadingBarMixin,
+ SearchFilterMixin,
+ TableFilterMixin,
+ ],
+ beforeRouteLeave(to, from, next) {
+ // Hide loader if the user navigates to another page
+ // before request is fulfilled.
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'checkbox',
+ sortable: false,
+ },
+ {
+ key: 'dateTime',
+ label: this.$t('pageDumps.table.dateAndTime'),
+ sortable: true,
+ },
+ {
+ key: 'dumpType',
+ label: this.$t('pageDumps.table.dumpType'),
+ sortable: true,
+ },
+ {
+ key: 'id',
+ label: this.$t('pageDumps.table.id'),
+ sortable: true,
+ },
+ {
+ key: 'size',
+ label: this.$t('pageDumps.table.size'),
+ sortable: true,
+ },
+ {
+ key: 'actions',
+ sortable: false,
+ label: '',
+ tdClass: 'text-right text-nowrap',
+ },
+ ],
+ batchActions: [
+ {
+ value: 'delete',
+ label: this.$t('global.action.delete'),
+ },
+ ],
+ tableFilters: [
+ {
+ key: 'dumpType',
+ label: this.$t('pageDumps.table.dumpType'),
+ values: [
+ 'BMC Dump Entry',
+ 'Hostboot Dump Entry',
+ 'Resource Dump Entry',
+ 'System Dump Entry',
+ ],
+ },
+ ],
+ activeFilters: [],
+ currentPage: currentPage,
+ filterEndDate: null,
+ filterStartDate: null,
+ itemsPerPageOptions: itemsPerPageOptions,
+ perPage: perPage,
+ searchFilter,
+ searchTotalFilteredRows: 0,
+ selectedRows,
+ tableHeaderCheckboxIndeterminate,
+ tableHeaderCheckboxModel,
+ };
+ },
+ computed: {
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.filteredDumps.length;
+ },
+ allDumps() {
+ return this.$store.getters['dumps/allDumps'].map((item) => {
+ return {
+ ...item,
+ actions: [
+ {
+ value: 'download',
+ title: this.$t('global.action.download'),
+ },
+ {
+ value: 'delete',
+ title: this.$t('global.action.delete'),
+ },
+ ],
+ };
+ });
+ },
+ filteredDumpsByDate() {
+ return this.getFilteredTableDataByDate(
+ this.allDumps,
+ this.filterStartDate,
+ this.filterEndDate,
+ 'dateTime'
+ );
+ },
+ filteredDumps() {
+ return this.getFilteredTableData(
+ this.filteredDumpsByDate,
+ this.activeFilters
+ );
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store.dispatch('dumps/getBmcDumpEntries').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ convertBytesToMegabytes(bytes) {
+ return parseFloat((bytes / 1000000).toFixed(3));
+ },
+ onFilterChange({ activeFilters }) {
+ this.activeFilters = activeFilters;
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ onChangeDateTimeFilter({ fromDate, toDate }) {
+ this.filterStartDate = fromDate;
+ this.filterEndDate = toDate;
+ },
+ onTableRowAction(action, dump) {
+ if (action === 'delete') {
+ this.$bvModal
+ .msgBoxConfirm(this.$tc('pageDumps.modal.deleteDumpConfirmation'), {
+ title: this.$tc('pageDumps.modal.deleteDump'),
+ okTitle: this.$tc('pageDumps.modal.deleteDump'),
+ cancelTitle: this.$t('global.action.cancel'),
+ })
+ .then((deleteConfrimed) => {
+ if (deleteConfrimed) {
+ this.$store
+ .dispatch('dumps/deleteDumps', [dump])
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') {
+ this.successToast(message);
+ } else if (type === 'error') {
+ this.errorToast(message);
+ }
+ });
+ });
+ }
+ });
+ }
+ },
+ onTableBatchAction(action) {
+ if (action === 'delete') {
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$tc(
+ 'pageDumps.modal.deleteDumpConfirmation',
+ this.selectedRows.length
+ ),
+ {
+ title: this.$tc(
+ 'pageDumps.modal.deleteDump',
+ this.selectedRows.length
+ ),
+ okTitle: this.$tc(
+ 'pageDumps.modal.deleteDump',
+ this.selectedRows.length
+ ),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfrimed) => {
+ if (deleteConfrimed) {
+ if (this.selectedRows.length === this.dumps.length) {
+ this.$store
+ .dispatch('dumps/deleteAllDumps')
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message));
+ } else {
+ this.$store
+ .dispatch('dumps/deleteDumps', this.selectedRows)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') {
+ this.successToast(message);
+ } else if (type === 'error') {
+ this.errorToast(message);
+ }
+ });
+ });
+ }
+ }
+ });
+ }
+ },
+ exportFileName(row) {
+ let filename = row.item.dumpType + '_' + row.item.id + '.tar.xz';
+ filename = filename.replace(RegExp(' ', 'g'), '_');
+ return filename;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Logs/Dumps/DumpsForm.vue b/src/views/_sila/Logs/Dumps/DumpsForm.vue
new file mode 100644
index 00000000..07f4a060
--- /dev/null
+++ b/src/views/_sila/Logs/Dumps/DumpsForm.vue
@@ -0,0 +1,97 @@
+<template>
+ <div class="form-background p-3">
+ <b-form id="form-new-dump" novalidate @submit.prevent="handleSubmit">
+ <b-form-group
+ :label="$t('pageDumps.form.selectDumpType')"
+ label-for="selectDumpType"
+ >
+ <b-form-select
+ id="selectDumpType"
+ v-model="selectedDumpType"
+ :options="dumpTypeOptions"
+ :state="getValidationState($v.selectedDumpType)"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.required') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ <alert variant="info" class="mb-3" :show="selectedDumpType === 'system'">
+ {{ $t('pageDumps.form.systemDumpInfo') }}
+ </alert>
+ <b-button variant="primary" type="submit" form="form-new-dump">
+ {{ $t('pageDumps.form.initiateDump') }}
+ </b-button>
+ </b-form>
+ <modal-confirmation @ok="createSystemDump" />
+ </div>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators';
+import ModalConfirmation from './DumpsModalConfirmation';
+import Alert from '@/components/Global/Alert';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+ components: { Alert, ModalConfirmation },
+ mixins: [BVToastMixin, VuelidateMixin],
+ data() {
+ return {
+ selectedDumpType: null,
+ dumpTypeOptions: [
+ { value: 'bmc', text: this.$t('pageDumps.form.bmcDump') },
+ { value: 'system', text: this.$t('pageDumps.form.systemDump') },
+ ],
+ };
+ },
+ validations() {
+ return {
+ selectedDumpType: { required },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+
+ // System dump initiation
+ if (this.selectedDumpType === 'system') {
+ this.showConfirmationModal();
+ }
+ // BMC dump initiation
+ else if (this.selectedDumpType === 'bmc') {
+ this.$store
+ .dispatch('dumps/createBmcDump')
+ .then(() =>
+ this.infoToast(this.$t('pageDumps.toast.successStartBmcDump'), {
+ title: this.$t('pageDumps.toast.successStartBmcDumpTitle'),
+ timestamp: true,
+ })
+ )
+ .catch(({ message }) => this.errorToast(message));
+ }
+ },
+ showConfirmationModal() {
+ this.$bvModal.show('modal-confirmation');
+ },
+ createSystemDump() {
+ this.$store
+ .dispatch('dumps/createSystemDump')
+ .then(() =>
+ this.infoToast(this.$t('pageDumps.toast.successStartSystemDump'), {
+ title: this.$t('pageDumps.toast.successStartSystemDumpTitle'),
+ timestamp: true,
+ })
+ )
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Logs/Dumps/DumpsModalConfirmation.vue b/src/views/_sila/Logs/Dumps/DumpsModalConfirmation.vue
new file mode 100644
index 00000000..f8e20cfd
--- /dev/null
+++ b/src/views/_sila/Logs/Dumps/DumpsModalConfirmation.vue
@@ -0,0 +1,75 @@
+<template>
+ <b-modal
+ id="modal-confirmation"
+ ref="modal"
+ :title="$t('pageDumps.modal.initiateSystemDump')"
+ @hidden="resetForm"
+ >
+ <p>
+ <strong>
+ {{ $t('pageDumps.modal.initiateSystemDumpMessage1') }}
+ </strong>
+ </p>
+ <p>
+ {{ $t('pageDumps.modal.initiateSystemDumpMessage2') }}
+ </p>
+ <p>
+ <status-icon status="danger" />
+ {{ $t('pageDumps.modal.initiateSystemDumpMessage3') }}
+ </p>
+ <b-form-checkbox v-model="confirmed" @input="$v.confirmed.$touch()">
+ {{ $t('pageDumps.modal.initiateSystemDumpMessage4') }}
+ </b-form-checkbox>
+ <b-form-invalid-feedback
+ :state="getValidationState($v.confirmed)"
+ role="alert"
+ >
+ {{ $t('global.form.required') }}
+ </b-form-invalid-feedback>
+ <template #modal-footer="{ cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button variant="danger" @click="handleSubmit">
+ {{ $t('pageDumps.form.initiateDump') }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import StatusIcon from '@/components/Global/StatusIcon';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+ components: { StatusIcon },
+ mixins: [VuelidateMixin],
+ data() {
+ return {
+ confirmed: false,
+ };
+ },
+ validations: {
+ confirmed: {
+ mustBeTrue: (value) => value === true,
+ },
+ },
+ methods: {
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok');
+ this.closeModal();
+ },
+ resetForm() {
+ this.confirmed = false;
+ this.$v.$reset();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Logs/Dumps/index.js b/src/views/_sila/Logs/Dumps/index.js
new file mode 100644
index 00000000..65525fb0
--- /dev/null
+++ b/src/views/_sila/Logs/Dumps/index.js
@@ -0,0 +1,2 @@
+import Dumps from './Dumps.vue';
+export default Dumps;
diff --git a/src/views/_sila/Logs/EventLogs/EventLogs.vue b/src/views/_sila/Logs/EventLogs/EventLogs.vue
new file mode 100644
index 00000000..5b8ca110
--- /dev/null
+++ b/src/views/_sila/Logs/EventLogs/EventLogs.vue
@@ -0,0 +1,604 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row class="align-items-start">
+ <b-col sm="8" xl="6" class="d-sm-flex align-items-end mb-4">
+ <search
+ :placeholder="$t('pageEventLogs.table.searchLogs')"
+ data-test-id="eventLogs-input-searchLogs"
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ <div class="ml-sm-4">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="allLogs.length"
+ ></table-cell-count>
+ </div>
+ </b-col>
+ <b-col sm="8" md="7" xl="6">
+ <table-date-filter @change="onChangeDateTimeFilter" />
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col class="text-right">
+ <table-filter :filters="tableFilters" @filter-change="onFilterChange" />
+ <b-button
+ variant="link"
+ :disabled="allLogs.length === 0"
+ @click="deleteAllLogs"
+ >
+ <icon-delete /> {{ $t('global.action.deleteAll') }}
+ </b-button>
+ <b-button
+ variant="primary"
+ :class="{ disabled: allLogs.length === 0 }"
+ :download="exportFileNameByDate()"
+ :href="href"
+ >
+ <icon-export /> {{ $t('global.action.exportAll') }}
+ </b-button>
+ </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"
+ >
+ <template #toolbar-buttons>
+ <b-button variant="primary" @click="resolveLogs">
+ {{ $t('pageEventLogs.resolve') }}
+ </b-button>
+ <b-button variant="primary" @click="unresolveLogs">
+ {{ $t('pageEventLogs.unresolve') }}
+ </b-button>
+ <table-toolbar-export
+ :data="batchExportData"
+ :file-name="exportFileNameByDate()"
+ />
+ </template>
+ </table-toolbar>
+ <b-table
+ id="table-event-logs"
+ ref="table"
+ responsive="md"
+ selectable
+ no-select-on-click
+ sort-icon-left
+ hover
+ no-sort-reset
+ sort-desc
+ show-empty
+ sort-by="id"
+ :fields="fields"
+ :items="filteredLogs"
+ :sort-compare="onSortCompare"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :per-page="perPage"
+ :current-page="currentPage"
+ :filter="searchFilter"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ @row-selected="onRowSelected($event, filteredLogs.length)"
+ >
+ <!-- Checkbox column -->
+ <template #head(checkbox)>
+ <b-form-checkbox
+ v-model="tableHeaderCheckboxModel"
+ data-test-id="eventLogs-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="`eventLogs-checkbox-selectRow-${row.index}`"
+ @change="toggleSelectRow($refs.table, row.index)"
+ >
+ <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+ </b-form-checkbox>
+ </template>
+
+ <!-- Expand chevron icon -->
+ <template #cell(expandRow)="row">
+ <b-button
+ variant="link"
+ :aria-label="expandRowLabel"
+ :title="expandRowLabel"
+ class="btn-icon-only"
+ @click="toggleRowDetails(row)"
+ >
+ <icon-chevron />
+ </b-button>
+ </template>
+
+ <template #row-details="{ item }">
+ <b-container fluid>
+ <b-row>
+ <b-col>
+ <dl>
+ <!-- Name -->
+ <dt>{{ $t('pageEventLogs.table.name') }}:</dt>
+ <dd>{{ dataFormatter(item.name) }}</dd>
+ </dl>
+ <dl>
+ <!-- Type -->
+ <dt>{{ $t('pageEventLogs.table.type') }}:</dt>
+ <dd>{{ dataFormatter(item.type) }}</dd>
+ </dl>
+ </b-col>
+ <b-col>
+ <dl>
+ <!-- Modified date -->
+ <dt>{{ $t('pageEventLogs.table.modifiedDate') }}:</dt>
+ <dd v-if="item.modifiedDate">
+ {{ item.modifiedDate | formatDate }}
+ {{ item.modifiedDate | formatTime }}
+ </dd>
+ <dd v-else>--</dd>
+ </dl>
+ </b-col>
+ <b-col class="text-nowrap">
+ <b-button
+ class="btn btn-secondary float-right"
+ :href="item.additionalDataUri"
+ target="_blank"
+ >
+ <icon-download />{{ $t('pageEventLogs.additionalDataUri') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ </b-container>
+ </template>
+
+ <!-- Severity column -->
+ <template #cell(severity)="{ value }">
+ <status-icon v-if="value" :status="statusIcon(value)" />
+ {{ value }}
+ </template>
+ <!-- Date column -->
+ <template #cell(date)="{ value }">
+ <p class="mb-0">{{ value | formatDate }}</p>
+ <p class="mb-0">{{ value | formatTime }}</p>
+ </template>
+
+ <!-- Status column -->
+ <template #cell(status)="row">
+ <b-form-checkbox
+ v-model="row.item.status"
+ name="switch"
+ switch
+ @change="changelogStatus(row.item)"
+ >
+ <span v-if="row.item.status">
+ {{ $t('pageEventLogs.resolved') }}
+ </span>
+ <span v-else> {{ $t('pageEventLogs.unresolved') }} </span>
+ </b-form-checkbox>
+ </template>
+ <template #cell(filterByStatus)="{ value }">
+ {{ value }}
+ </template>
+
+ <!-- Actions column -->
+ <template #cell(actions)="row">
+ <table-row-action
+ v-for="(action, index) in row.item.actions"
+ :key="index"
+ :value="action.value"
+ :title="action.title"
+ :row-data="row.item"
+ :export-name="exportFileNameByDate('export')"
+ :data-test-id="`eventLogs-button-deleteRow-${row.index}`"
+ @click-table-action="onTableRowAction($event, row.item)"
+ >
+ <template #icon>
+ <icon-export v-if="action.value === 'export'" />
+ <icon-trashcan v-if="action.value === 'delete'" />
+ </template>
+ </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(filteredRows)"
+ aria-controls="table-event-logs"
+ />
+ </b-col>
+ </b-row>
+ </b-container>
+</template>
+
+<script>
+import IconDelete from '@carbon/icons-vue/es/trash-can/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import IconExport from '@carbon/icons-vue/es/document--export/20';
+import IconChevron from '@carbon/icons-vue/es/chevron--down/20';
+import IconDownload from '@carbon/icons-vue/es/download/20';
+import { omit } from 'lodash';
+
+import PageTitle from '@/components/Global/PageTitle';
+import StatusIcon from '@/components/Global/StatusIcon';
+import Search from '@/components/Global/Search';
+import TableCellCount from '@/components/Global/TableCellCount';
+import TableDateFilter from '@/components/Global/TableDateFilter';
+import TableFilter from '@/components/Global/TableFilter';
+import TableRowAction from '@/components/Global/TableRowAction';
+import TableToolbar from '@/components/Global/TableToolbar';
+import TableToolbarExport from '@/components/Global/TableToolbarExport';
+
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import TableFilterMixin from '@/components/Mixins/TableFilterMixin';
+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 DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+
+export default {
+ components: {
+ IconDelete,
+ IconExport,
+ IconTrashcan,
+ IconChevron,
+ IconDownload,
+ PageTitle,
+ Search,
+ StatusIcon,
+ TableCellCount,
+ TableFilter,
+ TableRowAction,
+ TableToolbar,
+ TableToolbarExport,
+ TableDateFilter,
+ },
+ mixins: [
+ BVPaginationMixin,
+ BVTableSelectableMixin,
+ BVToastMixin,
+ LoadingBarMixin,
+ TableFilterMixin,
+ DataFormatterMixin,
+ TableSortMixin,
+ TableRowExpandMixin,
+ SearchFilterMixin,
+ ],
+ beforeRouteLeave(to, from, next) {
+ // Hide loader if the user navigates to another page
+ // before request is fulfilled.
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'expandRow',
+ label: '',
+ tdClass: 'table-row-expand',
+ },
+ {
+ key: 'checkbox',
+ sortable: false,
+ },
+ {
+ key: 'id',
+ label: this.$t('pageEventLogs.table.id'),
+ sortable: true,
+ },
+ {
+ key: 'severity',
+ label: this.$t('pageEventLogs.table.severity'),
+ sortable: true,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'date',
+ label: this.$t('pageEventLogs.table.date'),
+ sortable: true,
+ tdClass: 'text-nowrap',
+ },
+ {
+ key: 'description',
+ label: this.$t('pageEventLogs.table.description'),
+ tdClass: 'text-break',
+ },
+ {
+ key: 'status',
+ label: this.$t('pageEventLogs.table.status'),
+ },
+ {
+ key: 'actions',
+ sortable: false,
+ label: '',
+ tdClass: 'text-right text-nowrap',
+ },
+ ],
+ tableFilters: [
+ {
+ key: 'severity',
+ label: this.$t('pageEventLogs.table.severity'),
+ values: ['OK', 'Warning', 'Critical'],
+ },
+ {
+ key: 'filterByStatus',
+ label: this.$t('pageEventLogs.table.status'),
+ values: ['Resolved', 'Unresolved'],
+ },
+ ],
+ expandRowLabel,
+ activeFilters: [],
+ batchActions: [
+ {
+ value: 'delete',
+ label: this.$t('global.action.delete'),
+ },
+ ],
+ currentPage: currentPage,
+ filterStartDate: null,
+ filterEndDate: null,
+ itemsPerPageOptions: itemsPerPageOptions,
+ perPage: perPage,
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ selectedRows: selectedRows,
+ tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+ };
+ },
+ computed: {
+ href() {
+ return `data:text/json;charset=utf-8,${this.exportAllLogs()}`;
+ },
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.filteredLogs.length;
+ },
+ allLogs() {
+ return this.$store.getters['eventLog/allEvents'].map((event) => {
+ return {
+ ...event,
+ actions: [
+ {
+ value: 'export',
+ title: this.$t('global.action.export'),
+ },
+ {
+ value: 'delete',
+ title: this.$t('global.action.delete'),
+ },
+ ],
+ };
+ });
+ },
+ batchExportData() {
+ return this.selectedRows.map((row) => omit(row, 'actions'));
+ },
+ filteredLogsByDate() {
+ return this.getFilteredTableDataByDate(
+ this.allLogs,
+ this.filterStartDate,
+ this.filterEndDate
+ );
+ },
+ filteredLogs() {
+ return this.getFilteredTableData(
+ this.filteredLogsByDate,
+ this.activeFilters
+ );
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store.dispatch('eventLog/getEventLogData').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ changelogStatus(row) {
+ this.$store
+ .dispatch('eventLog/updateEventLogStatus', {
+ uri: row.uri,
+ status: row.status,
+ })
+ .then((success) => {
+ this.successToast(success);
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ deleteAllLogs() {
+ this.$bvModal
+ .msgBoxConfirm(this.$t('pageEventLogs.modal.deleteAllMessage'), {
+ title: this.$t('pageEventLogs.modal.deleteAllTitle'),
+ okTitle: this.$t('global.action.delete'),
+ okVariant: 'danger',
+ cancelTitle: this.$t('global.action.cancel'),
+ })
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) {
+ this.$store
+ .dispatch('eventLog/deleteAllEventLogs', this.allLogs)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ }
+ });
+ },
+ deleteLogs(uris) {
+ this.$store
+ .dispatch('eventLog/deleteEventLogs', uris)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') {
+ this.successToast(message);
+ } else if (type === 'error') {
+ this.errorToast(message);
+ }
+ });
+ });
+ },
+ exportAllLogs() {
+ {
+ return this.$store.getters['eventLog/allEvents'].map((eventLogs) => {
+ const allEventLogsString = JSON.stringify(eventLogs);
+ return allEventLogsString;
+ });
+ }
+ },
+ onFilterChange({ activeFilters }) {
+ this.activeFilters = activeFilters;
+ },
+ onSortCompare(a, b, key) {
+ if (key === 'severity') {
+ return this.sortStatus(a, b, key);
+ }
+ },
+ onTableRowAction(action, { uri }) {
+ if (action === 'delete') {
+ this.$bvModal
+ .msgBoxConfirm(this.$tc('pageEventLogs.modal.deleteMessage'), {
+ title: this.$tc('pageEventLogs.modal.deleteTitle'),
+ okTitle: this.$t('global.action.delete'),
+ cancelTitle: this.$t('global.action.cancel'),
+ })
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) this.deleteLogs([uri]);
+ });
+ }
+ },
+ onBatchAction(action) {
+ if (action === 'delete') {
+ const uris = this.selectedRows.map((row) => row.uri);
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$tc(
+ 'pageEventLogs.modal.deleteMessage',
+ this.selectedRows.length
+ ),
+ {
+ title: this.$tc(
+ 'pageEventLogs.modal.deleteTitle',
+ this.selectedRows.length
+ ),
+ okTitle: this.$t('global.action.delete'),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) {
+ if (this.selectedRows.length === this.allLogs.length) {
+ this.$store
+ .dispatch(
+ 'eventLog/deleteAllEventLogs',
+ this.selectedRows.length
+ )
+ .then(() => {
+ this.successToast(
+ this.$tc('pageEventLogs.toast.successDelete', uris.length)
+ );
+ })
+ .catch(({ message }) => this.errorToast(message));
+ } else {
+ this.deleteLogs(uris);
+ }
+ }
+ });
+ }
+ },
+ onChangeDateTimeFilter({ fromDate, toDate }) {
+ this.filterStartDate = fromDate;
+ this.filterEndDate = toDate;
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ // Create export file name based on date
+ exportFileNameByDate(value) {
+ let date = new Date();
+ date =
+ date.toISOString().slice(0, 10) +
+ '_' +
+ date.toString().split(':').join('-').split(' ')[4];
+ let fileName;
+ if (value === 'export') {
+ fileName = 'event_log_';
+ } else {
+ fileName = 'all_event_logs_';
+ }
+ return fileName + date;
+ },
+ resolveLogs() {
+ this.$store
+ .dispatch('eventLog/resolveEventLogs', this.selectedRows)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') {
+ this.successToast(message);
+ } else if (type === 'error') {
+ this.errorToast(message);
+ }
+ });
+ });
+ },
+ unresolveLogs() {
+ this.$store
+ .dispatch('eventLog/unresolveEventLogs', this.selectedRows)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') {
+ this.successToast(message);
+ } else if (type === 'error') {
+ this.errorToast(message);
+ }
+ });
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Logs/EventLogs/index.js b/src/views/_sila/Logs/EventLogs/index.js
new file mode 100644
index 00000000..521efde4
--- /dev/null
+++ b/src/views/_sila/Logs/EventLogs/index.js
@@ -0,0 +1,2 @@
+import EventLogs from './EventLogs.vue';
+export default EventLogs;
diff --git a/src/views/_sila/Logs/PostCodeLogs/PostCodeLogs.vue b/src/views/_sila/Logs/PostCodeLogs/PostCodeLogs.vue
new file mode 100644
index 00000000..d116d2ed
--- /dev/null
+++ b/src/views/_sila/Logs/PostCodeLogs/PostCodeLogs.vue
@@ -0,0 +1,347 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row class="align-items-start">
+ <b-col sm="8" xl="6" class="d-sm-flex align-items-end mb-4">
+ <search
+ :placeholder="$t('pagePostCodeLogs.table.searchLogs')"
+ @change-search="onChangeSearchInput"
+ @clear-search="onClearSearchInput"
+ />
+ <div class="ml-sm-4">
+ <table-cell-count
+ :filtered-items-count="filteredRows"
+ :total-number-of-cells="allLogs.length"
+ ></table-cell-count>
+ </div>
+ </b-col>
+ <b-col sm="8" md="7" xl="6">
+ <table-date-filter @change="onChangeDateTimeFilter" />
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="12" class="text-right">
+ <b-button
+ variant="primary"
+ :disabled="allLogs.length === 0"
+ :download="exportFileNameByDate()"
+ :href="href"
+ >
+ <icon-export /> {{ $t('pagePostCodeLogs.button.exportAll') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col>
+ <table-toolbar
+ ref="toolbar"
+ :selected-items-count="selectedRows.length"
+ @clear-selected="clearSelectedRows($refs.table)"
+ >
+ <template #toolbar-buttons>
+ <table-toolbar-export
+ :data="batchExportData"
+ :file-name="exportFileNameByDate()"
+ />
+ </template>
+ </table-toolbar>
+ <b-table
+ id="table-post-code-logs"
+ ref="table"
+ responsive="md"
+ selectable
+ no-select-on-click
+ sort-icon-left
+ hover
+ no-sort-reset
+ sort-desc
+ show-empty
+ sort-by="id"
+ :fields="fields"
+ :items="filteredLogs"
+ :empty-text="$t('global.table.emptyMessage')"
+ :empty-filtered-text="$t('global.table.emptySearchMessage')"
+ :per-page="perPage"
+ :current-page="currentPage"
+ :filter="searchFilter"
+ :busy="isBusy"
+ @filtered="onFiltered"
+ @row-selected="onRowSelected($event, filteredLogs.length)"
+ >
+ <!-- Checkbox column -->
+ <template #head(checkbox)>
+ <b-form-checkbox
+ v-model="tableHeaderCheckboxModel"
+ data-test-id="postCode-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="`postCode-checkbox-selectRow-${row.index}`"
+ @change="toggleSelectRow($refs.table, row.index)"
+ >
+ <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+ </b-form-checkbox>
+ </template>
+ <!-- Date column -->
+ <template #cell(date)="{ value }">
+ <p class="mb-0">{{ value | formatDate }}</p>
+ <p class="mb-0">{{ value | formatTime }}</p>
+ </template>
+
+ <!-- Actions column -->
+ <template #cell(actions)="row">
+ <table-row-action
+ v-for="(action, index) in row.item.actions"
+ :key="index"
+ :value="action.value"
+ :title="action.title"
+ :row-data="row.item"
+ :btn-icon-only="true"
+ :export-name="exportFileNameByDate(action.value)"
+ :download-location="row.item.uri"
+ :download-in-new-tab="true"
+ :show-button="false"
+ >
+ <template #icon>
+ <icon-export v-if="action.value === 'export'" />
+ <icon-download v-if="action.value === 'download'" />
+ </template>
+ </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(filteredRows)"
+ aria-controls="table-post-code-logs"
+ />
+ </b-col>
+ </b-row>
+ </b-container>
+</template>
+
+<script>
+import IconDownload from '@carbon/icons-vue/es/download/20';
+import IconExport from '@carbon/icons-vue/es/document--export/20';
+import { omit } from 'lodash';
+import PageTitle from '@/components/Global/PageTitle';
+import Search from '@/components/Global/Search';
+import TableCellCount from '@/components/Global/TableCellCount';
+import TableDateFilter from '@/components/Global/TableDateFilter';
+import TableRowAction from '@/components/Global/TableRowAction';
+import TableToolbar from '@/components/Global/TableToolbar';
+import TableToolbarExport from '@/components/Global/TableToolbarExport';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import TableFilterMixin from '@/components/Mixins/TableFilterMixin';
+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 TableSortMixin from '@/components/Mixins/TableSortMixin';
+import TableRowExpandMixin, {
+ expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import SearchFilterMixin, {
+ searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+
+export default {
+ components: {
+ IconExport,
+ IconDownload,
+ PageTitle,
+ Search,
+ TableCellCount,
+ TableRowAction,
+ TableToolbar,
+ TableToolbarExport,
+ TableDateFilter,
+ },
+ mixins: [
+ BVPaginationMixin,
+ BVTableSelectableMixin,
+ BVToastMixin,
+ LoadingBarMixin,
+ TableFilterMixin,
+ TableSortMixin,
+ TableRowExpandMixin,
+ SearchFilterMixin,
+ ],
+ beforeRouteLeave(to, from, next) {
+ // Hide loader if the user navigates to another page
+ // before request is fulfilled.
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ isBusy: true,
+ fields: [
+ {
+ key: 'checkbox',
+ sortable: false,
+ },
+ {
+ key: 'date',
+ label: this.$t('pagePostCodeLogs.table.created'),
+ sortable: true,
+ },
+ {
+ key: 'timeStampOffset',
+ label: this.$t('pagePostCodeLogs.table.timeStampOffset'),
+ },
+ {
+ key: 'bootCount',
+ label: this.$t('pagePostCodeLogs.table.bootCount'),
+ },
+ {
+ key: 'postCode',
+ label: this.$t('pagePostCodeLogs.table.postCode'),
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'text-right text-nowrap',
+ },
+ ],
+ expandRowLabel,
+ activeFilters: [],
+ currentPage: currentPage,
+ filterStartDate: null,
+ filterEndDate: null,
+ itemsPerPageOptions: itemsPerPageOptions,
+ perPage: perPage,
+ searchFilter: searchFilter,
+ searchTotalFilteredRows: 0,
+ selectedRows: selectedRows,
+ tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+ };
+ },
+ computed: {
+ href() {
+ return `data:text/json;charset=utf-8,${this.exportAllLogsString()}`;
+ },
+ filteredRows() {
+ return this.searchFilter
+ ? this.searchTotalFilteredRows
+ : this.filteredLogs.length;
+ },
+ allLogs() {
+ return this.$store.getters['postCodeLogs/allPostCodes'].map(
+ (postCodes) => {
+ return {
+ ...postCodes,
+ actions: [
+ {
+ value: 'export',
+ title: this.$t('pagePostCodeLogs.action.exportLogs'),
+ },
+ {
+ value: 'download',
+ title: this.$t('pagePostCodeLogs.action.downloadDetails'),
+ },
+ ],
+ };
+ }
+ );
+ },
+ batchExportData() {
+ return this.selectedRows.map((row) => omit(row, 'actions'));
+ },
+ filteredLogsByDate() {
+ return this.getFilteredTableDataByDate(
+ this.allLogs,
+ this.filterStartDate,
+ this.filterEndDate
+ );
+ },
+ filteredLogs() {
+ return this.getFilteredTableData(
+ this.filteredLogsByDate,
+ this.activeFilters
+ );
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store.dispatch('postCodeLogs/getPostCodesLogData').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ exportAllLogsString() {
+ {
+ return this.$store.getters['postCodeLogs/allPostCodes'].map(
+ (postCodes) => {
+ const allLogsString = JSON.stringify(postCodes);
+ return allLogsString;
+ }
+ );
+ }
+ },
+ onFilterChange({ activeFilters }) {
+ this.activeFilters = activeFilters;
+ },
+ onChangeDateTimeFilter({ fromDate, toDate }) {
+ this.filterStartDate = fromDate;
+ this.filterEndDate = toDate;
+ },
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ // Create export file name based on date and action
+ exportFileNameByDate(value) {
+ let date = new Date();
+ date =
+ date.toISOString().slice(0, 10) +
+ '_' +
+ date.toString().split(':').join('-').split(' ')[4];
+ let fileName;
+ if (value === 'download') {
+ fileName = this.$t('pagePostCodeLogs.downloadFilePrefix');
+ } else if (value === 'export') {
+ fileName = this.$t('pagePostCodeLogs.exportFilePrefix');
+ } else {
+ fileName = this.$t('pagePostCodeLogs.allExportFilePrefix');
+ }
+ return fileName + date;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Logs/PostCodeLogs/index.js b/src/views/_sila/Logs/PostCodeLogs/index.js
new file mode 100644
index 00000000..ab591124
--- /dev/null
+++ b/src/views/_sila/Logs/PostCodeLogs/index.js
@@ -0,0 +1,2 @@
+import PostCodeLogs from './PostCodeLogs.vue';
+export default PostCodeLogs;
diff --git a/src/views/_sila/Operations/FactoryReset/FactoryReset.vue b/src/views/_sila/Operations/FactoryReset/FactoryReset.vue
new file mode 100644
index 00000000..897348fc
--- /dev/null
+++ b/src/views/_sila/Operations/FactoryReset/FactoryReset.vue
@@ -0,0 +1,117 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pageFactoryReset.description')" />
+
+ <!-- Reset Form -->
+ <b-form id="factory-reset" @submit.prevent="onResetSubmit">
+ <b-row>
+ <b-col md="8">
+ <b-form-group :label="$t('pageFactoryReset.form.resetOptionsLabel')">
+ <b-form-radio-group
+ id="factory-reset-options"
+ v-model="resetOption"
+ stacked
+ >
+ <b-form-radio
+ class="mb-1"
+ value="resetBios"
+ aria-describedby="reset-bios"
+ data-test-id="factoryReset-radio-resetBios"
+ >
+ {{ $t('pageFactoryReset.form.resetBiosOptionLabel') }}
+ </b-form-radio>
+ <b-form-text id="reset-bios" class="ml-4 mb-3">
+ {{ $t('pageFactoryReset.form.resetBiosOptionHelperText') }}
+ </b-form-text>
+
+ <b-form-radio
+ class="mb-1"
+ value="resetToDefaults"
+ aria-describedby="reset-to-defaults"
+ data-test-id="factoryReset-radio-resetToDefaults"
+ >
+ {{ $t('pageFactoryReset.form.resetToDefaultsOptionLabel') }}
+ </b-form-radio>
+ <b-form-text id="reset-to-defaults" class="ml-4 mb-3">
+ {{
+ $t('pageFactoryReset.form.resetToDefaultsOptionHelperText')
+ }}
+ </b-form-text>
+ </b-form-radio-group>
+ </b-form-group>
+ <b-button
+ type="submit"
+ variant="primary"
+ data-test-id="factoryReset-button-submit"
+ >
+ {{ $t('global.action.reset') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ </b-form>
+
+ <!-- Modals -->
+ <modal-reset :reset-type="resetOption" @okConfirm="onOkConfirm" />
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import ModalReset from './FactoryResetModal';
+
+export default {
+ name: 'FactoryReset',
+ components: { PageTitle, ModalReset },
+ mixins: [LoadingBarMixin, BVToastMixin],
+ data() {
+ return {
+ resetOption: 'resetBios',
+ };
+ },
+ created() {
+ this.hideLoader();
+ },
+ methods: {
+ onResetSubmit() {
+ this.$bvModal.show('modal-reset');
+ },
+ onOkConfirm() {
+ if (this.resetOption == 'resetBios') {
+ this.onResetBiosConfirm();
+ } else {
+ this.onResetToDefaultsConfirm();
+ }
+ },
+ onResetBiosConfirm() {
+ this.$store
+ .dispatch('factoryReset/resetBios')
+ .then((title) => {
+ this.successToast('', {
+ title,
+ });
+ })
+ .catch(({ message }) => {
+ this.errorToast('', {
+ title: message,
+ });
+ });
+ },
+ onResetToDefaultsConfirm() {
+ this.$store
+ .dispatch('factoryReset/resetToDefaults')
+ .then((title) => {
+ this.successToast('', {
+ title,
+ });
+ })
+ .catch(({ message }) => {
+ this.errorToast('', {
+ title: message,
+ });
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/FactoryReset/FactoryResetModal.vue b/src/views/_sila/Operations/FactoryReset/FactoryResetModal.vue
new file mode 100644
index 00000000..170bf284
--- /dev/null
+++ b/src/views/_sila/Operations/FactoryReset/FactoryResetModal.vue
@@ -0,0 +1,113 @@
+<template>
+ <b-modal
+ id="modal-reset"
+ ref="modal"
+ :title="$t(`pageFactoryReset.modal.${resetType}Title`)"
+ title-tag="h2"
+ @hidden="resetConfirm"
+ >
+ <p class="mb-2">
+ <strong>{{ $t(`pageFactoryReset.modal.${resetType}Header`) }}</strong>
+ </p>
+ <ul class="pl-3 mb-4">
+ <li
+ v-for="(item, index) in $t(
+ `pageFactoryReset.modal.${resetType}SettingsList`
+ )"
+ :key="index"
+ class="mt-1 mb-1"
+ >
+ {{ $t(item) }}
+ </li>
+ </ul>
+
+ <!-- Warning message -->
+ <template v-if="!isServerOff">
+ <p class="d-flex mb-2">
+ <status-icon status="danger" />
+ <span id="reset-to-default-warning" class="ml-1">
+ {{ $t(`pageFactoryReset.modal.resetWarningMessage`) }}
+ </span>
+ </p>
+ <b-form-checkbox
+ v-model="confirm"
+ aria-describedby="reset-to-default-warning"
+ @input="$v.confirm.$touch()"
+ >
+ {{ $t(`pageFactoryReset.modal.resetWarningCheckLabel`) }}
+ </b-form-checkbox>
+ <b-form-invalid-feedback
+ role="alert"
+ :state="getValidationState($v.confirm)"
+ >
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </template>
+
+ <template #modal-footer="{ cancel }">
+ <b-button
+ variant="secondary"
+ data-test-id="factoryReset-button-cancel"
+ @click="cancel()"
+ >
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button
+ type="sumbit"
+ variant="primary"
+ data-test-id="factoryReset-button-confirm"
+ @click="handleConfirm"
+ >
+ {{ $t(`pageFactoryReset.modal.${resetType}SubmitText`) }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+<script>
+import StatusIcon from '@/components/Global/StatusIcon';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin';
+
+export default {
+ components: { StatusIcon },
+ mixins: [VuelidateMixin],
+ props: {
+ resetType: {
+ type: String,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ confirm: false,
+ };
+ },
+ computed: {
+ serverStatus() {
+ return this.$store.getters['global/serverStatus'];
+ },
+ isServerOff() {
+ return this.serverStatus === 'off' ? true : false;
+ },
+ },
+ validations: {
+ confirm: {
+ mustBeTrue: function (value) {
+ return this.isServerOff || value === true;
+ },
+ },
+ },
+ methods: {
+ handleConfirm() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('okConfirm');
+ this.$nextTick(() => this.$refs.modal.hide());
+ this.resetConfirm();
+ },
+ resetConfirm() {
+ this.confirm = false;
+ this.$v.$reset();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/FactoryReset/index.js b/src/views/_sila/Operations/FactoryReset/index.js
new file mode 100644
index 00000000..eae747e0
--- /dev/null
+++ b/src/views/_sila/Operations/FactoryReset/index.js
@@ -0,0 +1,2 @@
+import FactoryReset from './FactoryReset.vue';
+export default FactoryReset;
diff --git a/src/views/_sila/Operations/Firmware/Firmware.vue b/src/views/_sila/Operations/Firmware/Firmware.vue
new file mode 100644
index 00000000..a2acb9b0
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/Firmware.vue
@@ -0,0 +1,93 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <alerts-server-power
+ v-if="isServerPowerOffRequired"
+ :is-server-off="isServerOff"
+ />
+
+ <!-- Firmware cards -->
+ <b-row>
+ <b-col xl="10">
+ <!-- BMC Firmware -->
+ <bmc-cards :is-page-disabled="isPageDisabled" />
+
+ <!-- Host Firmware -->
+ <host-cards v-if="!isSingleFileUploadEnabled" />
+ </b-col>
+ </b-row>
+
+ <!-- Update firmware-->
+ <page-section
+ :section-title="$t('pageFirmware.sectionTitleUpdateFirmware')"
+ >
+ <b-row>
+ <b-col sm="8" md="6" xl="4">
+ <!-- Update form -->
+ <form-update
+ :is-server-off="isServerOff"
+ :is-page-disabled="isPageDisabled"
+ />
+ </b-col>
+ </b-row>
+ </page-section>
+ </b-container>
+</template>
+
+<script>
+import AlertsServerPower from './FirmwareAlertServerPower';
+import BmcCards from './FirmwareCardsBmc';
+import FormUpdate from './FirmwareFormUpdate';
+import HostCards from './FirmwareCardsHost';
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+
+export default {
+ name: 'FirmwareSingleImage',
+ components: {
+ AlertsServerPower,
+ BmcCards,
+ FormUpdate,
+ HostCards,
+ PageSection,
+ PageTitle,
+ },
+ mixins: [LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ loading,
+ isServerPowerOffRequired:
+ process.env.VUE_APP_SERVER_OFF_REQUIRED === 'true',
+ };
+ },
+ computed: {
+ serverStatus() {
+ return this.$store.getters['global/serverStatus'];
+ },
+ isServerOff() {
+ return this.serverStatus === 'off' ? true : false;
+ },
+ isSingleFileUploadEnabled() {
+ return this.$store.getters['firmware/isSingleFileUploadEnabled'];
+ },
+ isPageDisabled() {
+ if (this.isServerPowerOffRequired) {
+ return !this.isServerOff || this.loading || this.isOperationInProgress;
+ }
+ return this.loading || this.isOperationInProgress;
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store
+ .dispatch('firmware/getFirmwareInformation')
+ .finally(() => this.endLoader());
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/Firmware/FirmwareAlertServerPower.vue b/src/views/_sila/Operations/Firmware/FirmwareAlertServerPower.vue
new file mode 100644
index 00000000..24aa1d69
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/FirmwareAlertServerPower.vue
@@ -0,0 +1,50 @@
+<template>
+ <b-row>
+ <b-col xl="10">
+ <!-- Operation in progress alert -->
+ <alert v-if="isOperationInProgress" variant="info" class="mb-5">
+ <p>
+ {{ $t('pageFirmware.alert.operationInProgress') }}
+ </p>
+ </alert>
+ <!-- Power off server warning alert -->
+ <alert v-else-if="!isServerOff" variant="warning" class="mb-5">
+ <p class="mb-0">
+ {{ $t('pageFirmware.alert.serverMustBePoweredOffTo') }}
+ </p>
+ <ul class="m-0">
+ <li>
+ {{ $t('pageFirmware.alert.switchRunningAndBackupImages') }}
+ </li>
+ <li>
+ {{ $t('pageFirmware.alert.updateFirmware') }}
+ </li>
+ </ul>
+ <template #action>
+ <b-link to="/operations/server-power-operations">
+ {{ $t('pageFirmware.alert.viewServerPowerOperations') }}
+ </b-link>
+ </template>
+ </alert>
+ </b-col>
+ </b-row>
+</template>
+
+<script>
+import Alert from '@/components/Global/Alert';
+
+export default {
+ components: { Alert },
+ props: {
+ isServerOff: {
+ required: true,
+ type: Boolean,
+ },
+ },
+ computed: {
+ isOperationInProgress() {
+ return this.$store.getters['controls/isOperationInProgress'];
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/Firmware/FirmwareCardsBmc.vue b/src/views/_sila/Operations/Firmware/FirmwareCardsBmc.vue
new file mode 100644
index 00000000..d79a8769
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/FirmwareCardsBmc.vue
@@ -0,0 +1,136 @@
+<template>
+ <div>
+ <page-section :section-title="sectionTitle">
+ <b-card-group deck>
+ <!-- Running image -->
+ <b-card>
+ <template #header>
+ <p class="font-weight-bold m-0">
+ {{ $t('pageFirmware.cardTitleRunning') }}
+ </p>
+ </template>
+ <dl class="mb-0">
+ <dt>{{ $t('pageFirmware.cardBodyVersion') }}</dt>
+ <dd class="mb-0">{{ runningVersion }}</dd>
+ </dl>
+ </b-card>
+
+ <!-- Backup image -->
+ <b-card>
+ <template #header>
+ <p class="font-weight-bold m-0">
+ {{ $t('pageFirmware.cardTitleBackup') }}
+ </p>
+ </template>
+ <dl>
+ <dt>{{ $t('pageFirmware.cardBodyVersion') }}</dt>
+ <dd>
+ <status-icon v-if="showBackupImageStatus" status="danger" />
+ <span v-if="showBackupImageStatus" class="sr-only">
+ {{ backupStatus }}
+ </span>
+ {{ backupVersion }}
+ </dd>
+ </dl>
+ <b-btn
+ v-if="!switchToBackupImageDisabled"
+ v-b-modal.modal-switch-to-running
+ data-test-id="firmware-button-switchToRunning"
+ variant="link"
+ size="sm"
+ class="py-0 px-1 mt-2"
+ :disabled="isPageDisabled || !backup"
+ >
+ <icon-switch class="d-none d-sm-inline-block" />
+ {{ $t('pageFirmware.cardActionSwitchToRunning') }}
+ </b-btn>
+ </b-card>
+ </b-card-group>
+ </page-section>
+ <modal-switch-to-running :backup="backupVersion" @ok="switchToRunning" />
+ </div>
+</template>
+
+<script>
+import IconSwitch from '@carbon/icons-vue/es/arrows--horizontal/20';
+import PageSection from '@/components/Global/PageSection';
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+import ModalSwitchToRunning from './FirmwareModalSwitchToRunning';
+
+export default {
+ components: { IconSwitch, ModalSwitchToRunning, PageSection },
+ mixins: [BVToastMixin, LoadingBarMixin],
+ props: {
+ isPageDisabled: {
+ required: true,
+ type: Boolean,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ loading,
+ switchToBackupImageDisabled:
+ process.env.VUE_APP_SWITCH_TO_BACKUP_IMAGE_DISABLED === 'true',
+ };
+ },
+ computed: {
+ isSingleFileUploadEnabled() {
+ return this.$store.getters['firmware/isSingleFileUploadEnabled'];
+ },
+ sectionTitle() {
+ if (this.isSingleFileUploadEnabled) {
+ return this.$t('pageFirmware.sectionTitleBmcCardsCombined');
+ }
+ return this.$t('pageFirmware.sectionTitleBmcCards');
+ },
+ running() {
+ return this.$store.getters['firmware/activeBmcFirmware'];
+ },
+ backup() {
+ return this.$store.getters['firmware/backupBmcFirmware'];
+ },
+ runningVersion() {
+ return this.running?.version || '--';
+ },
+ backupVersion() {
+ return this.backup?.version || '--';
+ },
+ backupStatus() {
+ return this.backup?.status || null;
+ },
+ showBackupImageStatus() {
+ return (
+ this.backupStatus === 'Critical' || this.backupStatus === 'Warning'
+ );
+ },
+ },
+ methods: {
+ switchToRunning() {
+ this.startLoader();
+ const timerId = setTimeout(() => {
+ this.endLoader();
+ this.infoToast(this.$t('pageFirmware.toast.verifySwitchMessage'), {
+ title: this.$t('pageFirmware.toast.verifySwitch'),
+ refreshAction: true,
+ });
+ }, 60000);
+
+ this.$store
+ .dispatch('firmware/switchBmcFirmwareAndReboot')
+ .then(() =>
+ this.infoToast(this.$t('pageFirmware.toast.rebootStartedMessage'), {
+ title: this.$t('pageFirmware.toast.rebootStarted'),
+ })
+ )
+ .catch(({ message }) => {
+ this.errorToast(message);
+ clearTimeout(timerId);
+ this.endLoader();
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/Firmware/FirmwareCardsHost.vue b/src/views/_sila/Operations/Firmware/FirmwareCardsHost.vue
new file mode 100644
index 00000000..b4a8e90d
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/FirmwareCardsHost.vue
@@ -0,0 +1,73 @@
+<template>
+ <page-section :section-title="$t('pageFirmware.sectionTitleHostCards')">
+ <b-card-group deck>
+ <!-- Running image -->
+ <b-card>
+ <template #header>
+ <p class="font-weight-bold m-0">
+ {{ $t('pageFirmware.cardTitleRunning') }}
+ </p>
+ </template>
+ <dl class="mb-0">
+ <dt>{{ $t('pageFirmware.cardBodyVersion') }}</dt>
+ <dd class="mb-0">{{ runningVersion }}</dd>
+ </dl>
+ </b-card>
+
+ <!-- Backup image -->
+ <b-card>
+ <template #header>
+ <p class="font-weight-bold m-0">
+ {{ $t('pageFirmware.cardTitleBackup') }}
+ </p>
+ </template>
+ <dl class="mb-0">
+ <dt>{{ $t('pageFirmware.cardBodyVersion') }}</dt>
+ <dd class="mb-0">
+ <status-icon v-if="showBackupImageStatus" status="danger" />
+ <span v-if="showBackupImageStatus" class="sr-only">
+ {{ backupStatus }}
+ </span>
+ {{ backupVersion }}
+ </dd>
+ </dl>
+ </b-card>
+ </b-card-group>
+ </page-section>
+</template>
+
+<script>
+import PageSection from '@/components/Global/PageSection';
+
+export default {
+ components: { PageSection },
+ computed: {
+ running() {
+ return this.$store.getters['firmware/activeHostFirmware'];
+ },
+ backup() {
+ return this.$store.getters['firmware/backupHostFirmware'];
+ },
+ runningVersion() {
+ return this.running?.version || '--';
+ },
+ backupVersion() {
+ return this.backup?.version || '--';
+ },
+ backupStatus() {
+ return this.backup?.status || null;
+ },
+ showBackupImageStatus() {
+ return (
+ this.backupStatus === 'Critical' || this.backupStatus === 'Warning'
+ );
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.page-section {
+ margin-top: -$spacer * 1.5;
+}
+</style>
diff --git a/src/views/_sila/Operations/Firmware/FirmwareFormUpdate.vue b/src/views/_sila/Operations/Firmware/FirmwareFormUpdate.vue
new file mode 100644
index 00000000..ac4b23fc
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/FirmwareFormUpdate.vue
@@ -0,0 +1,187 @@
+<template>
+ <div>
+ <div class="form-background p-3">
+ <b-form @submit.prevent="onSubmitUpload">
+ <b-form-group
+ v-if="isTftpUploadAvailable"
+ :label="$t('pageFirmware.form.updateFirmware.fileSource')"
+ :disabled="isPageDisabled"
+ >
+ <b-form-radio v-model="isWorkstationSelected" :value="true">
+ {{ $t('pageFirmware.form.updateFirmware.workstation') }}
+ </b-form-radio>
+ <b-form-radio v-model="isWorkstationSelected" :value="false">
+ {{ $t('pageFirmware.form.updateFirmware.tftpServer') }}
+ </b-form-radio>
+ </b-form-group>
+
+ <!-- Workstation Upload -->
+ <template v-if="isWorkstationSelected">
+ <b-form-group
+ :label="$t('pageFirmware.form.updateFirmware.imageFile')"
+ label-for="image-file"
+ >
+ <form-file
+ id="image-file"
+ :disabled="isPageDisabled"
+ :state="getValidationState($v.file)"
+ aria-describedby="image-file-help-block"
+ @input="onFileUpload($event)"
+ >
+ <template #invalid>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.required') }}
+ </b-form-invalid-feedback>
+ </template>
+ </form-file>
+ </b-form-group>
+ </template>
+
+ <!-- TFTP Server Upload -->
+ <template v-else>
+ <b-form-group
+ :label="$t('pageFirmware.form.updateFirmware.fileAddress')"
+ label-for="tftp-address"
+ >
+ <b-form-input
+ id="tftp-address"
+ v-model="tftpFileAddress"
+ type="text"
+ :state="getValidationState($v.tftpFileAddress)"
+ :disabled="isPageDisabled"
+ @input="$v.tftpFileAddress.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </template>
+ <b-btn
+ data-test-id="firmware-button-startUpdate"
+ type="submit"
+ variant="primary"
+ :disabled="isPageDisabled"
+ >
+ {{ $t('pageFirmware.form.updateFirmware.startUpdate') }}
+ </b-btn>
+ </b-form>
+ </div>
+
+ <!-- Modals -->
+ <modal-update-firmware @ok="updateFirmware" />
+ </div>
+</template>
+
+<script>
+import { requiredIf } from 'vuelidate/lib/validators';
+
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+import FormFile from '@/components/Global/FormFile';
+import ModalUpdateFirmware from './FirmwareModalUpdateFirmware';
+
+export default {
+ components: { FormFile, ModalUpdateFirmware },
+ mixins: [BVToastMixin, LoadingBarMixin, VuelidateMixin],
+ props: {
+ isPageDisabled: {
+ required: true,
+ type: Boolean,
+ default: false,
+ },
+ isServerOff: {
+ required: true,
+ type: Boolean,
+ },
+ },
+ data() {
+ return {
+ loading,
+ isWorkstationSelected: true,
+ file: null,
+ tftpFileAddress: null,
+ isServerPowerOffRequired:
+ process.env.VUE_APP_SERVER_OFF_REQUIRED === 'true',
+ };
+ },
+ computed: {
+ isTftpUploadAvailable() {
+ return this.$store.getters['firmware/isTftpUploadAvailable'];
+ },
+ },
+ watch: {
+ isWorkstationSelected: function () {
+ this.$v.$reset();
+ this.file = null;
+ this.tftpFileAddress = null;
+ },
+ },
+ validations() {
+ return {
+ file: {
+ required: requiredIf(function () {
+ return this.isWorkstationSelected;
+ }),
+ },
+ tftpFileAddress: {
+ required: requiredIf(function () {
+ return !this.isWorkstationSelected;
+ }),
+ },
+ };
+ },
+ created() {
+ this.$store.dispatch('firmware/getUpdateServiceSettings');
+ },
+ methods: {
+ updateFirmware() {
+ this.startLoader();
+ const timerId = setTimeout(() => {
+ this.endLoader();
+ this.infoToast(this.$t('pageFirmware.toast.verifyUpdateMessage'), {
+ title: this.$t('pageFirmware.toast.verifyUpdate'),
+ refreshAction: true,
+ });
+ }, 360000);
+ this.infoToast(this.$t('pageFirmware.toast.updateStartedMessage'), {
+ title: this.$t('pageFirmware.toast.updateStarted'),
+ timestamp: true,
+ });
+ if (this.isWorkstationSelected) {
+ this.dispatchWorkstationUpload(timerId);
+ } else {
+ this.dispatchTftpUpload(timerId);
+ }
+ },
+ dispatchWorkstationUpload(timerId) {
+ this.$store
+ .dispatch('firmware/uploadFirmware', this.file)
+ .catch(({ message }) => {
+ this.endLoader();
+ this.errorToast(message);
+ clearTimeout(timerId);
+ });
+ },
+ dispatchTftpUpload(timerId) {
+ this.$store
+ .dispatch('firmware/uploadFirmwareTFTP', this.tftpFileAddress)
+ .catch(({ message }) => {
+ this.endLoader();
+ this.errorToast(message);
+ clearTimeout(timerId);
+ });
+ },
+ onSubmitUpload() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$bvModal.show('modal-update-firmware');
+ },
+ onFileUpload(file) {
+ this.file = file;
+ this.$v.file.$touch();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/Firmware/FirmwareModalSwitchToRunning.vue b/src/views/_sila/Operations/Firmware/FirmwareModalSwitchToRunning.vue
new file mode 100644
index 00000000..dc4a4973
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/FirmwareModalSwitchToRunning.vue
@@ -0,0 +1,31 @@
+<template>
+ <b-modal
+ id="modal-switch-to-running"
+ :ok-title="$t('pageFirmware.modal.switchImages')"
+ :cancel-title="$t('global.action.cancel')"
+ :title="$t('pageFirmware.modal.switchRunningImage')"
+ @ok="$emit('ok')"
+ >
+ <p>
+ {{ $t('pageFirmware.modal.switchRunningImageInfo') }}
+ </p>
+ <p class="m-0">
+ {{
+ $t('pageFirmware.modal.switchRunningImageInfo2', {
+ backup,
+ })
+ }}
+ </p>
+ </b-modal>
+</template>
+
+<script>
+export default {
+ props: {
+ backup: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/Firmware/FirmwareModalUpdateFirmware.vue b/src/views/_sila/Operations/Firmware/FirmwareModalUpdateFirmware.vue
new file mode 100644
index 00000000..18355217
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/FirmwareModalUpdateFirmware.vue
@@ -0,0 +1,44 @@
+<template>
+ <b-modal
+ id="modal-update-firmware"
+ :title="$t('pageFirmware.sectionTitleUpdateFirmware')"
+ :ok-title="$t('pageFirmware.form.updateFirmware.startUpdate')"
+ :cancel-title="$t('global.action.cancel')"
+ @ok="$emit('ok')"
+ >
+ <template v-if="isSingleFileUploadEnabled">
+ <p>
+ {{ $t('pageFirmware.modal.updateFirmwareInfo') }}
+ </p>
+ <p>
+ {{
+ $t('pageFirmware.modal.updateFirmwareInfo2', {
+ running: runningBmcVersion,
+ })
+ }}
+ </p>
+ <p class="m-0">
+ {{ $t('pageFirmware.modal.updateFirmwareInfo3') }}
+ </p>
+ </template>
+ <template v-else>
+ {{ $t('pageFirmware.modal.updateFirmwareInfoDefault') }}
+ </template>
+ </b-modal>
+</template>
+
+<script>
+export default {
+ computed: {
+ runningBmc() {
+ return this.$store.getters['firmware/activeBmcFirmware'];
+ },
+ runningBmcVersion() {
+ return this.runningBmc?.version || '--';
+ },
+ isSingleFileUploadEnabled() {
+ return this.$store.getters['firmware/isSingleFileUploadEnabled'];
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/Firmware/index.js b/src/views/_sila/Operations/Firmware/index.js
new file mode 100644
index 00000000..ad15cc03
--- /dev/null
+++ b/src/views/_sila/Operations/Firmware/index.js
@@ -0,0 +1,2 @@
+import Firmware from './Firmware.vue';
+export default Firmware;
diff --git a/src/views/_sila/Operations/KeyClear/KeyClear.vue b/src/views/_sila/Operations/KeyClear/KeyClear.vue
new file mode 100644
index 00000000..2524da10
--- /dev/null
+++ b/src/views/_sila/Operations/KeyClear/KeyClear.vue
@@ -0,0 +1,106 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pageKeyClear.description')" />
+ <b-row>
+ <b-col md="8" xl="6">
+ <alert variant="info" class="mb-4">
+ <div class="font-weight-bold">
+ {{ $t('pageKeyClear.alert.title') }}
+ </div>
+ <div>
+ {{ $t('pageKeyClear.alert.description') }}
+ </div>
+ </alert>
+ </b-col>
+ </b-row>
+ <!-- Reset Form -->
+ <b-form id="key-clear" @submit.prevent="onKeyClearSubmit(keyOption)">
+ <b-row>
+ <b-col md="8">
+ <b-form-group :label="$t('pageKeyClear.form.keyClearOptionsLabel')">
+ <b-form-radio-group
+ id="key-clear-options"
+ v-model="keyOption"
+ stacked
+ >
+ <b-form-radio class="mb-1" value="NONE">
+ {{ $t('pageKeyClear.form.none') }}
+ </b-form-radio>
+ <b-form-text id="key-clear-not-requested" class="ml-4 mb-3">
+ {{ $t('pageKeyClear.form.keyClearNotRequested') }}
+ </b-form-text>
+ <b-form-radio class="mb-1" value="ALL">
+ {{ $t('pageKeyClear.form.clearAllLabel') }}
+ </b-form-radio>
+ <b-form-text id="clear-all" class="ml-4 mb-3">
+ {{ $t('pageKeyClear.form.clearAllHeperText') }}
+ </b-form-text>
+ <b-form-radio class="mb-1" value="POWERVM_SYSKEY">
+ {{ $t('pageKeyClear.form.clearHypervisorSystemKeyLabel') }}
+ </b-form-radio>
+ <b-form-text id="clear-hypervisor-key" class="ml-4 mb-3">
+ {{ $t('pageKeyClear.form.clearHypervisorSystemKeyHelperText') }}
+ </b-form-text>
+ <template v-if="username == 'service'">
+ <b-form-radio class="mb-1" value="MFG_ALL">
+ {{ $t('pageKeyClear.form.clearAllSetGenesisIPL') }}
+ </b-form-radio>
+ <b-form-radio class="mb-1" value="MFG">
+ {{ $t('pageKeyClear.form.setFactoryDefault') }}
+ </b-form-radio>
+ </template>
+ </b-form-radio-group>
+ </b-form-group>
+ <b-button
+ type="submit"
+ variant="primary"
+ data-test-id="keyClear-button-submit"
+ >
+ {{ $t('pageKeyClear.form.clear') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ </b-form>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import Alert from '@/components/Global/Alert';
+
+export default {
+ name: 'KeyClear',
+ components: { PageTitle, Alert },
+ mixins: [LoadingBarMixin, BVToastMixin],
+ data() {
+ return {
+ keyOption: 'NONE',
+ username: this.$store.getters['global/username'],
+ };
+ },
+ created() {
+ this.hideLoader();
+ },
+ methods: {
+ onKeyClearSubmit(valueSelected) {
+ this.$bvModal
+ .msgBoxConfirm(this.$t('pageKeyClear.modal.clearAllMessage'), {
+ title: this.$t('pageKeyClear.modal.clearAllTitle'),
+ okTitle: this.$t('pageKeyClear.modal.clear'),
+ okVariant: 'danger',
+ cancelTitle: this.$t('global.action.cancel'),
+ })
+ .then((clearConfirmed) => {
+ if (clearConfirmed) {
+ this.$store
+ .dispatch('keyClear/clearEncryptionKeys', valueSelected)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ }
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/KeyClear/index.js b/src/views/_sila/Operations/KeyClear/index.js
new file mode 100644
index 00000000..56de8c4e
--- /dev/null
+++ b/src/views/_sila/Operations/KeyClear/index.js
@@ -0,0 +1,2 @@
+import KeyClear from './KeyClear.vue';
+export default KeyClear;
diff --git a/src/views/_sila/Operations/Kvm/Kvm.vue b/src/views/_sila/Operations/Kvm/Kvm.vue
new file mode 100644
index 00000000..1a41baaf
--- /dev/null
+++ b/src/views/_sila/Operations/Kvm/Kvm.vue
@@ -0,0 +1,24 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <div class="terminal-container">
+ <kvm-console :is-full-window="false" />
+ </div>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import KvmConsole from './KvmConsole';
+
+export default {
+ name: 'Kvm',
+ components: { PageTitle, KvmConsole },
+};
+</script>
+
+<style scoped>
+.terminal-container {
+ width: 100%;
+}
+</style>
diff --git a/src/views/_sila/Operations/Kvm/KvmConsole.vue b/src/views/_sila/Operations/Kvm/KvmConsole.vue
new file mode 100644
index 00000000..c028a9fc
--- /dev/null
+++ b/src/views/_sila/Operations/Kvm/KvmConsole.vue
@@ -0,0 +1,170 @@
+<template>
+ <div :class="marginClass">
+ <div ref="toolbar" class="kvm-toolbar">
+ <b-row class="d-flex">
+ <b-col class="d-flex flex-column justify-content-end" cols="4">
+ <dl class="mb-2" sm="2" md="2">
+ <dt class="d-inline font-weight-bold mr-1">
+ {{ $t('pageKvm.status') }}:
+ </dt>
+ <dd class="d-inline">
+ <status-icon :status="serverStatusIcon" />
+ <span class="d-none d-md-inline"> {{ serverStatus }}</span>
+ </dd>
+ </dl>
+ </b-col>
+
+ <b-col class="d-flex justify-content-end pr-1">
+ <b-button
+ v-if="isConnected"
+ variant="link"
+ type="button"
+ @click="sendCtrlAltDel"
+ >
+ <icon-arrow-down />
+ {{ $t('pageKvm.buttonCtrlAltDelete') }}
+ </b-button>
+ <b-button
+ v-if="!isFullWindow"
+ variant="link"
+ type="button"
+ @click="openConsoleWindow()"
+ >
+ <icon-launch />
+ {{ $t('pageKvm.openNewTab') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ </div>
+ <div id="terminal-kvm" ref="panel" :class="terminalClass"></div>
+ </div>
+</template>
+
+<script>
+import RFB from '@novnc/novnc/core/rfb';
+import StatusIcon from '@/components/Global/StatusIcon';
+import IconLaunch from '@carbon/icons-vue/es/launch/20';
+import IconArrowDown from '@carbon/icons-vue/es/arrow--down/16';
+import { throttle } from 'lodash';
+
+const Connecting = 0;
+const Connected = 1;
+const Disconnected = 2;
+
+export default {
+ name: 'KvmConsole',
+ components: { StatusIcon, IconLaunch, IconArrowDown },
+ props: {
+ isFullWindow: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ rfb: null,
+ isConnected: false,
+ terminalClass: this.isFullWindow ? 'full-window' : '',
+ marginClass: this.isFullWindow ? 'margin-left-full-window' : '',
+ status: Connecting,
+ convasRef: null,
+ resizeKvmWindow: null,
+ };
+ },
+ computed: {
+ serverStatusIcon() {
+ if (this.status === Connected) {
+ return 'success';
+ } else if (this.status === Disconnected) {
+ return 'danger';
+ }
+ return 'secondary';
+ },
+ serverStatus() {
+ if (this.status === Connected) {
+ return this.$t('pageKvm.connected');
+ } else if (this.status === Disconnected) {
+ return this.$t('pageKvm.disconnected');
+ }
+ return this.$t('pageKvm.connecting');
+ },
+ },
+ mounted() {
+ this.openTerminal();
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeKvmWindow);
+ this.closeTerminal();
+ },
+ methods: {
+ sendCtrlAltDel() {
+ this.rfb.sendCtrlAltDel();
+ },
+ closeTerminal() {
+ this.rfb.disconnect();
+ this.rfb = null;
+ },
+ openTerminal() {
+ const token = this.$store.getters['authentication/token'];
+ this.rfb = new RFB(
+ this.$refs.panel,
+ `wss://${window.location.host}/kvm/0`,
+ { wsProtocols: [token] }
+ );
+
+ this.rfb.scaleViewport = true;
+ this.rfb.clipViewport = true;
+ const that = this;
+
+ this.resizeKvmWindow = throttle(() => {
+ setTimeout(that.setWidthToolbar, 0);
+ }, 1000);
+ window.addEventListener('resize', this.resizeKvmWindow);
+
+ this.rfb.addEventListener('connect', () => {
+ that.isConnected = true;
+ that.status = Connected;
+ that.setWidthToolbar();
+ });
+
+ this.rfb.addEventListener('disconnect', () => {
+ this.isConnected = false;
+ that.status = Disconnected;
+ });
+ },
+ setWidthToolbar() {
+ if (
+ this.$refs.panel.children &&
+ this.$refs.panel.children.length > 0 &&
+ this.$refs.panel.children[0].children.length > 0
+ ) {
+ this.$refs.toolbar.style.width =
+ this.$refs.panel.children[0].children[0].clientWidth - 10 + 'px';
+ }
+ },
+ openConsoleWindow() {
+ window.open(
+ '#/console/kvm',
+ '_blank',
+ 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=yes,width=700,height=550'
+ );
+ },
+ },
+};
+</script>
+
+<style scoped lang="scss">
+.button-ctrl-alt-delete {
+ float: right;
+}
+
+.kvm-status {
+ padding-top: $spacer / 2;
+ padding-left: $spacer / 4;
+ display: inline-block;
+}
+
+.margin-left-full-window {
+ margin-left: 5px;
+}
+</style>
diff --git a/src/views/_sila/Operations/Kvm/index.js b/src/views/_sila/Operations/Kvm/index.js
new file mode 100644
index 00000000..ac4f9667
--- /dev/null
+++ b/src/views/_sila/Operations/Kvm/index.js
@@ -0,0 +1,2 @@
+import Kvm from './Kvm.vue';
+export default Kvm;
diff --git a/src/views/_sila/Operations/RebootBmc/RebootBmc.vue b/src/views/_sila/Operations/RebootBmc/RebootBmc.vue
new file mode 100644
index 00000000..900619cd
--- /dev/null
+++ b/src/views/_sila/Operations/RebootBmc/RebootBmc.vue
@@ -0,0 +1,83 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row>
+ <b-col md="8" lg="8" xl="6">
+ <page-section>
+ <b-row>
+ <b-col>
+ <dl>
+ <dt>
+ {{ $t('pageRebootBmc.lastReboot') }}
+ </dt>
+ <dd v-if="lastBmcRebootTime">
+ {{ lastBmcRebootTime | formatDate }}
+ {{ lastBmcRebootTime | formatTime }}
+ </dd>
+ <dd v-else>--</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ {{ $t('pageRebootBmc.rebootInformation') }}
+ <b-button
+ variant="primary"
+ class="d-block mt-5"
+ data-test-id="rebootBmc-button-reboot"
+ @click="onClick"
+ >
+ {{ $t('pageRebootBmc.rebootBmc') }}
+ </b-button>
+ </page-section>
+ </b-col>
+ </b-row>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+
+export default {
+ name: 'RebootBmc',
+ components: { PageTitle, PageSection },
+ mixins: [BVToastMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ computed: {
+ lastBmcRebootTime() {
+ return this.$store.getters['controls/lastBmcRebootTime'];
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store
+ .dispatch('controls/getLastBmcRebootTime')
+ .finally(() => this.endLoader());
+ },
+ methods: {
+ onClick() {
+ this.$bvModal
+ .msgBoxConfirm(this.$t('pageRebootBmc.modal.confirmMessage'), {
+ title: this.$t('pageRebootBmc.modal.confirmTitle'),
+ okTitle: this.$t('global.action.confirm'),
+ cancelTitle: this.$t('global.action.cancel'),
+ })
+ .then((confirmed) => {
+ if (confirmed) this.rebootBmc();
+ });
+ },
+ rebootBmc() {
+ this.$store
+ .dispatch('controls/rebootBmc')
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/_sila/Operations/RebootBmc/index.js b/src/views/_sila/Operations/RebootBmc/index.js
new file mode 100644
index 00000000..ac31417e
--- /dev/null
+++ b/src/views/_sila/Operations/RebootBmc/index.js
@@ -0,0 +1,2 @@
+import RebootBmc from './RebootBmc.vue';
+export default RebootBmc;
diff --git a/src/views/_sila/Operations/SerialOverLan/SerialOverLan.vue b/src/views/_sila/Operations/SerialOverLan/SerialOverLan.vue
new file mode 100644
index 00000000..48a68345
--- /dev/null
+++ b/src/views/_sila/Operations/SerialOverLan/SerialOverLan.vue
@@ -0,0 +1,24 @@
+<template>
+ <b-container fluid="xl">
+ <page-title class="mb-4" :description="$t('pageSerialOverLan.subTitle')" />
+
+ <page-section class="mb-0">
+ <serial-over-lan-console :is-full-window="false" />
+ </page-section>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import SerialOverLanConsole from './SerialOverLanConsole';
+
+export default {
+ name: 'SerialOverLan',
+ components: {
+ PageSection,
+ PageTitle,
+ SerialOverLanConsole,
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/SerialOverLan/SerialOverLanConsole.vue b/src/views/_sila/Operations/SerialOverLan/SerialOverLanConsole.vue
new file mode 100644
index 00000000..694083fd
--- /dev/null
+++ b/src/views/_sila/Operations/SerialOverLan/SerialOverLanConsole.vue
@@ -0,0 +1,172 @@
+<template>
+ <div :class="isFullWindow ? 'full-window-container' : 'terminal-container'">
+ <b-row class="d-flex">
+ <b-col sm="4" md="6">
+ <alert
+ v-if="serverStatus === 'on' ? false : true"
+ variant="warning"
+ :small="true"
+ class="mt-4"
+ >
+ <p class="col-form-label">
+ {{ $t('pageSerialOverLan.alert.disconnectedAlertMessage') }}
+ </p>
+ </alert>
+ </b-col>
+ </b-row>
+ <b-row class="d-flex">
+ <b-col class="d-flex flex-column justify-content-end">
+ <dl class="mb-2" sm="6" md="6">
+ <dt class="d-inline font-weight-bold mr-1">
+ {{ $t('pageSerialOverLan.status') }}:
+ </dt>
+ <dd class="d-inline">
+ <status-icon :status="serverStatusIcon" /> {{ connectionStatus }}
+ </dd>
+ </dl>
+ </b-col>
+
+ <b-col v-if="!isFullWindow" class="d-flex justify-content-end">
+ <b-button variant="link" type="button" @click="openConsoleWindow()">
+ <icon-launch />
+ {{ $t('pageSerialOverLan.openNewTab') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <div id="terminal" ref="panel"></div>
+ </div>
+</template>
+
+<script>
+import Alert from '@/components/Global/Alert';
+import { AttachAddon } from 'xterm-addon-attach';
+import { FitAddon } from 'xterm-addon-fit';
+import { Terminal } from 'xterm';
+import { throttle } from 'lodash';
+import IconLaunch from '@carbon/icons-vue/es/launch/20';
+import StatusIcon from '@/components/Global/StatusIcon';
+
+export default {
+ name: 'SerialOverLanConsole',
+ components: {
+ Alert,
+ IconLaunch,
+ StatusIcon,
+ },
+ props: {
+ isFullWindow: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ resizeConsoleWindow: null,
+ };
+ },
+ computed: {
+ serverStatus() {
+ return this.$store.getters['global/serverStatus'];
+ },
+ serverStatusIcon() {
+ return this.serverStatus === 'on' ? 'success' : 'danger';
+ },
+ connectionStatus() {
+ return this.serverStatus === 'on'
+ ? this.$t('pageSerialOverLan.connected')
+ : this.$t('pageSerialOverLan.disconnected');
+ },
+ },
+ created() {
+ this.$store.dispatch('global/getServerStatus');
+ },
+ mounted() {
+ this.openTerminal();
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeConsoleWindow);
+ this.closeTerminal();
+ },
+ methods: {
+ openTerminal() {
+ const token = this.$store.getters['authentication/token'];
+
+ this.ws = new WebSocket(`wss://${window.location.host}/console0`, [
+ token,
+ ]);
+
+ // Refer https://github.com/xtermjs/xterm.js/ for xterm implementation and addons.
+
+ this.term = new Terminal({
+ fontSize: 15,
+ fontFamily:
+ 'SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
+ });
+
+ const attachAddon = new AttachAddon(this.ws);
+ this.term.loadAddon(attachAddon);
+
+ const fitAddon = new FitAddon();
+ this.term.loadAddon(fitAddon);
+
+ const SOL_THEME = {
+ background: '#19273c',
+ cursor: 'rgba(83, 146, 255, .5)',
+ scrollbar: 'rgba(83, 146, 255, .5)',
+ };
+ this.term.setOption('theme', SOL_THEME);
+
+ this.term.open(this.$refs.panel);
+ fitAddon.fit();
+
+ this.resizeConsoleWindow = throttle(() => {
+ fitAddon.fit();
+ }, 1000);
+ window.addEventListener('resize', this.resizeConsoleWindow);
+
+ try {
+ this.ws.onopen = function () {
+ console.log('websocket console0/ opened');
+ };
+ this.ws.onclose = function (event) {
+ console.log(
+ 'websocket console0/ closed. code: ' +
+ event.code +
+ ' reason: ' +
+ event.reason
+ );
+ };
+ } catch (error) {
+ console.log(error);
+ }
+ },
+ closeTerminal() {
+ console.log('closeTerminal');
+ this.term.dispose();
+ this.term = null;
+ this.ws.close();
+ this.ws = null;
+ },
+ openConsoleWindow() {
+ window.open(
+ '#/console/serial-over-lan-console',
+ '_blank',
+ 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=yes,width=600,height=550'
+ );
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+@import '~xterm/css/xterm.css';
+
+#terminal {
+ overflow: auto;
+}
+
+.full-window-container {
+ width: 97%;
+ margin: 1.5%;
+}
+</style>
diff --git a/src/views/_sila/Operations/SerialOverLan/index.js b/src/views/_sila/Operations/SerialOverLan/index.js
new file mode 100644
index 00000000..7c8bc7c0
--- /dev/null
+++ b/src/views/_sila/Operations/SerialOverLan/index.js
@@ -0,0 +1,2 @@
+import SerialOverLan from './SerialOverLan.vue';
+export default SerialOverLan;
diff --git a/src/views/_sila/Operations/ServerPowerOperations/BootSettings.vue b/src/views/_sila/Operations/ServerPowerOperations/BootSettings.vue
new file mode 100644
index 00000000..4896286b
--- /dev/null
+++ b/src/views/_sila/Operations/ServerPowerOperations/BootSettings.vue
@@ -0,0 +1,132 @@
+<template>
+ <div class="form-background p-3">
+ <b-form novalidate @submit.prevent="handleSubmit">
+ <b-form-group
+ :label="
+ $t('pageServerPowerOperations.bootSettings.bootSettingsOverride')
+ "
+ label-for="boot-option"
+ class="mb-3"
+ >
+ <b-form-select
+ id="boot-option"
+ v-model="form.bootOption"
+ :disabled="bootSourceOptions.length === 0"
+ :options="bootSourceOptions"
+ @change="onChangeSelect"
+ >
+ </b-form-select>
+ </b-form-group>
+ <b-form-checkbox
+ v-model="form.oneTimeBoot"
+ class="mb-4"
+ :disabled="form.bootOption === 'None'"
+ @change="$v.form.oneTimeBoot.$touch()"
+ >
+ {{ $t('pageServerPowerOperations.bootSettings.enableOneTimeBoot') }}
+ </b-form-checkbox>
+ <b-form-group
+ :label="$t('pageServerPowerOperations.bootSettings.tpmRequiredPolicy')"
+ >
+ <b-form-text id="tpm-required-policy-help-block">
+ {{
+ $t('pageServerPowerOperations.bootSettings.tpmRequiredPolicyHelper')
+ }}
+ </b-form-text>
+ <b-form-checkbox
+ id="tpm-required-policy"
+ v-model="form.tpmPolicyOn"
+ aria-describedby="tpm-required-policy-help-block"
+ @change="$v.form.tpmPolicyOn.$touch()"
+ >
+ {{ $t('global.status.enabled') }}
+ </b-form-checkbox>
+ </b-form-group>
+ <b-button variant="primary" type="submit" class="mb-3">
+ {{ $t('global.action.save') }}
+ </b-button>
+ </b-form>
+ </div>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+
+export default {
+ name: 'BootSettings',
+ mixins: [BVToastMixin, LoadingBarMixin],
+ data() {
+ return {
+ form: {
+ bootOption: this.$store.getters['serverBootSettings/bootSource'],
+ oneTimeBoot: this.$store.getters['serverBootSettings/overrideEnabled'],
+ tpmPolicyOn: this.$store.getters['serverBootSettings/tpmEnabled'],
+ },
+ };
+ },
+ computed: {
+ ...mapState('serverBootSettings', [
+ 'bootSourceOptions',
+ 'bootSource',
+ 'overrideEnabled',
+ 'tpmEnabled',
+ ]),
+ },
+ watch: {
+ bootSource: function (value) {
+ this.form.bootOption = value;
+ },
+ overrideEnabled: function (value) {
+ this.form.oneTimeBoot = value;
+ },
+ tpmEnabled: function (value) {
+ this.form.tpmPolicyOn = value;
+ },
+ },
+ validations: {
+ // Empty validations to leverage vuelidate form states
+ // to check for changed values
+ form: {
+ bootOption: {},
+ oneTimeBoot: {},
+ tpmPolicyOn: {},
+ },
+ },
+ created() {
+ this.$store
+ .dispatch('serverBootSettings/getTpmPolicy')
+ .finally(() =>
+ this.$root.$emit('server-power-operations-boot-settings-complete')
+ );
+ },
+ methods: {
+ handleSubmit() {
+ this.startLoader();
+ const tpmPolicyChanged = this.$v.form.tpmPolicyOn.$dirty;
+ let settings;
+ let bootSource = this.form.bootOption;
+ let overrideEnabled = this.form.oneTimeBoot;
+ let tpmEnabled = null;
+
+ if (tpmPolicyChanged) tpmEnabled = this.form.tpmPolicyOn;
+ settings = { bootSource, overrideEnabled, tpmEnabled };
+
+ this.$store
+ .dispatch('serverBootSettings/saveSettings', settings)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => {
+ this.$v.form.$reset();
+ this.endLoader();
+ });
+ },
+ onChangeSelect(selectedOption) {
+ this.$v.form.bootOption.$touch();
+ // Disable one time boot if selected boot option is 'None'
+ if (selectedOption === 'None') this.form.oneTimeBoot = false;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/ServerPowerOperations/ServerPowerOperations.vue b/src/views/_sila/Operations/ServerPowerOperations/ServerPowerOperations.vue
new file mode 100644
index 00000000..9e030837
--- /dev/null
+++ b/src/views/_sila/Operations/ServerPowerOperations/ServerPowerOperations.vue
@@ -0,0 +1,260 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row class="mb-4">
+ <b-col md="8" xl="6">
+ <page-section
+ :section-title="$t('pageServerPowerOperations.currentStatus')"
+ >
+ <b-row>
+ <b-col>
+ <dl>
+ <dt>{{ $t('pageServerPowerOperations.serverStatus') }}</dt>
+ <dd
+ v-if="serverStatus === 'on'"
+ data-test-id="powerServerOps-text-hostStatus"
+ >
+ {{ $t('global.status.on') }}
+ </dd>
+ <dd
+ v-else-if="serverStatus === 'off'"
+ data-test-id="powerServerOps-text-hostStatus"
+ >
+ {{ $t('global.status.off') }}
+ </dd>
+ <dd v-else>
+ {{ $t('global.status.notAvailable') }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col>
+ <dl>
+ <dt>
+ {{ $t('pageServerPowerOperations.lastPowerOperation') }}
+ </dt>
+ <dd
+ v-if="lastPowerOperationTime"
+ data-test-id="powerServerOps-text-lastPowerOp"
+ >
+ {{ lastPowerOperationTime | formatDate }}
+ {{ lastPowerOperationTime | formatTime }}
+ </dd>
+ <dd v-else>--</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </page-section>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col v-if="hasBootSourceOptions" sm="8" md="6" xl="4">
+ <page-section
+ :section-title="$t('pageServerPowerOperations.serverBootSettings')"
+ >
+ <boot-settings />
+ </page-section>
+ </b-col>
+ <b-col sm="8" md="6" xl="7">
+ <page-section
+ :section-title="$t('pageServerPowerOperations.operations')"
+ >
+ <alert :show="oneTimeBootEnabled" variant="warning">
+ {{ $t('pageServerPowerOperations.oneTimeBootWarning') }}
+ </alert>
+ <template v-if="isOperationInProgress">
+ <alert variant="info">
+ {{ $t('pageServerPowerOperations.operationInProgress') }}
+ </alert>
+ </template>
+ <template v-else-if="serverStatus === 'off'">
+ <b-button
+ variant="primary"
+ data-test-id="serverPowerOperations-button-powerOn"
+ @click="powerOn"
+ >
+ {{ $t('pageServerPowerOperations.powerOn') }}
+ </b-button>
+ </template>
+ <template v-else>
+ <!-- Reboot server options -->
+ <b-form novalidate class="mb-5" @submit.prevent="rebootServer">
+ <b-form-group
+ :label="$t('pageServerPowerOperations.rebootServer')"
+ >
+ <b-form-radio
+ v-model="form.rebootOption"
+ name="reboot-option"
+ data-test-id="serverPowerOperations-radio-rebootOrderly"
+ value="orderly"
+ >
+ {{ $t('pageServerPowerOperations.orderlyReboot') }}
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.rebootOption"
+ name="reboot-option"
+ data-test-id="serverPowerOperations-radio-rebootImmediate"
+ value="immediate"
+ >
+ {{ $t('pageServerPowerOperations.immediateReboot') }}
+ </b-form-radio>
+ </b-form-group>
+ <b-button
+ variant="primary"
+ type="submit"
+ data-test-id="serverPowerOperations-button-reboot"
+ >
+ {{ $t('pageServerPowerOperations.reboot') }}
+ </b-button>
+ </b-form>
+ <!-- Shutdown server options -->
+ <b-form novalidate @submit.prevent="shutdownServer">
+ <b-form-group
+ :label="$t('pageServerPowerOperations.shutdownServer')"
+ >
+ <b-form-radio
+ v-model="form.shutdownOption"
+ name="shutdown-option"
+ data-test-id="serverPowerOperations-radio-shutdownOrderly"
+ value="orderly"
+ >
+ {{ $t('pageServerPowerOperations.orderlyShutdown') }}
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.shutdownOption"
+ name="shutdown-option"
+ data-test-id="serverPowerOperations-radio-shutdownImmediate"
+ value="immediate"
+ >
+ {{ $t('pageServerPowerOperations.immediateShutdown') }}
+ </b-form-radio>
+ </b-form-group>
+ <b-button
+ variant="primary"
+ type="submit"
+ data-test-id="serverPowerOperations-button-shutDown"
+ >
+ {{ $t('pageServerPowerOperations.shutDown') }}
+ </b-button>
+ </b-form>
+ </template>
+ </page-section>
+ </b-col>
+ </b-row>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import BootSettings from './BootSettings';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import Alert from '@/components/Global/Alert';
+
+export default {
+ name: 'ServerPowerOperations',
+ components: { PageTitle, PageSection, BootSettings, Alert },
+ mixins: [BVToastMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ form: {
+ rebootOption: 'orderly',
+ shutdownOption: 'orderly',
+ },
+ };
+ },
+ computed: {
+ serverStatus() {
+ return this.$store.getters['global/serverStatus'];
+ },
+ isOperationInProgress() {
+ return this.$store.getters['controls/isOperationInProgress'];
+ },
+ lastPowerOperationTime() {
+ return this.$store.getters['controls/lastPowerOperationTime'];
+ },
+ oneTimeBootEnabled() {
+ return this.$store.getters['serverBootSettings/overrideEnabled'];
+ },
+ hasBootSourceOptions() {
+ let bootOptions = this.$store.getters[
+ 'serverBootSettings/bootSourceOptions'
+ ];
+ return bootOptions.length !== 0;
+ },
+ },
+ created() {
+ this.startLoader();
+ const bootSettingsPromise = new Promise((resolve) => {
+ this.$root.$on('server-power-operations-boot-settings-complete', () =>
+ resolve()
+ );
+ });
+ Promise.all([
+ this.$store.dispatch('serverBootSettings/getBootSettings'),
+ this.$store.dispatch('controls/getLastPowerOperationTime'),
+ bootSettingsPromise,
+ ]).finally(() => this.endLoader());
+ },
+ methods: {
+ powerOn() {
+ this.$store.dispatch('controls/serverPowerOn');
+ },
+ rebootServer() {
+ const modalMessage = this.$t(
+ 'pageServerPowerOperations.modal.confirmRebootMessage'
+ );
+ const modalOptions = {
+ title: this.$t('pageServerPowerOperations.modal.confirmRebootTitle'),
+ okTitle: this.$t('global.action.confirm'),
+ cancelTitle: this.$t('global.action.cancel'),
+ };
+
+ if (this.form.rebootOption === 'orderly') {
+ this.$bvModal
+ .msgBoxConfirm(modalMessage, modalOptions)
+ .then((confirmed) => {
+ if (confirmed) this.$store.dispatch('controls/serverSoftReboot');
+ });
+ } else if (this.form.rebootOption === 'immediate') {
+ this.$bvModal
+ .msgBoxConfirm(modalMessage, modalOptions)
+ .then((confirmed) => {
+ if (confirmed) this.$store.dispatch('controls/serverHardReboot');
+ });
+ }
+ },
+ shutdownServer() {
+ const modalMessage = this.$t(
+ 'pageServerPowerOperations.modal.confirmShutdownMessage'
+ );
+ const modalOptions = {
+ title: this.$t('pageServerPowerOperations.modal.confirmShutdownTitle'),
+ okTitle: this.$t('global.action.confirm'),
+ cancelTitle: this.$t('global.action.cancel'),
+ };
+
+ if (this.form.shutdownOption === 'orderly') {
+ this.$bvModal
+ .msgBoxConfirm(modalMessage, modalOptions)
+ .then((confirmed) => {
+ if (confirmed) this.$store.dispatch('controls/serverSoftPowerOff');
+ });
+ }
+ if (this.form.shutdownOption === 'immediate') {
+ this.$bvModal
+ .msgBoxConfirm(modalMessage, modalOptions)
+ .then((confirmed) => {
+ if (confirmed) this.$store.dispatch('controls/serverHardPowerOff');
+ });
+ }
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/ServerPowerOperations/index.js b/src/views/_sila/Operations/ServerPowerOperations/index.js
new file mode 100644
index 00000000..10430047
--- /dev/null
+++ b/src/views/_sila/Operations/ServerPowerOperations/index.js
@@ -0,0 +1,2 @@
+import ServerPowerOperations from './ServerPowerOperations.vue';
+export default ServerPowerOperations;
diff --git a/src/views/_sila/Operations/VirtualMedia/ModalConfigureConnection.vue b/src/views/_sila/Operations/VirtualMedia/ModalConfigureConnection.vue
new file mode 100644
index 00000000..b0bcfb2b
--- /dev/null
+++ b/src/views/_sila/Operations/VirtualMedia/ModalConfigureConnection.vue
@@ -0,0 +1,145 @@
+<template>
+ <b-modal
+ id="configure-connection"
+ ref="modal"
+ @ok="onOk"
+ @hidden="resetForm"
+ @show="initModal"
+ >
+ <template #modal-title>
+ {{ $t('pageVirtualMedia.modal.title') }}
+ </template>
+ <b-form>
+ <b-form-group
+ :label="$t('pageVirtualMedia.modal.serverUri')"
+ label-for="serverUri"
+ >
+ <b-form-input
+ id="serverUri"
+ v-model="form.serverUri"
+ type="text"
+ :state="getValidationState($v.form.serverUri)"
+ data-test-id="configureConnection-input-serverUri"
+ @input="$v.form.serverUri.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.serverUri.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ <b-form-group
+ :label="$t('pageVirtualMedia.modal.username')"
+ label-for="username"
+ >
+ <b-form-input
+ id="username"
+ v-model="form.username"
+ type="text"
+ data-test-id="configureConnection-input-username"
+ />
+ </b-form-group>
+ <b-form-group
+ :label="$t('pageVirtualMedia.modal.password')"
+ label-for="password"
+ >
+ <b-form-input
+ id="password"
+ v-model="form.password"
+ type="password"
+ data-test-id="configureConnection-input-password"
+ />
+ </b-form-group>
+ <b-form-group>
+ <b-form-checkbox
+ v-model="form.isRW"
+ data-test-id="configureConnection-input-isRW"
+ name="check-button"
+ >
+ RW
+ </b-form-checkbox>
+ </b-form-group>
+ </b-form>
+ <template #modal-ok>
+ {{ $t('global.action.save') }}
+ </template>
+ <template #modal-cancel>
+ {{ $t('global.action.cancel') }}
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ connection: {
+ type: Object,
+ default: null,
+ validator: (prop) => {
+ console.log(prop);
+ return true;
+ },
+ },
+ },
+ data() {
+ return {
+ form: {
+ serverUri: null,
+ username: null,
+ password: null,
+ isRW: false,
+ },
+ };
+ },
+ watch: {
+ connection: function (value) {
+ if (value === null) return;
+ Object.assign(this.form, value);
+ },
+ },
+ validations() {
+ return {
+ form: {
+ serverUri: {
+ required,
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ let connectionData = {};
+ Object.assign(connectionData, this.form);
+ this.$emit('ok', connectionData);
+ this.closeModal();
+ },
+ initModal() {
+ if (this.connection) {
+ Object.assign(this.form, this.connection);
+ }
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.serverUri = null;
+ this.form.username = null;
+ this.form.password = null;
+ this.form.isRW = false;
+ this.$v.$reset();
+ },
+ onOk(bvModalEvt) {
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/VirtualMedia/VirtualMedia.vue b/src/views/_sila/Operations/VirtualMedia/VirtualMedia.vue
new file mode 100644
index 00000000..8a3d5add
--- /dev/null
+++ b/src/views/_sila/Operations/VirtualMedia/VirtualMedia.vue
@@ -0,0 +1,221 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row class="mb-4">
+ <b-col md="12">
+ <page-section
+ :section-title="$t('pageVirtualMedia.virtualMediaSubTitleFirst')"
+ >
+ <b-row>
+ <b-col v-for="(dev, $index) in proxyDevices" :key="$index" md="6">
+ <b-form-group :label="dev.id" label-class="bold">
+ <form-file
+ v-if="!dev.isActive"
+ :id="concatId(dev.id)"
+ v-model="dev.file"
+ >
+ <template #invalid>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.required') }}
+ </b-form-invalid-feedback>
+ </template>
+ </form-file>
+ </b-form-group>
+ <b-button
+ v-if="!dev.isActive"
+ variant="primary"
+ :disabled="!dev.file"
+ @click="startVM(dev)"
+ >
+ {{ $t('pageVirtualMedia.start') }}
+ </b-button>
+ <b-button
+ v-if="dev.isActive"
+ variant="primary"
+ :disabled="!dev.file"
+ @click="stopVM(dev)"
+ >
+ {{ $t('pageVirtualMedia.stop') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ </page-section>
+ </b-col>
+ </b-row>
+ <b-row v-if="loadImageFromExternalServer" class="mb-4">
+ <b-col md="12">
+ <page-section
+ :section-title="$t('pageVirtualMedia.virtualMediaSubTitleSecond')"
+ >
+ <b-row>
+ <b-col
+ v-for="(device, $index) in legacyDevices"
+ :key="$index"
+ md="6"
+ >
+ <b-form-group
+ :label="device.id"
+ :label-for="device.id"
+ label-class="bold"
+ >
+ <b-button
+ variant="primary"
+ :disabled="device.isActive"
+ @click="configureConnection(device)"
+ >
+ {{ $t('pageVirtualMedia.configureConnection') }}
+ </b-button>
+
+ <b-button
+ v-if="!device.isActive"
+ variant="primary"
+ class="float-right"
+ :disabled="!device.serverUri"
+ @click="startLegacy(device)"
+ >
+ {{ $t('pageVirtualMedia.start') }}
+ </b-button>
+ <b-button
+ v-if="device.isActive"
+ variant="primary"
+ class="float-right"
+ @click="stopLegacy(device)"
+ >
+ {{ $t('pageVirtualMedia.stop') }}
+ </b-button>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </page-section>
+ </b-col>
+ </b-row>
+ <modal-configure-connection
+ :connection="modalConfigureConnection"
+ @ok="saveConnection"
+ />
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import ModalConfigureConnection from './ModalConfigureConnection';
+import NbdServer from '@/utilities/NBDServer';
+import FormFile from '@/components/Global/FormFile';
+
+export default {
+ name: 'VirtualMedia',
+ components: { PageTitle, PageSection, ModalConfigureConnection, FormFile },
+ mixins: [BVToastMixin, LoadingBarMixin],
+ data() {
+ return {
+ modalConfigureConnection: null,
+ loadImageFromExternalServer:
+ process.env.VUE_APP_VIRTUAL_MEDIA_LIST_ENABLED === 'true'
+ ? true
+ : false,
+ };
+ },
+ computed: {
+ proxyDevices() {
+ return this.$store.getters['virtualMedia/proxyDevices'];
+ },
+ legacyDevices() {
+ return this.$store.getters['virtualMedia/legacyDevices'];
+ },
+ },
+ created() {
+ if (this.proxyDevices.length > 0 || this.legacyDevices.length > 0) return;
+ this.startLoader();
+ this.$store
+ .dispatch('virtualMedia/getData')
+ .finally(() => this.endLoader());
+ },
+ methods: {
+ startVM(device) {
+ const token = this.$store.getters['authentication/token'];
+ device.nbd = new NbdServer(
+ `wss://${window.location.host}${device.websocket}`,
+ device.file,
+ device.id,
+ token
+ );
+ device.nbd.socketStarted = () =>
+ this.successToast(this.$t('pageVirtualMedia.toast.serverRunning'));
+ device.nbd.errorReadingFile = () =>
+ this.errorToast(this.$t('pageVirtualMedia.toast.errorReadingFile'));
+ device.nbd.socketClosed = (code) => {
+ if (code === 1000)
+ this.successToast(
+ this.$t('pageVirtualMedia.toast.serverClosedSuccessfully')
+ );
+ else
+ this.errorToast(
+ this.$t('pageVirtualMedia.toast.serverClosedWithErrors')
+ );
+ device.file = null;
+ device.isActive = false;
+ };
+
+ device.nbd.start();
+ device.isActive = true;
+ },
+ stopVM(device) {
+ device.nbd.stop();
+ },
+ startLegacy(connectionData) {
+ var data = {};
+ data.Image = connectionData.serverUri;
+ data.UserName = connectionData.username;
+ data.Password = connectionData.password;
+ data.WriteProtected = !connectionData.isRW;
+ this.startLoader();
+ this.$store
+ .dispatch('virtualMedia/mountImage', {
+ id: connectionData.id,
+ data: data,
+ })
+ .then(() => {
+ this.successToast(
+ this.$t('pageVirtualMedia.toast.serverConnectionEstablished')
+ );
+ connectionData.isActive = true;
+ })
+ .catch(() => {
+ this.errorToast(this.$t('pageVirtualMedia.toast.errorMounting'));
+ this.isActive = false;
+ })
+ .finally(() => this.endLoader());
+ },
+ stopLegacy(connectionData) {
+ this.$store
+ .dispatch('virtualMedia/unmountImage', connectionData.id)
+ .then(() => {
+ this.successToast(
+ this.$t('pageVirtualMedia.toast.serverClosedSuccessfully')
+ );
+ connectionData.isActive = false;
+ })
+ .catch(() =>
+ this.errorToast(this.$t('pageVirtualMedia.toast.errorUnmounting'))
+ )
+ .finally(() => this.endLoader());
+ },
+ saveConnection(connectionData) {
+ this.modalConfigureConnection.serverUri = connectionData.serverUri;
+ this.modalConfigureConnection.username = connectionData.username;
+ this.modalConfigureConnection.password = connectionData.password;
+ this.modalConfigureConnection.isRW = connectionData.isRW;
+ },
+ configureConnection(connectionData) {
+ this.modalConfigureConnection = connectionData;
+ this.$bvModal.show('configure-connection');
+ },
+ concatId(val) {
+ return val.split(' ').join('_').toLowerCase();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Operations/VirtualMedia/index.js b/src/views/_sila/Operations/VirtualMedia/index.js
new file mode 100644
index 00000000..4573e865
--- /dev/null
+++ b/src/views/_sila/Operations/VirtualMedia/index.js
@@ -0,0 +1,2 @@
+import VirtualMedia from './VirtualMedia.vue';
+export default VirtualMedia;
diff --git a/src/views/_sila/Overview/Overview.vue b/src/views/_sila/Overview/Overview.vue
new file mode 100644
index 00000000..9960f373
--- /dev/null
+++ b/src/views/_sila/Overview/Overview.vue
@@ -0,0 +1,100 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <overview-quick-links class="mb-4" />
+ <page-section
+ :section-title="$t('pageOverview.systemInformation')"
+ class="mb-1"
+ >
+ <b-card-group deck>
+ <overview-server />
+ <overview-firmware />
+ </b-card-group>
+ <b-card-group deck>
+ <overview-network />
+ <overview-power />
+ </b-card-group>
+ </page-section>
+ <page-section :section-title="$t('pageOverview.statusInformation')">
+ <b-card-group deck>
+ <overview-events />
+ <overview-inventory />
+ <overview-dumps v-if="showDumps" />
+ </b-card-group>
+ </page-section>
+ </b-container>
+</template>
+
+<script>
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import OverviewDumps from './OverviewDumps.vue';
+import OverviewEvents from './OverviewEvents.vue';
+import OverviewFirmware from './OverviewFirmware.vue';
+import OverviewInventory from './OverviewInventory.vue';
+import OverviewNetwork from './OverviewNetwork';
+import OverviewPower from './OverviewPower';
+import OverviewQuickLinks from './OverviewQuickLinks';
+import OverviewServer from './OverviewServer';
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+
+export default {
+ name: 'Overview',
+ components: {
+ OverviewDumps,
+ OverviewEvents,
+ OverviewFirmware,
+ OverviewInventory,
+ OverviewNetwork,
+ OverviewPower,
+ OverviewQuickLinks,
+ OverviewServer,
+ PageSection,
+ PageTitle,
+ },
+ mixins: [LoadingBarMixin],
+ data() {
+ return {
+ showDumps: process.env.VUE_APP_ENV_NAME === 'ibm',
+ };
+ },
+ created() {
+ this.startLoader();
+ const dumpsPromise = new Promise((resolve) => {
+ this.$root.$on('overview-dumps-complete', () => resolve());
+ });
+ const eventsPromise = new Promise((resolve) => {
+ this.$root.$on('overview-events-complete', () => resolve());
+ });
+ const firmwarePromise = new Promise((resolve) => {
+ this.$root.$on('overview-firmware-complete', () => resolve());
+ });
+ const inventoryPromise = new Promise((resolve) => {
+ this.$root.$on('overview-inventory-complete', () => resolve());
+ });
+ const networkPromise = new Promise((resolve) => {
+ this.$root.$on('overview-network-complete', () => resolve());
+ });
+ const powerPromise = new Promise((resolve) => {
+ this.$root.$on('overview-power-complete', () => resolve());
+ });
+ const quicklinksPromise = new Promise((resolve) => {
+ this.$root.$on('overview-quicklinks-complete', () => resolve());
+ });
+ const serverPromise = new Promise((resolve) => {
+ this.$root.$on('overview-server-complete', () => resolve());
+ });
+
+ Promise.all([
+ dumpsPromise,
+ eventsPromise,
+ firmwarePromise,
+ inventoryPromise,
+ networkPromise,
+ powerPromise,
+ quicklinksPromise,
+ serverPromise,
+ ]).finally(() => this.endLoader());
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/OverviewCard.vue b/src/views/_sila/Overview/OverviewCard.vue
new file mode 100644
index 00000000..4fc0a031
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewCard.vue
@@ -0,0 +1,81 @@
+<template>
+ <b-card bg-variant="light" border-variant="light" class="mb-4">
+ <div class="justify-content-between align-items-center d-flex flex-wrap">
+ <h3 class="h5 mb-0">{{ title }}</h3>
+ <div class="card-buttons">
+ <b-button
+ v-if="exportButton || downloadButton"
+ :disabled="disabled"
+ :download="download"
+ :href="href"
+ class="p-0"
+ variant="link"
+ >
+ <span v-if="downloadButton">{{ $t('global.action.download') }}</span>
+ <span v-if="exportButton">{{ $t('global.action.exportAll') }}</span>
+ </b-button>
+ <span v-if="exportButton || downloadButton" class="pl-2 pr-2">|</span>
+ <b-link :to="to">{{ $t('pageOverview.viewMore') }}</b-link>
+ </div>
+ </div>
+ <slot></slot>
+ </b-card>
+</template>
+
+<script>
+export default {
+ name: 'OverviewCard',
+ props: {
+ data: {
+ type: Array,
+ default: () => [],
+ },
+ disabled: {
+ type: Boolean,
+ default: true,
+ },
+ downloadButton: {
+ type: Boolean,
+ default: false,
+ },
+ exportButton: {
+ type: Boolean,
+ default: false,
+ },
+
+ fileName: {
+ type: String,
+ default: 'data',
+ },
+ title: {
+ type: String,
+ default: '',
+ },
+ to: {
+ type: String,
+ default: '/',
+ },
+ },
+ computed: {
+ dataForExport() {
+ return JSON.stringify(this.data);
+ },
+ download() {
+ return `${this.fileName}.json`;
+ },
+ href() {
+ return `data:text/json;charset=utf-8,${this.dataForExport}`;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+a {
+ vertical-align: middle;
+ font-size: 14px;
+}
+.card {
+ min-width: 310px;
+}
+</style>
diff --git a/src/views/_sila/Overview/OverviewDumps.vue b/src/views/_sila/Overview/OverviewDumps.vue
new file mode 100644
index 00000000..a2ae4e4e
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewDumps.vue
@@ -0,0 +1,54 @@
+<template>
+ <overview-card
+ :data="dumps"
+ :disabled="dumps.length === 0"
+ :download-button="true"
+ :file-name="exportFileNameByDate()"
+ :title="$t('pageOverview.dumps')"
+ :to="`/logs/dumps`"
+ >
+ <b-row class="mt-3">
+ <b-col sm="6">
+ <dl>
+ <dt>{{ $t('pageOverview.total') }}</dt>
+ <dd class="h3">{{ dataFormatter(dumps.length) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ name: 'Dumps',
+ components: {
+ OverviewCard,
+ },
+ mixins: [DataFormatterMixin],
+ computed: {
+ dumps() {
+ return this.$store.getters['dumps/allDumps'];
+ },
+ },
+ created() {
+ this.$store.dispatch('dumps/getBmcDumpEntries').finally(() => {
+ this.$root.$emit('overview-dumps-complete');
+ });
+ },
+ methods: {
+ exportFileNameByDate() {
+ // Create export file name based on date
+ let date = new Date();
+ date =
+ date.toISOString().slice(0, 10) +
+ '_' +
+ date.toString().split(':').join('-').split(' ')[4];
+ let fileName = 'all_dumps_';
+ return fileName + date;
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/OverviewEvents.vue b/src/views/_sila/Overview/OverviewEvents.vue
new file mode 100644
index 00000000..b73c0b48
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewEvents.vue
@@ -0,0 +1,85 @@
+<template>
+ <overview-card
+ :data="eventLogData"
+ :disabled="eventLogData.length === 0"
+ :export-button="true"
+ :file-name="exportFileNameByDate()"
+ :title="$t('pageOverview.eventLogs')"
+ :to="`/logs/event-logs`"
+ >
+ <b-row class="mt-3">
+ <b-col sm="6">
+ <dl>
+ <dt>{{ $t('pageOverview.criticalEvents') }}</dt>
+ <dd class="h3">
+ {{ dataFormatter(criticalEvents.length) }}
+ <status-icon status="danger" />
+ </dd>
+ </dl>
+ </b-col>
+ <b-col sm="6">
+ <dl>
+ <dt>{{ $t('pageOverview.warningEvents') }}</dt>
+ <dd class="h3">
+ {{ dataFormatter(warningEvents.length) }}
+ <status-icon status="warning" />
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+import StatusIcon from '@/components/Global/StatusIcon';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ name: 'Events',
+ components: { OverviewCard, StatusIcon },
+ mixins: [DataFormatterMixin],
+ computed: {
+ eventLogData() {
+ return this.$store.getters['eventLog/allEvents'];
+ },
+ criticalEvents() {
+ return this.eventLogData
+ .filter((log) => log.severity === 'Critical')
+ .map((log) => {
+ return log;
+ });
+ },
+ warningEvents() {
+ return this.eventLogData
+ .filter((log) => log.severity === 'Warning')
+ .map((log) => {
+ return log;
+ });
+ },
+ },
+ created() {
+ this.$store.dispatch('eventLog/getEventLogData').finally(() => {
+ this.$root.$emit('overview-events-complete');
+ });
+ },
+ methods: {
+ exportFileNameByDate() {
+ // Create export file name based on date
+ let date = new Date();
+ date =
+ date.toISOString().slice(0, 10) +
+ '_' +
+ date.toString().split(':').join('-').split(' ')[4];
+ let fileName = 'all_event_logs_';
+ return fileName + date;
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.status-icon {
+ vertical-align: text-top;
+}
+</style>
diff --git a/src/views/_sila/Overview/OverviewFirmware.vue b/src/views/_sila/Overview/OverviewFirmware.vue
new file mode 100644
index 00000000..f1f9ce53
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewFirmware.vue
@@ -0,0 +1,49 @@
+<template>
+ <overview-card
+ :title="$t('pageOverview.firmwareInformation')"
+ :to="`/operations/firmware`"
+ >
+ <b-row class="mt-3">
+ <b-col>
+ <dl>
+ <dt>{{ $t('pageOverview.runningVersion') }}</dt>
+ <dd>{{ dataFormatter(runningVersion) }}</dd>
+ <dt>{{ $t('pageOverview.backupVersion') }}</dt>
+ <dd>{{ dataFormatter(backupVersion) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ name: 'Firmware',
+ components: {
+ OverviewCard,
+ },
+ mixins: [DataFormatterMixin],
+ computed: {
+ backupBmcFirmware() {
+ return this.$store.getters['firmware/backupBmcFirmware'];
+ },
+ backupVersion() {
+ return this.backupBmcFirmware?.version;
+ },
+ activeBmcFirmware() {
+ return this.$store.getters[`firmware/activeBmcFirmware`];
+ },
+ runningVersion() {
+ return this.activeBmcFirmware?.version;
+ },
+ },
+ created() {
+ this.$store.dispatch('firmware/getFirmwareInformation').finally(() => {
+ this.$root.$emit('overview-firmware-complete');
+ });
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/OverviewInventory.vue b/src/views/_sila/Overview/OverviewInventory.vue
new file mode 100644
index 00000000..575cb7b7
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewInventory.vue
@@ -0,0 +1,57 @@
+<template>
+ <overview-card
+ :title="$t('pageOverview.inventory')"
+ :to="`/hardware-status/inventory`"
+ >
+ <b-row class="mt-3">
+ <b-col sm="6">
+ <dl sm="6">
+ <dt>{{ $t('pageOverview.systemIdentifyLed') }}</dt>
+ <dd>
+ <b-form-checkbox
+ id="identifyLedSwitch"
+ v-model="systems.locationIndicatorActive"
+ data-test-id="overviewInventory-checkbox-identifyLed"
+ switch
+ @change="toggleIdentifyLedSwitch"
+ >
+ <span v-if="systems.locationIndicatorActive">
+ {{ $t('global.status.on') }}
+ </span>
+ <span v-else>{{ $t('global.status.off') }}</span>
+ </b-form-checkbox>
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+
+export default {
+ name: 'Inventory',
+ components: {
+ OverviewCard,
+ },
+ computed: {
+ systems() {
+ let systemData = this.$store.getters['system/systems'][0];
+ return systemData ? systemData : {};
+ },
+ },
+ created() {
+ this.$store.dispatch('system/getSystem').finally(() => {
+ this.$root.$emit('overview-inventory-complete');
+ });
+ },
+ methods: {
+ toggleIdentifyLedSwitch(state) {
+ this.$store
+ .dispatch('system/changeIdentifyLedState', state)
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/OverviewNetwork.vue b/src/views/_sila/Overview/OverviewNetwork.vue
new file mode 100644
index 00000000..b81e5c73
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewNetwork.vue
@@ -0,0 +1,71 @@
+<template>
+ <overview-card
+ v-if="network"
+ :title="$t('pageOverview.networkInformation')"
+ :to="`/settings/network`"
+ >
+ <b-row class="mt-3">
+ <b-col sm="6">
+ <dl>
+ <dt>{{ $t('pageOverview.hostName') }}</dt>
+ <dd>{{ dataFormatter(network.hostname) }}</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6">
+ <dl>
+ <dt>{{ $t('pageOverview.linkStatus') }}</dt>
+ <dd>
+ {{ dataFormatter(network.linkStatus) }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col>
+ <dl>
+ <dt>{{ $t('pageOverview.ipv4') }}</dt>
+ <dd>
+ {{ dataFormatter(network.staticAddress) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col>
+ <dl>
+ <dt>{{ $t('pageOverview.dhcp') }}</dt>
+ <dd>
+ {{
+ dataFormatter(
+ network.dhcpAddress.length !== 0
+ ? network.dhcpAddress[0].Address
+ : null
+ )
+ }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+
+export default {
+ name: 'Network',
+ components: {
+ OverviewCard,
+ },
+ mixins: [DataFormatterMixin],
+ computed: {
+ network() {
+ return this.$store.getters['network/globalNetworkSettings'][0];
+ },
+ },
+ created() {
+ this.$store.dispatch('network/getEthernetData').finally(() => {
+ this.$root.$emit('overview-network-complete');
+ });
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/OverviewPower.vue b/src/views/_sila/Overview/OverviewPower.vue
new file mode 100644
index 00000000..0d84c76c
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewPower.vue
@@ -0,0 +1,48 @@
+<template>
+ <overview-card
+ :title="$t('pageOverview.powerInformation')"
+ :to="`/resource-management/power`"
+ >
+ <b-row class="mt-3">
+ <b-col sm="6">
+ <dl>
+ <dt>{{ $t('pageOverview.powerConsumption') }}</dt>
+ <dd v-if="powerConsumptionValue == null">
+ {{ $t('global.status.notAvailable') }}
+ </dd>
+ <dd v-else>{{ powerConsumptionValue }} W</dd>
+ <dt>{{ $t('pageOverview.powerCap') }}</dt>
+ <dd v-if="powerCapValue == null">
+ {{ $t('global.status.disabled') }}
+ </dd>
+ <dd v-else>{{ powerCapValue }} W</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import { mapGetters } from 'vuex';
+
+export default {
+ name: 'Power',
+ components: {
+ OverviewCard,
+ },
+ mixins: [DataFormatterMixin],
+ computed: {
+ ...mapGetters({
+ powerCapValue: 'powerControl/powerCapValue',
+ powerConsumptionValue: 'powerControl/powerConsumptionValue',
+ }),
+ },
+ created() {
+ this.$store.dispatch('powerControl/getPowerControl').finally(() => {
+ this.$root.$emit('overview-power-complete');
+ });
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/OverviewQuickLinks.vue b/src/views/_sila/Overview/OverviewQuickLinks.vue
new file mode 100644
index 00000000..bc579b03
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewQuickLinks.vue
@@ -0,0 +1,56 @@
+<template>
+ <b-card bg-variant="light" border-variant="light">
+ <b-row class="d-flex justify-content-between align-items-center">
+ <b-col sm="6" lg="9" class="mb-2 mt-2">
+ <dl>
+ <dt>{{ $t('pageOverview.bmcTime') }}</dt>
+ <dd v-if="bmcTime" data-test-id="overviewQuickLinks-text-bmcTime">
+ {{ bmcTime | formatDate }} {{ bmcTime | formatTime }}
+ </dd>
+ <dd v-else>--</dd>
+ </dl>
+ </b-col>
+ <b-col sm="6" lg="3" class="mb-2 mt-2">
+ <b-button
+ to="/operations/serial-over-lan"
+ variant="secondary"
+ data-test-id="overviewQuickLinks-button-solConsole"
+ class="d-flex justify-content-between align-items-center"
+ >
+ {{ $t('pageOverview.solConsole') }}
+ <icon-arrow-right />
+ </b-button>
+ </b-col>
+ </b-row>
+ </b-card>
+</template>
+
+<script>
+import ArrowRight16 from '@carbon/icons-vue/es/arrow--right/16';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+export default {
+ name: 'QuickLinks',
+ components: {
+ IconArrowRight: ArrowRight16,
+ },
+ mixins: [BVToastMixin],
+ computed: {
+ bmcTime() {
+ return this.$store.getters['global/bmcTime'];
+ },
+ },
+ created() {
+ Promise.all([this.$store.dispatch('global/getBmcTime')]).finally(() => {
+ this.$root.$emit('overview-quicklinks-complete');
+ });
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+dd,
+dl {
+ margin: 0;
+}
+</style>
diff --git a/src/views/_sila/Overview/OverviewServer.vue b/src/views/_sila/Overview/OverviewServer.vue
new file mode 100644
index 00000000..d066d391
--- /dev/null
+++ b/src/views/_sila/Overview/OverviewServer.vue
@@ -0,0 +1,47 @@
+<template>
+ <overview-card
+ :title="$t('pageOverview.serverInformation')"
+ :to="`/hardware-status/inventory`"
+ >
+ <b-row class="mt-3">
+ <b-col lg="6">
+ <dl>
+ <dt>{{ $t('pageOverview.model') }}</dt>
+ <dd>{{ dataFormatter(serverModel) }}</dd>
+ <dt>{{ $t('pageOverview.serialNumber') }}</dt>
+ <dd>{{ dataFormatter(serverSerialNumber) }}</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </overview-card>
+</template>
+
+<script>
+import OverviewCard from './OverviewCard';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'Server',
+ components: {
+ OverviewCard,
+ },
+ mixins: [DataFormatterMixin],
+ computed: {
+ ...mapState({
+ server: (state) => state.system.systems[0],
+ serverModel() {
+ return this.server?.model;
+ },
+ serverSerialNumber() {
+ return this.server?.serialNumber;
+ },
+ }),
+ },
+ created() {
+ this.$store.dispatch('system/getSystem').finally(() => {
+ this.$root.$emit('overview-server-complete');
+ });
+ },
+};
+</script>
diff --git a/src/views/_sila/Overview/index.js b/src/views/_sila/Overview/index.js
new file mode 100644
index 00000000..8553ef3d
--- /dev/null
+++ b/src/views/_sila/Overview/index.js
@@ -0,0 +1,2 @@
+import Overview from './Overview.vue';
+export default Overview;
diff --git a/src/views/_sila/PageNotFound/PageNotFound.vue b/src/views/_sila/PageNotFound/PageNotFound.vue
new file mode 100644
index 00000000..91341dbb
--- /dev/null
+++ b/src/views/_sila/PageNotFound/PageNotFound.vue
@@ -0,0 +1,12 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pagePageNotFound.description')" />
+ </b-container>
+</template>
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+export default {
+ name: 'PageNotFound',
+ components: { PageTitle },
+};
+</script>
diff --git a/src/views/_sila/PageNotFound/index.js b/src/views/_sila/PageNotFound/index.js
new file mode 100644
index 00000000..ed1d519a
--- /dev/null
+++ b/src/views/_sila/PageNotFound/index.js
@@ -0,0 +1,2 @@
+import PageNotFound from './PageNotFound.vue';
+export default PageNotFound;
diff --git a/src/views/_sila/ProfileSettings/ProfileSettings.vue b/src/views/_sila/ProfileSettings/ProfileSettings.vue
new file mode 100644
index 00000000..8f01c59b
--- /dev/null
+++ b/src/views/_sila/ProfileSettings/ProfileSettings.vue
@@ -0,0 +1,222 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+
+ <b-row>
+ <b-col md="8" lg="8" xl="6">
+ <page-section
+ :section-title="$t('pageProfileSettings.profileInfoTitle')"
+ >
+ <dl>
+ <dt>{{ $t('pageProfileSettings.username') }}</dt>
+ <dd>
+ {{ username }}
+ </dd>
+ </dl>
+ </page-section>
+ </b-col>
+ </b-row>
+
+ <b-form @submit.prevent="submitForm">
+ <b-row>
+ <b-col sm="8" md="6" xl="3">
+ <page-section
+ :section-title="$t('pageProfileSettings.changePassword')"
+ >
+ <b-form-group
+ id="input-group-1"
+ :label="$t('pageProfileSettings.newPassword')"
+ label-for="input-1"
+ >
+ <b-form-text id="password-help-block">
+ {{
+ $t('pageUserManagement.modal.passwordMustBeBetween', {
+ min: passwordRequirements.minLength,
+ max: passwordRequirements.maxLength,
+ })
+ }}
+ </b-form-text>
+ <input-password-toggle>
+ <b-form-input
+ id="password"
+ v-model="form.newPassword"
+ type="password"
+ aria-describedby="password-help-block"
+ :state="getValidationState($v.form.newPassword)"
+ data-test-id="profileSettings-input-newPassword"
+ class="form-control-with-button"
+ @input="$v.form.newPassword.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template
+ v-if="
+ !$v.form.newPassword.minLength ||
+ !$v.form.newPassword.maxLength
+ "
+ >
+ {{
+ $t('pageProfileSettings.newPassLabelTextInfo', {
+ min: passwordRequirements.minLength,
+ max: passwordRequirements.maxLength,
+ })
+ }}
+ </template>
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ <b-form-group
+ id="input-group-2"
+ :label="$t('pageProfileSettings.confirmPassword')"
+ label-for="input-2"
+ >
+ <input-password-toggle>
+ <b-form-input
+ id="password-confirmation"
+ v-model="form.confirmPassword"
+ type="password"
+ :state="getValidationState($v.form.confirmPassword)"
+ data-test-id="profileSettings-input-confirmPassword"
+ class="form-control-with-button"
+ @input="$v.form.confirmPassword.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.confirmPassword.sameAsPassword">
+ {{ $t('pageProfileSettings.passwordsDoNotMatch') }}
+ </template>
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ </page-section>
+ </b-col>
+ </b-row>
+ <page-section :section-title="$t('pageProfileSettings.timezoneDisplay')">
+ <p>{{ $t('pageProfileSettings.timezoneDisplayDesc') }}</p>
+ <b-row>
+ <b-col md="9" lg="8" xl="9">
+ <b-form-group :label="$t('pageProfileSettings.timezone')">
+ <b-form-radio
+ v-model="form.isUtcDisplay"
+ :value="true"
+ data-test-id="profileSettings-radio-defaultUTC"
+ >
+ {{ $t('pageProfileSettings.defaultUTC') }}
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.isUtcDisplay"
+ :value="false"
+ data-test-id="profileSettings-radio-browserOffset"
+ >
+ {{
+ $t('pageProfileSettings.browserOffset', {
+ timezone,
+ })
+ }}
+ </b-form-radio>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </page-section>
+ <b-button
+ variant="primary"
+ type="submit"
+ data-test-id="profileSettings-button-saveSettings"
+ >
+ {{ $t('global.action.saveSettings') }}
+ </b-button>
+ </b-form>
+ </b-container>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+import { maxLength, minLength, sameAs } from 'vuelidate/lib/validators';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import LocalTimezoneLabelMixin from '@/components/Mixins/LocalTimezoneLabelMixin';
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+ name: 'ProfileSettings',
+ components: { InputPasswordToggle, PageSection, PageTitle },
+ mixins: [
+ BVToastMixin,
+ LocalTimezoneLabelMixin,
+ LoadingBarMixin,
+ VuelidateMixin,
+ ],
+ data() {
+ return {
+ form: {
+ newPassword: '',
+ confirmPassword: '',
+ isUtcDisplay: this.$store.getters['global/isUtcDisplay'],
+ },
+ };
+ },
+ computed: {
+ username() {
+ return this.$store.getters['global/username'];
+ },
+ passwordRequirements() {
+ return this.$store.getters['userManagement/accountPasswordRequirements'];
+ },
+ timezone() {
+ return this.localOffset();
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store
+ .dispatch('userManagement/getAccountSettings')
+ .finally(() => this.endLoader());
+ },
+ validations() {
+ return {
+ form: {
+ newPassword: {
+ minLength: minLength(this.passwordRequirements.minLength),
+ maxLength: maxLength(this.passwordRequirements.maxLength),
+ },
+ confirmPassword: {
+ sameAsPassword: sameAs('newPassword'),
+ },
+ },
+ };
+ },
+ methods: {
+ saveNewPasswordInputData() {
+ this.$v.form.confirmPassword.$touch();
+ this.$v.form.newPassword.$touch();
+ if (this.$v.$invalid) return;
+ let userData = {
+ originalUsername: this.username,
+ password: this.form.newPassword,
+ };
+
+ this.$store
+ .dispatch('userManagement/updateUser', userData)
+ .then((message) => {
+ (this.form.newPassword = ''), (this.form.confirmPassword = '');
+ this.$v.$reset();
+ this.successToast(message);
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ saveTimeZonePrefrenceData() {
+ localStorage.setItem('storedUtcDisplay', this.form.isUtcDisplay);
+ this.$store.commit('global/setUtcTime', this.form.isUtcDisplay);
+ this.successToast(
+ this.$t('pageProfileSettings.toast.successUpdatingTimeZone')
+ );
+ },
+ submitForm() {
+ if (this.form.confirmPassword || this.form.newPassword) {
+ this.saveNewPasswordInputData();
+ }
+ this.saveTimeZonePrefrenceData();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/ProfileSettings/index.js b/src/views/_sila/ProfileSettings/index.js
new file mode 100644
index 00000000..d6589c72
--- /dev/null
+++ b/src/views/_sila/ProfileSettings/index.js
@@ -0,0 +1,2 @@
+import ProfileSettings from './ProfileSettings.vue';
+export default ProfileSettings;
diff --git a/src/views/_sila/ResourceManagement/Power.vue b/src/views/_sila/ResourceManagement/Power.vue
new file mode 100644
index 00000000..cc0cc993
--- /dev/null
+++ b/src/views/_sila/ResourceManagement/Power.vue
@@ -0,0 +1,170 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pagePower.description')" />
+
+ <b-row>
+ <b-col sm="8" md="6" xl="12">
+ <dl>
+ <dt>{{ $t('pagePower.powerConsumption') }}</dt>
+ <dd>
+ {{
+ powerConsumptionValue
+ ? `${powerConsumptionValue} W`
+ : $t('global.status.notAvailable')
+ }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+
+ <b-form @submit.prevent="submitForm">
+ <b-form-group :disabled="loading">
+ <b-row>
+ <b-col sm="8" md="6" xl="12">
+ <b-form-group :label="$t('pagePower.powerCapSettingLabel')">
+ <b-form-checkbox
+ v-model="isPowerCapFieldEnabled"
+ data-test-id="power-checkbox-togglePowerCapField"
+ name="power-cap-setting"
+ >
+ {{ $t('pagePower.powerCapSettingData') }}
+ </b-form-checkbox>
+ </b-form-group>
+ </b-col>
+ </b-row>
+
+ <b-row>
+ <b-col sm="8" md="6" xl="3">
+ <b-form-group
+ id="input-group-1"
+ :label="$t('pagePower.powerCapLabel')"
+ label-for="input-1"
+ >
+ <b-form-text id="power-help-text">
+ {{
+ $t('pagePower.powerCapLabelTextInfo', {
+ min: 1,
+ max: 10000,
+ })
+ }}
+ </b-form-text>
+
+ <b-form-input
+ id="input-1"
+ v-model.number="powerCapValue"
+ :disabled="!isPowerCapFieldEnabled"
+ data-test-id="power-input-powerCapValue"
+ type="number"
+ aria-describedby="power-help-text"
+ :state="getValidationState($v.powerCapValue)"
+ ></b-form-input>
+
+ <b-form-invalid-feedback id="input-live-feedback" role="alert">
+ <template v-if="!$v.powerCapValue.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-else-if="!$v.powerCapValue.between">
+ {{ $t('global.form.invalidValue') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+
+ <b-button
+ variant="primary"
+ type="submit"
+ data-test-id="power-button-savePowerCapValue"
+ >
+ {{ $t('global.action.save') }}
+ </b-button>
+ </b-form-group>
+ </b-form>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import { requiredIf, between } from 'vuelidate/lib/validators';
+import { mapGetters } from 'vuex';
+
+export default {
+ name: 'Power',
+ components: { PageTitle },
+ mixins: [VuelidateMixin, BVToastMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ loading,
+ };
+ },
+ computed: {
+ ...mapGetters({
+ powerConsumptionValue: 'powerControl/powerConsumptionValue',
+ }),
+
+ /**
+ Computed property isPowerCapFieldEnabled is used to enable or disable the input field.
+ The input field is enabled when the powercapValue property is not null.
+ **/
+ isPowerCapFieldEnabled: {
+ get() {
+ return this.powerCapValue !== null;
+ },
+ set(value) {
+ this.$v.$reset();
+ let newValue = null;
+ if (value) {
+ if (this.powerCapValue) {
+ newValue = this.powerCapValue;
+ } else {
+ newValue = '';
+ }
+ }
+ this.$store.dispatch('powerControl/setPowerCapUpdatedValue', newValue);
+ },
+ },
+ powerCapValue: {
+ get() {
+ return this.$store.getters['powerControl/powerCapValue'];
+ },
+ set(value) {
+ this.$v.$touch();
+ this.$store.dispatch('powerControl/setPowerCapUpdatedValue', value);
+ },
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store
+ .dispatch('powerControl/getPowerControl')
+ .finally(() => this.endLoader());
+ },
+ validations: {
+ powerCapValue: {
+ between: between(1, 10000),
+ required: requiredIf(function () {
+ return this.isPowerCapFieldEnabled;
+ }),
+ },
+ },
+ methods: {
+ submitForm() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.startLoader();
+ this.$store
+ .dispatch('powerControl/setPowerControl', this.powerCapValue)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/ResourceManagement/index.js b/src/views/_sila/ResourceManagement/index.js
new file mode 100644
index 00000000..5882decd
--- /dev/null
+++ b/src/views/_sila/ResourceManagement/index.js
@@ -0,0 +1,2 @@
+import Power from './Power.vue';
+export default Power;
diff --git a/src/views/_sila/SecurityAndAccess/Certificates/Certificates.vue b/src/views/_sila/SecurityAndAccess/Certificates/Certificates.vue
new file mode 100644
index 00000000..0113b80a
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Certificates/Certificates.vue
@@ -0,0 +1,322 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row>
+ <b-col xl="11">
+ <!-- Expired certificates banner -->
+ <alert :show="expiredCertificateTypes.length > 0" variant="danger">
+ <template v-if="expiredCertificateTypes.length > 1">
+ {{ $t('pageCertificates.alert.certificatesExpiredMessage') }}
+ </template>
+ <template v-else>
+ {{
+ $t('pageCertificates.alert.certificateExpiredMessage', {
+ certificate: expiredCertificateTypes[0],
+ })
+ }}
+ </template>
+ </alert>
+ <!-- Expiring certificates banner -->
+ <alert :show="expiringCertificateTypes.length > 0" variant="warning">
+ <template v-if="expiringCertificateTypes.length > 1">
+ {{ $t('pageCertificates.alert.certificatesExpiringMessage') }}
+ </template>
+ <template v-else>
+ {{
+ $t('pageCertificates.alert.certificateExpiringMessage', {
+ certificate: expiringCertificateTypes[0],
+ })
+ }}
+ </template>
+ </alert>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="11" class="text-right">
+ <b-button
+ v-b-modal.generate-csr
+ data-test-id="certificates-button-generateCsr"
+ variant="link"
+ >
+ <icon-add />
+ {{ $t('pageCertificates.generateCsr') }}
+ </b-button>
+ <b-button
+ variant="primary"
+ :disabled="certificatesForUpload.length === 0"
+ @click="initModalUploadCertificate(null)"
+ >
+ <icon-add />
+ {{ $t('pageCertificates.addNewCertificate') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="11">
+ <b-table
+ responsive="md"
+ show-empty
+ hover
+ :busy="isBusy"
+ :fields="fields"
+ :items="tableItems"
+ :empty-text="$t('global.table.emptyMessage')"
+ >
+ <template #cell(validFrom)="{ value }">
+ {{ value | formatDate }}
+ </template>
+
+ <template #cell(validUntil)="{ value }">
+ <status-icon
+ v-if="getDaysUntilExpired(value) < 31"
+ :status="getIconStatus(value)"
+ />
+ {{ value | formatDate }}
+ </template>
+
+ <template #cell(actions)="{ value, item }">
+ <table-row-action
+ v-for="(action, index) in value"
+ :key="index"
+ :value="action.value"
+ :title="action.title"
+ :enabled="action.enabled"
+ @click-table-action="onTableRowAction($event, item)"
+ >
+ <template #icon>
+ <icon-replace v-if="action.value === 'replace'" />
+ <icon-trashcan v-if="action.value === 'delete'" />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </b-col>
+ </b-row>
+
+ <!-- Modals -->
+ <modal-upload-certificate :certificate="modalCertificate" @ok="onModalOk" />
+ <modal-generate-csr />
+ </b-container>
+</template>
+
+<script>
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconReplace from '@carbon/icons-vue/es/renew/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+
+import ModalGenerateCsr from './ModalGenerateCsr';
+import ModalUploadCertificate from './ModalUploadCertificate';
+import PageTitle from '@/components/Global/PageTitle';
+import TableRowAction from '@/components/Global/TableRowAction';
+import StatusIcon from '@/components/Global/StatusIcon';
+import Alert from '@/components/Global/Alert';
+
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+
+export default {
+ name: 'Certificates',
+ components: {
+ Alert,
+ IconAdd,
+ IconReplace,
+ IconTrashcan,
+ ModalGenerateCsr,
+ ModalUploadCertificate,
+ PageTitle,
+ StatusIcon,
+ TableRowAction,
+ },
+ mixins: [BVToastMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ isBusy: true,
+ modalCertificate: null,
+ fields: [
+ {
+ key: 'certificate',
+ label: this.$t('pageCertificates.table.certificate'),
+ },
+ {
+ key: 'issuedBy',
+ label: this.$t('pageCertificates.table.issuedBy'),
+ },
+ {
+ key: 'issuedTo',
+ label: this.$t('pageCertificates.table.issuedTo'),
+ },
+ {
+ key: 'validFrom',
+ label: this.$t('pageCertificates.table.validFrom'),
+ },
+ {
+ key: 'validUntil',
+ label: this.$t('pageCertificates.table.validUntil'),
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'text-right text-nowrap',
+ },
+ ],
+ };
+ },
+ computed: {
+ certificates() {
+ return this.$store.getters['certificates/allCertificates'];
+ },
+ tableItems() {
+ return this.certificates.map((certificate) => {
+ return {
+ ...certificate,
+ actions: [
+ {
+ value: 'replace',
+ title: this.$t('pageCertificates.replaceCertificate'),
+ },
+ {
+ value: 'delete',
+ title: this.$t('pageCertificates.deleteCertificate'),
+ enabled:
+ certificate.type === 'TrustStore Certificate' ? true : false,
+ },
+ ],
+ };
+ });
+ },
+ certificatesForUpload() {
+ return this.$store.getters['certificates/availableUploadTypes'];
+ },
+ bmcTime() {
+ return this.$store.getters['global/bmcTime'];
+ },
+ expiredCertificateTypes() {
+ return this.certificates.reduce((acc, val) => {
+ const daysUntilExpired = this.getDaysUntilExpired(val.validUntil);
+ if (daysUntilExpired < 1) {
+ acc.push(val.certificate);
+ }
+ return acc;
+ }, []);
+ },
+ expiringCertificateTypes() {
+ return this.certificates.reduce((acc, val) => {
+ const daysUntilExpired = this.getDaysUntilExpired(val.validUntil);
+ if (daysUntilExpired < 31 && daysUntilExpired > 0) {
+ acc.push(val.certificate);
+ }
+ return acc;
+ }, []);
+ },
+ },
+ async created() {
+ this.startLoader();
+ await this.$store.dispatch('global/getBmcTime');
+ this.$store.dispatch('certificates/getCertificates').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ onTableRowAction(event, rowItem) {
+ switch (event) {
+ case 'replace':
+ this.initModalUploadCertificate(rowItem);
+ break;
+ case 'delete':
+ this.initModalDeleteCertificate(rowItem);
+ break;
+ default:
+ break;
+ }
+ },
+ initModalUploadCertificate(certificate = null) {
+ this.modalCertificate = certificate;
+ this.$bvModal.show('upload-certificate');
+ },
+ initModalDeleteCertificate(certificate) {
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$t('pageCertificates.modal.deleteConfirmMessage', {
+ issuedBy: certificate.issuedBy,
+ certificate: certificate.certificate,
+ }),
+ {
+ title: this.$t('pageCertificates.deleteCertificate'),
+ okTitle: this.$t('global.action.delete'),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) this.deleteCertificate(certificate);
+ });
+ },
+ onModalOk({ addNew, file, type, location }) {
+ if (addNew) {
+ // Upload a new certificate
+ this.addNewCertificate(file, type);
+ } else {
+ // Replace an existing certificate
+ this.replaceCertificate(file, type, location);
+ }
+ },
+ addNewCertificate(file, type) {
+ this.startLoader();
+ this.$store
+ .dispatch('certificates/addNewCertificate', { file, type })
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ replaceCertificate(file, type, location) {
+ this.startLoader();
+ const reader = new FileReader();
+ reader.readAsBinaryString(file);
+ reader.onloadend = (event) => {
+ const certificateString = event.target.result;
+ this.$store
+ .dispatch('certificates/replaceCertificate', {
+ certificateString,
+ type,
+ location,
+ })
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ };
+ },
+ deleteCertificate({ type, location }) {
+ this.startLoader();
+ this.$store
+ .dispatch('certificates/deleteCertificate', {
+ type,
+ location,
+ })
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ getDaysUntilExpired(date) {
+ if (this.bmcTime) {
+ const validUntilMs = date.getTime();
+ const currentBmcTimeMs = this.bmcTime.getTime();
+ const oneDayInMs = 24 * 60 * 60 * 1000;
+ return Math.round((validUntilMs - currentBmcTimeMs) / oneDayInMs);
+ }
+ return new Date();
+ },
+ getIconStatus(date) {
+ const daysUntilExpired = this.getDaysUntilExpired(date);
+ if (daysUntilExpired < 1) {
+ return 'danger';
+ } else if (daysUntilExpired < 31) {
+ return 'warning';
+ }
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/Certificates/CsrCountryCodes.js b/src/views/_sila/SecurityAndAccess/Certificates/CsrCountryCodes.js
new file mode 100644
index 00000000..a2d70007
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Certificates/CsrCountryCodes.js
@@ -0,0 +1,345 @@
+import i18n from '@/i18n';
+
+export const COUNTRY_LIST = [
+ { name: 'Afghanistan', code: 'AF', label: i18n.t('countries.AF') },
+ { name: 'Albania', code: 'AL', label: i18n.t('countries.AL') },
+ { name: 'Algeria', code: 'DZ', label: i18n.t('countries.DZ') },
+ { name: 'American Samoa', code: 'AS', label: i18n.t('countries.AS') },
+ { name: 'Andorra', code: 'AD', label: i18n.t('countries.AD') },
+ { name: 'Angola', code: 'AO', label: i18n.t('countries.AO') },
+ { name: 'Anguilla', code: 'AI', label: i18n.t('countries.AI') },
+ { name: 'Antarctica', code: 'AQ', label: i18n.t('countries.AQ') },
+ { name: 'Antigua and Barbuda', code: 'AG', label: i18n.t('countries.AG') },
+ { name: 'Argentina', code: 'AR', label: i18n.t('countries.AR') },
+ { name: 'Armenia', code: 'AM', label: i18n.t('countries.AM') },
+ { name: 'Aruba', code: 'AW', label: i18n.t('countries.AW') },
+ { name: 'Australia', code: 'AU', label: i18n.t('countries.AU') },
+ { name: 'Austria', code: 'AT', label: i18n.t('countries.AT') },
+ { name: 'Azerbaijan', code: 'AZ', label: i18n.t('countries.AZ') },
+ { name: 'Bahamas, The', code: 'BS', label: i18n.t('countries.BS') },
+ { name: 'Bahrain', code: 'BH', label: i18n.t('countries.BH') },
+ { name: 'Bangladesh', code: 'BD', label: i18n.t('countries.BD') },
+ { name: 'Barbados', code: 'BB', label: i18n.t('countries.BB') },
+ { name: 'Belarus', code: 'BY', label: i18n.t('countries.BY') },
+ { name: 'Belgium', code: 'BE', label: i18n.t('countries.BE') },
+ { name: 'Belize', code: 'BZ', label: i18n.t('countries.BZ') },
+ { name: 'Benin', code: 'BJ', label: i18n.t('countries.BJ') },
+ { name: 'Bermuda', code: 'BM', label: i18n.t('countries.BM') },
+ { name: 'Bhutan', code: 'BT', label: i18n.t('countries.BT') },
+ { name: 'Bolivia', code: 'BO', label: i18n.t('countries.BO') },
+ {
+ name: 'Bonaire, Sint Eustatius and Saba',
+ code: 'BQ',
+ label: i18n.t('countries.BQ'),
+ },
+ {
+ name: 'Bosnia and Herzegovina ',
+ code: 'BA',
+ label: i18n.t('countries.BA'),
+ },
+ { name: 'Bostwana', code: 'BW', label: i18n.t('countries.BW') },
+ { name: 'Bouvet Island', code: 'BV', label: i18n.t('countries.BV') },
+ { name: 'Brazil', code: 'BR', label: i18n.t('countries.BR') },
+ {
+ name: 'British Indian Ocean Territory',
+ code: 'IO',
+ label: i18n.t('countries.IO'),
+ },
+ { name: 'Brunei Darussalam ', code: 'BN', label: i18n.t('countries.BN') },
+ { name: 'Bulgaria', code: 'BG', label: i18n.t('countries.BG') },
+ { name: 'Burkina Faso', code: 'BF', label: i18n.t('countries.BF') },
+ { name: 'Burundi', code: 'BI', label: i18n.t('countries.BI') },
+ { name: 'Cabo Verde', code: 'CV', label: i18n.t('countries.CV') },
+ { name: 'Cambodia', code: 'KH', label: i18n.t('countries.KH') },
+ { name: 'Cameroon', code: 'CM', label: i18n.t('countries.CM') },
+ { name: 'Canada', code: 'CA', label: i18n.t('countries.CA') },
+ { name: 'Cayman Islands', code: 'KY', label: i18n.t('countries.KY') },
+ {
+ name: 'Central African Republic',
+ code: 'CF',
+ label: i18n.t('countries.CF'),
+ },
+ { name: 'Chad', code: 'TD', label: i18n.t('countries.TD') },
+ { name: 'Chile', code: 'CL', label: i18n.t('countries.CL') },
+ { name: 'China', code: 'CN', label: i18n.t('countries.CN') },
+ { name: 'Christmas Island ', code: 'CX', label: i18n.t('countries.CX') },
+ { name: 'Cocos(Keeling) Islands', code: 'CC', label: i18n.t('countries.CC') },
+ { name: 'Columbia', code: 'CO', label: i18n.t('countries.CO') },
+ { name: 'Comoros', code: 'KM', label: i18n.t('countries.KM') },
+ {
+ name: 'Congo, The Democratic Republic of the',
+ code: 'CD',
+ label: i18n.t('countries.CD'),
+ },
+ { name: 'Congo', code: 'CG', label: i18n.t('countries.CG') },
+ { name: 'Cook Islands', code: 'CK', label: i18n.t('countries.CK') },
+ { name: 'Costa Rica', code: 'CR', label: i18n.t('countries.CR') },
+ { name: 'Croatia', code: 'HR', label: i18n.t('countries.HR') },
+ { name: 'Cuba', code: 'CU', label: i18n.t('countries.CU') },
+ { name: 'Curaçao', code: 'CW', label: i18n.t('countries.CW') },
+ { name: 'Cyprus', code: 'CY', label: i18n.t('countries.CY') },
+ { name: 'Czechia', code: 'CZ', label: i18n.t('countries.CZ') },
+ { name: "Côte d'Ivoire", code: 'CI', label: i18n.t('countries.CI') },
+ { name: 'Denmark', code: 'DK', label: i18n.t('countries.DK') },
+ { name: 'Djibouti', code: 'DJ', label: i18n.t('countries.DJ') },
+ { name: 'Dominica', code: 'DM', label: i18n.t('countries.DM') },
+ { name: 'Dominican Republic', code: 'DO', label: i18n.t('countries.DO') },
+ { name: 'Ecuador', code: 'EC', label: i18n.t('countries.EC') },
+ { name: 'Egypt', code: 'EG', label: i18n.t('countries.EG') },
+ { name: 'El Salvador', code: 'SV', label: i18n.t('countries.SV') },
+ { name: 'Equatorial Guinea ', code: 'GQ', label: i18n.t('countries.GQ') },
+ { name: 'Eritrea', code: 'ER', label: i18n.t('countries.ER') },
+ { name: 'Estonia', code: 'EE', label: i18n.t('countries.EE') },
+ { name: 'Eswatini', code: 'SZ', label: i18n.t('countries.SZ') },
+ { name: 'Ethiopia', code: 'ET', label: i18n.t('countries.ET') },
+ {
+ name: 'Falkland Islands (Malvinas)',
+ code: 'FK',
+ label: i18n.t('countries.FK'),
+ },
+ { name: 'Faroe Islands', code: 'FO', label: i18n.t('countries.FO') },
+ { name: 'Fiji', code: 'FJ', label: i18n.t('countries.FJ') },
+ { name: 'Finland', code: 'FI', label: i18n.t('countries.FI') },
+ { name: 'France', code: 'FR', label: i18n.t('countries.FR') },
+ { name: 'French Guiana', code: 'GF', label: i18n.t('countries.GF') },
+ { name: 'French Polynesia', code: 'PF', label: i18n.t('countries.PF') },
+ {
+ name: 'French Southern Territories',
+ code: 'TF',
+ label: i18n.t('countries.TF'),
+ },
+ { name: 'Gabon', code: 'GA', label: i18n.t('countries.GA') },
+ { name: 'Gambia, The', code: 'GM', label: i18n.t('countries.GM') },
+ { name: 'Georgia', code: 'GE', label: i18n.t('countries.GE') },
+ { name: 'Germany', code: 'DE', label: i18n.t('countries.DE') },
+ { name: 'Ghana', code: 'GH', label: i18n.t('countries.GH') },
+ { name: 'Gibraltar', code: 'GI', label: i18n.t('countries.GI') },
+ { name: 'Greece', code: 'GR', label: i18n.t('countries.GR') },
+ { name: 'Greenland', code: 'GL', label: i18n.t('countries.GL') },
+ { name: 'Grenada', code: 'GD', label: i18n.t('countries.GD') },
+ { name: 'Guadeloupe', code: 'GP', label: i18n.t('countries.GP') },
+ { name: 'Guam', code: 'GU', label: i18n.t('countries.GU') },
+ { name: 'Guatemala', code: 'GT', label: i18n.t('countries.GT') },
+ { name: 'Guernsey', code: 'GG', label: i18n.t('countries.GG') },
+ { name: 'Guinea', code: 'GN', label: i18n.t('countries.GN') },
+ { name: 'Guinea-Bissau', code: 'GW', label: i18n.t('countries.GW') },
+ { name: 'Guyana', code: 'GY', label: i18n.t('countries.GY') },
+ { name: 'Haiti', code: 'HT', label: i18n.t('countries.HT') },
+ {
+ name: 'Heard Island and McDonald Islands',
+ code: 'HM',
+ label: i18n.t('countries.HM'),
+ },
+ { name: 'Holy See', code: 'VA', label: i18n.t('countries.VA') },
+ { name: 'Honduras', code: 'HN', label: i18n.t('countries.HN') },
+ { name: 'Hong Kong', code: 'HK', label: i18n.t('countries.HK') },
+ { name: 'Hungary', code: 'HU', label: i18n.t('countries.HU') },
+ { name: 'Iceland', code: 'IS', label: i18n.t('countries.IS') },
+ { name: 'India', code: 'IN', label: i18n.t('countries.IN') },
+ { name: 'Indonesia', code: 'ID', label: i18n.t('countries.ID') },
+ {
+ name: 'Iran, Islamic Republic of',
+ code: 'IR',
+ label: i18n.t('countries.IR'),
+ },
+ { name: 'Iraq', code: 'IQ', label: i18n.t('countries.IQ') },
+ { name: 'Ireland', code: 'IE', label: i18n.t('countries.IE') },
+ { name: 'Isle of Man', code: 'IM', label: i18n.t('countries.IM') },
+ { name: 'Israel', code: 'IL', label: i18n.t('countries.IL') },
+ { name: 'Italy', code: 'IT', label: i18n.t('countries.IT') },
+ { name: 'Jamaica', code: 'JM', label: i18n.t('countries.JM') },
+ { name: 'Japan', code: 'JP', label: i18n.t('countries.JP') },
+ { name: 'Jersey', code: 'JE', label: i18n.t('countries.JE') },
+ { name: 'Jordan', code: 'JO', label: i18n.t('countries.JO') },
+ { name: 'Kazakhstan', code: 'KZ', label: i18n.t('countries.KZ') },
+ { name: 'Kenya', code: 'KE', label: i18n.t('countries.KE') },
+ { name: 'Kiribati', code: 'KI', label: i18n.t('countries.KI') },
+ { name: 'Korea, Republic of', code: 'KR', label: i18n.t('countries.KR') },
+ {
+ name: "Korea, Democratic People's Republic of",
+ code: 'KP',
+ label: i18n.t('countries.KP'),
+ },
+ { name: 'Kuwait', code: 'KW', label: i18n.t('countries.KW') },
+ { name: 'Kyrgyzstan', code: 'KG', label: i18n.t('countries.KG') },
+ {
+ name: "Lao People's Democratic Republic",
+ code: 'LA',
+ label: i18n.t('countries.LA'),
+ },
+ { name: 'Latvia', code: 'LV', label: i18n.t('countries.LV') },
+ { name: 'Lebanon', code: 'LB', label: i18n.t('countries.LB') },
+ { name: 'Lesotho', code: 'LS', label: i18n.t('countries.LS') },
+ { name: 'Liberia', code: 'LR', label: i18n.t('countries.LR') },
+ { name: 'Libya', code: 'LY', label: i18n.t('countries.LY') },
+ { name: 'Liechtenstein', code: 'LI', label: i18n.t('countries.LI') },
+ { name: 'Lithuania', code: 'LT', label: i18n.t('countries.LT') },
+ { name: 'Luxembourg', code: 'LU', label: i18n.t('countries.LU') },
+ { name: 'Macao', code: 'MO', label: i18n.t('countries.MO') },
+ {
+ name: 'Macedonia, The Former Yugoslav Republic of',
+ code: 'MK',
+ label: i18n.t('countries.MK'),
+ },
+ { name: 'Madagascar', code: 'MG', label: i18n.t('countries.MG') },
+ { name: 'Malawi', code: 'MW', label: i18n.t('countries.MW') },
+ { name: 'Malaysia', code: 'MY', label: i18n.t('countries.MY') },
+ { name: 'Maldives', code: 'MV', label: i18n.t('countries.MV') },
+ { name: 'Mali', code: 'ML', label: i18n.t('countries.ML') },
+ { name: 'Malta', code: 'MT', label: i18n.t('countries.MT') },
+ { name: 'Marshall Islands', code: 'MH', label: i18n.t('countries.MH') },
+ { name: 'Martinique', code: 'MQ', label: i18n.t('countries.MQ') },
+ { name: 'Mauritania', code: 'MR', label: i18n.t('countries.MR') },
+ { name: 'Mauritius', code: 'MU', label: i18n.t('countries.MU') },
+ { name: 'Mayotte', code: 'YT', label: i18n.t('countries.YT') },
+ { name: 'Mexico', code: 'MX', label: i18n.t('countries.MX') },
+ {
+ name: 'Micronesia, Federated States of',
+ code: 'FM',
+ label: i18n.t('countries.FM'),
+ },
+ { name: 'Moldova, Republic of', code: 'MD', label: i18n.t('countries.MD') },
+ { name: 'Monaco', code: 'MC', label: i18n.t('countries.MC') },
+ { name: 'Mongolia', code: 'MN', label: i18n.t('countries.MN') },
+ { name: 'Montenegro', code: 'ME', label: i18n.t('countries.ME') },
+ { name: 'Montserrat', code: 'MS', label: i18n.t('countries.MS') },
+ { name: 'Morocco', code: 'MA', label: i18n.t('countries.MA') },
+ { name: 'Mozambique', code: 'MZ', label: i18n.t('countries.MZ') },
+ { name: 'Myanmar', code: 'MM', label: i18n.t('countries.MM') },
+ { name: 'Namibia', code: 'NA', label: i18n.t('countries.NA') },
+ { name: 'Nauru', code: 'NR', label: i18n.t('countries.NR') },
+ { name: 'Nepal', code: 'NP', label: i18n.t('countries.NP') },
+ { name: 'Netherlands', code: 'NL', label: i18n.t('countries.NL') },
+ { name: 'New Caledonia', code: 'NC', label: i18n.t('countries.NC') },
+ { name: 'New Zealand', code: 'NZ', label: i18n.t('countries.NZ') },
+ { name: 'Nicaragua', code: 'NI', label: i18n.t('countries.NI') },
+ { name: 'Niger', code: 'NE', label: i18n.t('countries.NE') },
+ { name: 'Nigeria', code: 'NG', label: i18n.t('countries.NG') },
+ { name: 'Niue', code: 'NU', label: i18n.t('countries.NU') },
+ { name: 'Norfolk Island', code: 'NF', label: i18n.t('countries.NF') },
+ {
+ name: 'Northern Mariana Islands',
+ code: 'MP',
+ label: i18n.t('countries.MP'),
+ },
+ { name: 'Norway', code: 'NO', label: i18n.t('countries.NO') },
+ { name: 'Oman', code: 'OM', label: i18n.t('countries.OM') },
+ { name: 'Pakistan', code: 'PK', label: i18n.t('countries.PK') },
+ { name: 'Palau', code: 'PW', label: i18n.t('countries.PW') },
+ { name: 'Palestine', code: 'PS', label: i18n.t('countries.PS') },
+ { name: 'Panama', code: 'PA', label: i18n.t('countries.PA') },
+ { name: 'Papua New Guinea', code: 'PG', label: i18n.t('countries.PG') },
+ { name: 'Paraguay', code: 'PY', label: i18n.t('countries.PY') },
+ { name: 'Peru', code: 'PE', label: i18n.t('countries.PE') },
+ { name: 'Philippines', code: 'PH', label: i18n.t('countries.PH') },
+ { name: 'Pitcairn', code: 'PN', label: i18n.t('countries.PN') },
+ { name: 'Poland', code: 'PL', label: i18n.t('countries.PL') },
+ { name: 'Portugal', code: 'PT', label: i18n.t('countries.PT') },
+ { name: 'Puerto Rico', code: 'PR', label: i18n.t('countries.PR') },
+ { name: 'Qatar', code: 'QA', label: i18n.t('countries.QA') },
+ { name: 'Romania', code: 'RO', label: i18n.t('countries.RO') },
+ { name: 'Russian Federation', code: 'RU', label: i18n.t('countries.RU') },
+ { name: 'Rwanda', code: 'RW', label: i18n.t('countries.RW') },
+ { name: 'Réunion', code: 'RE', label: i18n.t('countries.RE') },
+ { name: 'Saint Barthélemy', code: 'BL', label: i18n.t('countries.BL') },
+ {
+ name: 'Saint Helena, Ascension and Tristan da Cunha',
+ code: 'SH',
+ label: i18n.t('countries.SH'),
+ },
+ { name: 'Saint Kitts and Nevis ', code: 'KN', label: i18n.t('countries.KN') },
+ { name: 'Saint Lucia', code: 'LC', label: i18n.t('countries.LC') },
+ { name: 'Saint Martin', code: 'MF', label: i18n.t('countries.MF') },
+ {
+ name: 'Saint Pierre and Miquelon',
+ code: 'PM',
+ label: i18n.t('countries.PM'),
+ },
+ {
+ name: 'Saint Vincent and the Grenadines',
+ code: 'VC',
+ label: i18n.t('countries.VC'),
+ },
+ { name: 'Samoa', code: 'WS', label: i18n.t('countries.WS') },
+ { name: 'San Marino ', code: 'SM', label: i18n.t('countries.SM') },
+ { name: 'Sao Tome and Principe', code: 'ST', label: i18n.t('countries.ST') },
+ { name: 'Saudi Arabia', code: 'SA', label: i18n.t('countries.SA') },
+ { name: 'Senegal', code: 'SN', label: i18n.t('countries.SN') },
+ { name: 'Serbia', code: 'RS', label: i18n.t('countries.RS') },
+ { name: 'Seychelles', code: 'SC', label: i18n.t('countries.SC') },
+ { name: 'Sierra Leone', code: 'SL', label: i18n.t('countries.SL') },
+ { name: 'Singapore', code: 'SG', label: i18n.t('countries.SG') },
+ { name: 'Sint Maarten', code: 'SX', label: i18n.t('countries.SX') },
+ { name: 'Slovakia', code: 'SK', label: i18n.t('countries.SK') },
+ { name: 'Slovenia', code: 'SI', label: i18n.t('countries.SI') },
+ { name: 'Solomon Islands', code: 'SB', label: i18n.t('countries.SB') },
+ { name: 'Somalia', code: 'SO', label: i18n.t('countries.SO') },
+ { name: 'South Africa ', code: 'ZA', label: i18n.t('countries.ZA') },
+ {
+ name: 'South Georgia and the South Sandwich Islands',
+ code: 'GS',
+ label: i18n.t('countries.GS'),
+ },
+ { name: 'South Sudan', code: 'SS', label: i18n.t('countries.SS') },
+ { name: 'Spain', code: 'ES', label: i18n.t('countries.ES') },
+ { name: 'Sri Lanka', code: 'LK', label: i18n.t('countries.LK') },
+ { name: 'Sudan', code: 'SD', label: i18n.t('countries.SD') },
+ { name: 'Suriname', code: 'SR', label: i18n.t('countries.SR') },
+ { name: 'Svalbard and Jan Mayen', code: 'SJ', label: i18n.t('countries.SJ') },
+ { name: 'Sweden', code: 'SE', label: i18n.t('countries.SE') },
+ { name: 'Switzerland', code: 'CH', label: i18n.t('countries.CH') },
+ { name: 'Syrian Arab Republic', code: 'SY', label: i18n.t('countries.SY') },
+ { name: 'Taiwan', code: 'TW', label: i18n.t('countries.TW') },
+ { name: 'Tajikistan', code: 'TJ', label: i18n.t('countries.TJ') },
+ {
+ name: 'Tanzania, United Republic of',
+ code: 'TZ',
+ label: i18n.t('countries.TZ'),
+ },
+ { name: 'Thailand', code: 'TH', label: i18n.t('countries.TH') },
+ { name: 'Timor-Leste', code: 'TL', label: i18n.t('countries.TL') },
+ { name: 'Togo', code: 'TG', label: i18n.t('countries.TG') },
+ { name: 'Tokelau', code: 'TK', label: i18n.t('countries.TK') },
+ { name: 'Tonga', code: 'TO', label: i18n.t('countries.TO') },
+ { name: 'Trinidad and Tobago', code: 'TT', label: i18n.t('countries.TT') },
+ { name: 'Tunisia', code: 'TN', label: i18n.t('countries.TN') },
+ { name: 'Turkey', code: 'TR', label: i18n.t('countries.TR') },
+ { name: 'Turkmenistan', code: 'TM', label: i18n.t('countries.TM') },
+ {
+ name: 'Turks and Caicos Islands',
+ code: 'TC',
+ label: i18n.t('countries.TC'),
+ },
+ { name: 'Tuvalu', code: 'TV', label: i18n.t('countries.TV') },
+ { name: 'Uganda', code: 'UG', label: i18n.t('countries.UG') },
+ { name: 'Ukraine', code: 'UA', label: i18n.t('countries.UA') },
+ { name: 'United Arab Emirates', code: 'AE', label: i18n.t('countries.AE') },
+ { name: 'United Kingdom', code: 'GB', label: i18n.t('countries.GB') },
+ {
+ name: 'United States Minor Outlying Islands',
+ code: 'UM',
+ label: i18n.t('countries.UM'),
+ },
+ {
+ name: 'United States of America',
+ code: 'US',
+ label: i18n.t('countries.US'),
+ },
+ { name: 'Uruguay', code: 'UY', label: i18n.t('countries.UY') },
+ { name: 'Uzbekistan', code: 'UZ', label: i18n.t('countries.UZ') },
+ { name: 'Vanuatu', code: 'VU', label: i18n.t('countries.VU') },
+ { name: 'Venezuela', code: 'VE', label: i18n.t('countries.VE') },
+ { name: 'Viet Nam', code: 'VN', label: i18n.t('countries.VN') },
+ {
+ name: 'Virgin Islands, British',
+ code: 'VG',
+ label: i18n.t('countries.VG'),
+ },
+ { name: 'Virgin Islands, U.S', code: 'VI', label: i18n.t('countries.VI') },
+ { name: 'Wallis and Futuna', code: 'WF', label: i18n.t('countries.WF') },
+ { name: 'Western Sahara', code: 'EH', label: i18n.t('countries.EH') },
+ { name: 'Yemen', code: 'YE', label: i18n.t('countries.YE') },
+ { name: 'Zambia', code: 'ZM', label: i18n.t('countries.ZM') },
+ { name: 'Zimbabwe', code: 'ZW', label: i18n.t('countries.ZW') },
+ { name: 'Åland Islands', code: 'AX', label: i18n.t('countries.AX') },
+];
diff --git a/src/views/_sila/SecurityAndAccess/Certificates/ModalGenerateCsr.vue b/src/views/_sila/SecurityAndAccess/Certificates/ModalGenerateCsr.vue
new file mode 100644
index 00000000..d76f9fe1
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Certificates/ModalGenerateCsr.vue
@@ -0,0 +1,496 @@
+<template>
+ <div>
+ <b-modal
+ id="generate-csr"
+ ref="modal"
+ size="lg"
+ no-stacking
+ :title="$t('pageCertificates.modal.generateACertificateSigningRequest')"
+ @ok="onOkGenerateCsrModal"
+ @cancel="resetForm"
+ @hidden="$v.$reset()"
+ >
+ <b-form id="generate-csr-form" novalidate @submit.prevent="handleSubmit">
+ <b-container fluid>
+ <b-row>
+ <b-col lg="9">
+ <b-row>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.certificateType')"
+ label-for="certificate-type"
+ >
+ <b-form-select
+ id="certificate-type"
+ v-model="form.certificateType"
+ data-test-id="modalGenerateCsr-select-certificateType"
+ :options="certificateOptions"
+ :state="getValidationState($v.form.certificateType)"
+ @input="$v.form.certificateType.$touch()"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.country')"
+ label-for="country"
+ >
+ <b-form-select
+ id="country"
+ v-model="form.country"
+ data-test-id="modalGenerateCsr-select-country"
+ :options="countryOptions"
+ :state="getValidationState($v.form.country)"
+ @input="$v.form.country.$touch()"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.state')"
+ label-for="state"
+ >
+ <b-form-input
+ id="state"
+ v-model="form.state"
+ type="text"
+ data-test-id="modalGenerateCsr-input-state"
+ :state="getValidationState($v.form.state)"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.city')"
+ label-for="city"
+ >
+ <b-form-input
+ id="city"
+ v-model="form.city"
+ type="text"
+ data-test-id="modalGenerateCsr-input-city"
+ :state="getValidationState($v.form.city)"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.companyName')"
+ label-for="company-name"
+ >
+ <b-form-input
+ id="company-name"
+ v-model="form.companyName"
+ type="text"
+ data-test-id="modalGenerateCsr-input-companyName"
+ :state="getValidationState($v.form.companyName)"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.companyUnit')"
+ label-for="company-unit"
+ >
+ <b-form-input
+ id="company-unit"
+ v-model="form.companyUnit"
+ type="text"
+ data-test-id="modalGenerateCsr-input-companyUnit"
+ :state="getValidationState($v.form.companyUnit)"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col lg="6">
+ <b-form-group
+ :label="$t('pageCertificates.modal.commonName')"
+ label-for="common-name"
+ >
+ <b-form-input
+ id="common-name"
+ v-model="form.commonName"
+ type="text"
+ data-test-id="modalGenerateCsr-input-commonName"
+ :state="getValidationState($v.form.commonName)"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col lg="6">
+ <b-form-group label-for="challenge-password">
+ <template #label>
+ {{ $t('pageCertificates.modal.challengePassword') }} -
+ <span class="form-text d-inline">
+ {{ $t('global.form.optional') }}
+ </span>
+ </template>
+ <b-form-input
+ id="challenge-password"
+ v-model="form.challengePassword"
+ type="text"
+ data-test-id="modalGenerateCsr-input-challengePassword"
+ />
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col lg="6">
+ <b-form-group label-for="contact-person">
+ <template #label>
+ {{ $t('pageCertificates.modal.contactPerson') }} -
+ <span class="form-text d-inline">
+ {{ $t('global.form.optional') }}
+ </span>
+ </template>
+ <b-form-input
+ id="contact-person"
+ v-model="form.contactPerson"
+ type="text"
+ data-test-id="modalGenerateCsr-input-contactPerson"
+ />
+ </b-form-group>
+ </b-col>
+ <b-col lg="6">
+ <b-form-group label-for="email-address">
+ <template #label>
+ {{ $t('pageCertificates.modal.emailAddress') }} -
+ <span class="form-text d-inline">
+ {{ $t('global.form.optional') }}
+ </span>
+ </template>
+ <b-form-input
+ id="email-address"
+ v-model="form.emailAddress"
+ type="text"
+ data-test-id="modalGenerateCsr-input-emailAddress"
+ />
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col lg="12">
+ <b-form-group label-for="alternate-name">
+ <template #label>
+ {{ $t('pageCertificates.modal.alternateName') }} -
+ <span class="form-text d-inline">
+ {{ $t('global.form.optional') }}
+ </span>
+ </template>
+ <b-form-text id="alternate-name-help-block">
+ {{ $t('pageCertificates.modal.alternateNameHelperText') }}
+ </b-form-text>
+ <b-form-tags
+ v-model="form.alternateName"
+ :remove-on-delete="true"
+ :tag-pills="true"
+ input-id="alternate-name"
+ size="lg"
+ separator=" "
+ :input-attrs="{
+ 'aria-describedby': 'alternate-name-help-block',
+ }"
+ :duplicate-tag-text="
+ $t('pageCertificates.modal.duplicateAlternateName')
+ "
+ placeholder=""
+ data-test-id="modalGenerateCsr-input-alternateName"
+ >
+ <template #add-button-text>
+ <icon-add /> {{ $t('global.action.add') }}
+ </template>
+ </b-form-tags>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-col>
+ <b-col lg="3">
+ <b-row>
+ <b-col lg="12">
+ <p class="col-form-label">
+ {{ $t('pageCertificates.modal.privateKey') }}
+ </p>
+ <b-form-group
+ :label="$t('pageCertificates.modal.keyPairAlgorithm')"
+ label-for="key-pair-algorithm"
+ >
+ <b-form-select
+ id="key-pair-algorithm"
+ v-model="form.keyPairAlgorithm"
+ data-test-id="modalGenerateCsr-select-keyPairAlgorithm"
+ :options="keyPairAlgorithmOptions"
+ :state="getValidationState($v.form.keyPairAlgorithm)"
+ @input="$v.form.keyPairAlgorithm.$touch()"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col lg="12">
+ <template v-if="$v.form.keyPairAlgorithm.$model === 'EC'">
+ <b-form-group
+ :label="$t('pageCertificates.modal.keyCurveId')"
+ label-for="key-curve-id"
+ >
+ <b-form-select
+ id="key-curve-id"
+ v-model="form.keyCurveId"
+ data-test-id="modalGenerateCsr-select-keyCurveId"
+ :options="keyCurveIdOptions"
+ :state="getValidationState($v.form.keyCurveId)"
+ @input="$v.form.keyCurveId.$touch()"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </template>
+ <template v-if="$v.form.keyPairAlgorithm.$model === 'RSA'">
+ <b-form-group
+ :label="$t('pageCertificates.modal.keyBitLength')"
+ label-for="key-bit-length"
+ >
+ <b-form-select
+ id="key-bit-length"
+ v-model="form.keyBitLength"
+ data-test-id="modalGenerateCsr-select-keyBitLength"
+ :options="keyBitLengthOptions"
+ :state="getValidationState($v.form.keyBitLength)"
+ @input="$v.form.keyBitLength.$touch()"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </template>
+ </b-col>
+ </b-row>
+ </b-col>
+ </b-row>
+ </b-container>
+ </b-form>
+ <template #modal-footer="{ ok, cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button
+ form="generate-csr-form"
+ type="submit"
+ variant="primary"
+ data-test-id="modalGenerateCsr-button-ok"
+ @click="ok()"
+ >
+ {{ $t('pageCertificates.generateCsr') }}
+ </b-button>
+ </template>
+ </b-modal>
+ <b-modal
+ id="csr-string"
+ no-stacking
+ size="lg"
+ :title="$t('pageCertificates.modal.certificateSigningRequest')"
+ @hidden="onHiddenCsrStringModal"
+ >
+ {{ csrString }}
+ <template #modal-footer>
+ <b-btn variant="secondary" @click="copyCsrString">
+ <template v-if="csrStringCopied">
+ <icon-checkmark />
+ {{ $t('global.status.copied') }}
+ </template>
+ <template v-else>
+ {{ $t('global.action.copy') }}
+ </template>
+ </b-btn>
+ <a
+ :href="`data:text/json;charset=utf-8,${csrString}`"
+ download="certificate.txt"
+ class="btn btn-primary"
+ >
+ {{ $t('global.action.download') }}
+ </a>
+ </template>
+ </b-modal>
+ </div>
+</template>
+
+<script>
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconCheckmark from '@carbon/icons-vue/es/checkmark/20';
+
+import { required, requiredIf } from 'vuelidate/lib/validators';
+
+import { COUNTRY_LIST } from './CsrCountryCodes';
+import { CERTIFICATE_TYPES } from '@/store/modules/SecurityAndAccess/CertificatesStore';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+ name: 'ModalGenerateCsr',
+ components: { IconAdd, IconCheckmark },
+ mixins: [BVToastMixin, VuelidateMixin],
+ data() {
+ return {
+ form: {
+ certificateType: null,
+ country: null,
+ state: null,
+ city: null,
+ companyName: null,
+ companyUnit: null,
+ commonName: null,
+ challengePassword: null,
+ contactPerson: null,
+ emailAddress: null,
+ alternateName: [],
+ keyPairAlgorithm: null,
+ keyCurveId: null,
+ keyBitLength: null,
+ },
+ certificateOptions: CERTIFICATE_TYPES.reduce((arr, cert) => {
+ if (cert.type === 'TrustStore Certificate') return arr;
+ arr.push({
+ text: cert.label,
+ value: cert.type,
+ });
+ return arr;
+ }, []),
+ countryOptions: COUNTRY_LIST.map((country) => ({
+ text: country.label,
+ value: country.code,
+ })),
+ keyPairAlgorithmOptions: ['EC', 'RSA'],
+ keyCurveIdOptions: ['prime256v1', 'secp521r1', 'secp384r1'],
+ keyBitLengthOptions: [2048],
+ csrString: '',
+ csrStringCopied: false,
+ };
+ },
+ validations: {
+ form: {
+ certificateType: { required },
+ country: { required },
+ state: { required },
+ city: { required },
+ companyName: { required },
+ companyUnit: { required },
+ commonName: { required },
+ challengePassword: {},
+ contactPerson: {},
+ emailAddress: {},
+ alternateName: {},
+ keyPairAlgorithm: { required },
+ keyCurveId: {
+ reuired: requiredIf(function (form) {
+ return form.keyPairAlgorithm === 'EC';
+ }),
+ },
+ keyBitLength: {
+ reuired: requiredIf(function (form) {
+ return form.keyPairAlgorithm === 'RSA';
+ }),
+ },
+ },
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$store
+ .dispatch('certificates/generateCsr', this.form)
+ .then(({ data: { CSRString } }) => {
+ this.csrString = CSRString;
+ this.$bvModal.show('csr-string');
+ this.$v.$reset();
+ });
+ },
+ resetForm() {
+ for (let key of Object.keys(this.form)) {
+ if (key === 'alternateName') {
+ this.form[key] = [];
+ } else {
+ this.form[key] = null;
+ }
+ }
+ },
+ onOkGenerateCsrModal(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ onHiddenCsrStringModal() {
+ this.csrString = '';
+ this.resetForm();
+ },
+ copyCsrString(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ navigator.clipboard.writeText(this.csrString).then(() => {
+ // Show copied text for 5 seconds
+ this.csrStringCopied = true;
+ setTimeout(() => {
+ this.csrStringCopied = false;
+ }, 5000 /*5 seconds*/);
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/Certificates/ModalUploadCertificate.vue b/src/views/_sila/SecurityAndAccess/Certificates/ModalUploadCertificate.vue
new file mode 100644
index 00000000..f4db7a26
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Certificates/ModalUploadCertificate.vue
@@ -0,0 +1,168 @@
+<template>
+ <b-modal id="upload-certificate" ref="modal" @ok="onOk" @hidden="resetForm">
+ <template #modal-title>
+ <template v-if="certificate">
+ {{ $t('pageCertificates.replaceCertificate') }}
+ </template>
+ <template v-else>
+ {{ $t('pageCertificates.addNewCertificate') }}
+ </template>
+ </template>
+ <b-form>
+ <!-- Replace Certificate type -->
+ <template v-if="certificate !== null">
+ <dl class="mb-4">
+ <dt>{{ $t('pageCertificates.modal.certificateType') }}</dt>
+ <dd>{{ certificate.certificate }}</dd>
+ </dl>
+ </template>
+
+ <!-- Add new Certificate type -->
+ <template v-else>
+ <b-form-group
+ :label="$t('pageCertificates.modal.certificateType')"
+ label-for="certificate-type"
+ >
+ <b-form-select
+ id="certificate-type"
+ v-model="form.certificateType"
+ :options="certificateOptions"
+ :state="getValidationState($v.form.certificateType)"
+ @input="$v.form.certificateType.$touch()"
+ >
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.certificateType.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </template>
+
+ <b-form-group :label="$t('pageCertificates.modal.certificateFile')">
+ <form-file
+ id="certificate-file"
+ v-model="form.file"
+ accept=".pem"
+ :state="getValidationState($v.form.file)"
+ >
+ <template #invalid>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.required') }}
+ </b-form-invalid-feedback>
+ </template>
+ </form-file>
+ </b-form-group>
+ </b-form>
+ <template #modal-ok>
+ <template v-if="certificate">
+ {{ $t('global.action.replace') }}
+ </template>
+ <template v-else>
+ {{ $t('global.action.add') }}
+ </template>
+ </template>
+ <template #modal-cancel>
+ {{ $t('global.action.cancel') }}
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import { required, requiredIf } from 'vuelidate/lib/validators';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+import FormFile from '@/components/Global/FormFile';
+
+export default {
+ components: { FormFile },
+ mixins: [VuelidateMixin],
+ props: {
+ certificate: {
+ type: Object,
+ default: null,
+ validator: (prop) => {
+ if (prop === null) return true;
+ return (
+ Object.prototype.hasOwnProperty.call(prop, 'type') &&
+ Object.prototype.hasOwnProperty.call(prop, 'certificate')
+ );
+ },
+ },
+ },
+ data() {
+ return {
+ form: {
+ certificateType: null,
+ file: null,
+ },
+ };
+ },
+ computed: {
+ certificateTypes() {
+ return this.$store.getters['certificates/availableUploadTypes'];
+ },
+ certificateOptions() {
+ return this.certificateTypes.map(({ type, label }) => {
+ return {
+ text: label,
+ value: type,
+ };
+ });
+ },
+ },
+ watch: {
+ certificateOptions: function (options) {
+ if (options.length) {
+ this.form.certificateType = options[0].value;
+ }
+ },
+ },
+ validations() {
+ return {
+ form: {
+ certificateType: {
+ required: requiredIf(function () {
+ return !this.certificate;
+ }),
+ },
+ file: {
+ required,
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', {
+ addNew: !this.certificate,
+ file: this.form.file,
+ location: this.certificate ? this.certificate.location : null,
+ type: this.certificate
+ ? this.certificate.type
+ : this.form.certificateType,
+ });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.certificateType = this.certificateOptions.length
+ ? this.certificateOptions[0].value
+ : null;
+ this.form.file = null;
+ this.$v.$reset();
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/Certificates/index.js b/src/views/_sila/SecurityAndAccess/Certificates/index.js
new file mode 100644
index 00000000..aff57b59
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Certificates/index.js
@@ -0,0 +1,2 @@
+import Certificates from './Certificates.vue';
+export default Certificates;
diff --git a/src/views/_sila/SecurityAndAccess/Ldap/Ldap.vue b/src/views/_sila/SecurityAndAccess/Ldap/Ldap.vue
new file mode 100644
index 00000000..1f2108de
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Ldap/Ldap.vue
@@ -0,0 +1,435 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pageLdap.pageDescription')" />
+ <page-section :section-title="$t('pageLdap.settings')">
+ <b-form novalidate @submit.prevent="handleSubmit">
+ <b-row>
+ <b-col>
+ <b-form-group
+ class="mb-3"
+ :label="$t('pageLdap.form.ldapAuthentication')"
+ :disabled="loading"
+ >
+ <b-form-checkbox
+ v-model="form.ldapAuthenticationEnabled"
+ data-test-id="ldap-checkbox-ldapAuthenticationEnabled"
+ @change="onChangeldapAuthenticationEnabled"
+ >
+ {{ $t('global.action.enable') }}
+ </b-form-checkbox>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <div class="form-background p-3">
+ <b-form-group
+ class="m-0"
+ :label="$t('pageLdap.ariaLabel.ldapSettings')"
+ label-class="sr-only"
+ :disabled="!form.ldapAuthenticationEnabled || loading"
+ >
+ <b-row>
+ <b-col md="3" lg="4" xl="3">
+ <b-form-group
+ class="mb-4"
+ :label="$t('pageLdap.form.secureLdapUsingSsl')"
+ >
+ <b-form-text id="enable-secure-help-block">
+ {{ $t('pageLdap.form.secureLdapHelper') }}
+ </b-form-text>
+ <b-form-checkbox
+ id="enable-secure-ldap"
+ v-model="form.secureLdapEnabled"
+ aria-describedby="enable-secure-help-block"
+ data-test-id="ldap-checkbox-secureLdapEnabled"
+ :disabled="
+ !caCertificateExpiration || !ldapCertificateExpiration
+ "
+ @change="$v.form.secureLdapEnabled.$touch()"
+ >
+ {{ $t('global.action.enable') }}
+ </b-form-checkbox>
+ </b-form-group>
+ <dl>
+ <dt>{{ $t('pageLdap.form.caCertificateValidUntil') }}</dt>
+ <dd v-if="caCertificateExpiration">
+ {{ caCertificateExpiration | formatDate }}
+ </dd>
+ <dd v-else>--</dd>
+ <dt>{{ $t('pageLdap.form.ldapCertificateValidUntil') }}</dt>
+ <dd v-if="ldapCertificateExpiration">
+ {{ ldapCertificateExpiration | formatDate }}
+ </dd>
+ <dd v-else>--</dd>
+ </dl>
+ <b-link
+ class="d-inline-block mb-4 m-md-0"
+ to="/security-and-access/certificates"
+ >
+ {{ $t('pageLdap.form.manageSslCertificates') }}
+ </b-link>
+ </b-col>
+ <b-col md="9" lg="8" xl="9">
+ <b-row>
+ <b-col>
+ <b-form-group :label="$t('pageLdap.form.serviceType')">
+ <b-form-radio
+ v-model="form.activeDirectoryEnabled"
+ data-test-id="ldap-radio-activeDirectoryEnabled"
+ :value="false"
+ @change="onChangeServiceType"
+ >
+ OpenLDAP
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.activeDirectoryEnabled"
+ data-test-id="ldap-radio-activeDirectoryEnabled"
+ :value="true"
+ @change="onChangeServiceType"
+ >
+ Active Directory
+ </b-form-radio>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col sm="6" xl="4">
+ <b-form-group label-for="server-uri">
+ <template #label>
+ {{ $t('pageLdap.form.serverUri') }}
+ <info-tooltip
+ :title="$t('pageLdap.form.serverUriTooltip')"
+ />
+ </template>
+ <b-input-group :prepend="ldapProtocol">
+ <b-form-input
+ id="server-uri"
+ v-model="form.serverUri"
+ data-test-id="ldap-input-serverUri"
+ :state="getValidationState($v.form.serverUri)"
+ @change="$v.form.serverUri.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <b-form-group
+ :label="$t('pageLdap.form.bindDn')"
+ label-for="bind-dn"
+ >
+ <b-form-input
+ id="bind-dn"
+ v-model="form.bindDn"
+ data-test-id="ldap-input-bindDn"
+ :state="getValidationState($v.form.bindDn)"
+ @change="$v.form.bindDn.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <b-form-group
+ :label="$t('pageLdap.form.bindPassword')"
+ label-for="bind-password"
+ >
+ <input-password-toggle
+ data-test-id="ldap-input-togglePassword"
+ >
+ <b-form-input
+ id="bind-password"
+ v-model="form.bindPassword"
+ type="password"
+ :state="getValidationState($v.form.bindPassword)"
+ class="form-control-with-button"
+ @change="$v.form.bindPassword.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <b-form-group
+ :label="$t('pageLdap.form.baseDn')"
+ label-for="base-dn"
+ >
+ <b-form-input
+ id="base-dn"
+ v-model="form.baseDn"
+ data-test-id="ldap-input-baseDn"
+ :state="getValidationState($v.form.baseDn)"
+ @change="$v.form.baseDn.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <b-form-group label-for="user-id-attribute">
+ <template #label>
+ {{ $t('pageLdap.form.userIdAttribute') }} -
+ <span class="form-text d-inline">
+ {{ $t('global.form.optional') }}
+ </span>
+ </template>
+ <b-form-input
+ id="user-id-attribute"
+ v-model="form.userIdAttribute"
+ data-test-id="ldap-input-userIdAttribute"
+ @change="$v.form.userIdAttribute.$touch()"
+ />
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" xl="4">
+ <b-form-group label-for="group-id-attribute">
+ <template #label>
+ {{ $t('pageLdap.form.groupIdAttribute') }} -
+ <span class="form-text d-inline">
+ {{ $t('global.form.optional') }}
+ </span>
+ </template>
+ <b-form-input
+ id="group-id-attribute"
+ v-model="form.groupIdAttribute"
+ data-test-id="ldap-input-groupIdAttribute"
+ @change="$v.form.groupIdAttribute.$touch()"
+ />
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-col>
+ </b-row>
+ </b-form-group>
+ </div>
+ <b-row class="mt-4 mb-5">
+ <b-col>
+ <b-btn
+ variant="primary"
+ type="submit"
+ data-test-id="ldap-button-saveSettings"
+ :disabled="loading"
+ >
+ {{ $t('global.action.saveSettings') }}
+ </b-btn>
+ </b-col>
+ </b-row>
+ </b-form>
+ </page-section>
+
+ <!-- Role groups -->
+ <page-section :section-title="$t('pageLdap.roleGroups')">
+ <table-role-groups />
+ </page-section>
+ </b-container>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import { find } from 'lodash';
+import { requiredIf } from 'vuelidate/lib/validators';
+
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin';
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import InfoTooltip from '@/components/Global/InfoTooltip';
+import TableRoleGroups from './TableRoleGroups';
+
+export default {
+ name: 'Ldap',
+ components: {
+ InfoTooltip,
+ InputPasswordToggle,
+ PageTitle,
+ PageSection,
+ TableRoleGroups,
+ },
+ mixins: [BVToastMixin, VuelidateMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ form: {
+ ldapAuthenticationEnabled: this.$store.getters['ldap/isServiceEnabled'],
+ secureLdapEnabled: false,
+ activeDirectoryEnabled: this.$store.getters[
+ 'ldap/isActiveDirectoryEnabled'
+ ],
+ serverUri: '',
+ bindDn: '',
+ bindPassword: '',
+ baseDn: '',
+ userIdAttribute: '',
+ groupIdAttribute: '',
+ loading,
+ },
+ };
+ },
+ computed: {
+ ...mapGetters('ldap', [
+ 'isServiceEnabled',
+ 'isActiveDirectoryEnabled',
+ 'ldap',
+ 'activeDirectory',
+ ]),
+ sslCertificates() {
+ return this.$store.getters['certificates/allCertificates'];
+ },
+ caCertificateExpiration() {
+ const caCertificate = find(this.sslCertificates, {
+ type: 'TrustStore Certificate',
+ });
+ if (caCertificate === undefined) return null;
+ return caCertificate.validUntil;
+ },
+ ldapCertificateExpiration() {
+ const ldapCertificate = find(this.sslCertificates, {
+ type: 'LDAP Certificate',
+ });
+ if (ldapCertificate === undefined) return null;
+ return ldapCertificate.validUntil;
+ },
+ ldapProtocol() {
+ return this.form.secureLdapEnabled ? 'ldaps://' : 'ldap://';
+ },
+ },
+ watch: {
+ isServiceEnabled: function (value) {
+ this.form.ldapAuthenticationEnabled = value;
+ },
+ isActiveDirectoryEnabled: function (value) {
+ this.form.activeDirectoryEnabled = value;
+ this.setFormValues();
+ },
+ },
+ validations: {
+ form: {
+ ldapAuthenticationEnabled: {},
+ secureLdapEnabled: {},
+ activeDirectoryEnabled: {
+ required: requiredIf(function () {
+ return this.form.ldapAuthenticationEnabled;
+ }),
+ },
+ serverUri: {
+ required: requiredIf(function () {
+ return this.form.ldapAuthenticationEnabled;
+ }),
+ },
+ bindDn: {
+ required: requiredIf(function () {
+ return this.form.ldapAuthenticationEnabled;
+ }),
+ },
+ bindPassword: {
+ required: requiredIf(function () {
+ return this.form.ldapAuthenticationEnabled;
+ }),
+ },
+ baseDn: {
+ required: requiredIf(function () {
+ return this.form.ldapAuthenticationEnabled;
+ }),
+ },
+ userIdAttribute: {},
+ groupIdAttribute: {},
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store
+ .dispatch('ldap/getAccountSettings')
+ .finally(() => this.endLoader());
+ this.$store
+ .dispatch('certificates/getCertificates')
+ .finally(() => this.endLoader());
+ this.setFormValues();
+ },
+ methods: {
+ setFormValues(serviceType) {
+ if (!serviceType) {
+ serviceType = this.isActiveDirectoryEnabled
+ ? this.activeDirectory
+ : this.ldap;
+ }
+ const {
+ serviceAddress = '',
+ bindDn = '',
+ baseDn = '',
+ userAttribute = '',
+ groupsAttribute = '',
+ } = serviceType;
+ const secureLdap =
+ serviceAddress && serviceAddress.includes('ldaps://') ? true : false;
+ const serverUri = serviceAddress
+ ? serviceAddress.replace(/ldaps?:\/\//, '')
+ : '';
+ this.form.secureLdapEnabled = secureLdap;
+ this.form.serverUri = serverUri;
+ this.form.bindDn = bindDn;
+ this.form.bindPassword = '';
+ this.form.baseDn = baseDn;
+ this.form.userIdAttribute = userAttribute;
+ this.form.groupIdAttribute = groupsAttribute;
+ },
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ const data = {
+ serviceEnabled: this.form.ldapAuthenticationEnabled,
+ activeDirectoryEnabled: this.form.activeDirectoryEnabled,
+ serviceAddress: `${this.ldapProtocol}${this.form.serverUri}`,
+ bindDn: this.form.bindDn,
+ bindPassword: this.form.bindPassword,
+ baseDn: this.form.baseDn,
+ userIdAttribute: this.form.userIdAttribute,
+ groupIdAttribute: this.form.groupIdAttribute,
+ };
+ this.startLoader();
+ this.$store
+ .dispatch('ldap/saveAccountSettings', data)
+ .then((success) => {
+ this.successToast(success);
+ })
+ .catch(({ message }) => {
+ this.errorToast(message);
+ })
+ .finally(() => {
+ this.form.bindPassword = '';
+ this.$v.form.$reset();
+ this.endLoader();
+ });
+ },
+ onChangeServiceType(isActiveDirectoryEnabled) {
+ this.$v.form.activeDirectoryEnabled.$touch();
+ const serviceType = isActiveDirectoryEnabled
+ ? this.activeDirectory
+ : this.ldap;
+ // Set form values according to user selected
+ // service type
+ this.setFormValues(serviceType);
+ },
+ onChangeldapAuthenticationEnabled(isServiceEnabled) {
+ this.$v.form.ldapAuthenticationEnabled.$touch();
+ if (!isServiceEnabled) {
+ // Request will fail if sent with empty values.
+ // The frontend only checks for required fields
+ // when the service is enabled. This is to prevent
+ // an error if a user clears any properties then
+ // disables the service.
+ this.setFormValues();
+ }
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue b/src/views/_sila/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue
new file mode 100644
index 00000000..6ea2561a
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue
@@ -0,0 +1,164 @@
+<template>
+ <b-modal id="modal-role-group" ref="modal" @ok="onOk" @hidden="resetForm">
+ <template #modal-title>
+ <template v-if="roleGroup">
+ {{ $t('pageLdap.modal.editRoleGroup') }}
+ </template>
+ <template v-else>
+ {{ $t('pageLdap.modal.addNewRoleGroup') }}
+ </template>
+ </template>
+ <b-container>
+ <b-row>
+ <b-col sm="8">
+ <b-form id="role-group" @submit.prevent="handleSubmit">
+ <!-- Edit role group -->
+ <template v-if="roleGroup !== null">
+ <dl class="mb-4">
+ <dt>{{ $t('pageLdap.modal.groupName') }}</dt>
+ <dd>{{ form.groupName }}</dd>
+ </dl>
+ </template>
+
+ <!-- Add new role group -->
+ <template v-else>
+ <b-form-group
+ :label="$t('pageLdap.modal.groupName')"
+ label-for="role-group-name"
+ >
+ <b-form-input
+ id="role-group-name"
+ v-model="form.groupName"
+ :state="getValidationState($v.form.groupName)"
+ @input="$v.form.groupName.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </template>
+
+ <b-form-group
+ :label="$t('pageLdap.modal.groupPrivilege')"
+ label-for="privilege"
+ >
+ <b-form-select
+ id="privilege"
+ v-model="form.groupPrivilege"
+ :options="accountRoles"
+ :state="getValidationState($v.form.groupPrivilege)"
+ @input="$v.form.groupPrivilege.$touch()"
+ >
+ <template v-if="!roleGroup" #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ {{ $t('global.form.fieldRequired') }}
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-form>
+ </b-col>
+ </b-row>
+ </b-container>
+ <template #modal-footer="{ cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button form="role-group" type="submit" variant="primary" @click="onOk">
+ <template v-if="roleGroup">
+ {{ $t('global.action.save') }}
+ </template>
+ <template v-else>
+ {{ $t('global.action.add') }}
+ </template>
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import { required, requiredIf } from 'vuelidate/lib/validators';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ roleGroup: {
+ type: Object,
+ default: null,
+ validator: (prop) => {
+ if (prop === null) return true;
+ return (
+ Object.prototype.hasOwnProperty.call(prop, 'groupName') &&
+ Object.prototype.hasOwnProperty.call(prop, 'groupPrivilege')
+ );
+ },
+ },
+ },
+ data() {
+ return {
+ form: {
+ groupName: null,
+ groupPrivilege: null,
+ },
+ };
+ },
+ computed: {
+ accountRoles() {
+ return this.$store.getters['userManagement/accountRoles'];
+ },
+ },
+ watch: {
+ roleGroup: function (value) {
+ if (value === null) return;
+ this.form.groupName = value.groupName;
+ this.form.groupPrivilege = value.groupPrivilege;
+ },
+ },
+ validations() {
+ return {
+ form: {
+ groupName: {
+ required: requiredIf(function () {
+ return !this.roleGroup;
+ }),
+ },
+ groupPrivilege: {
+ required,
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', {
+ addNew: !this.roleGroup,
+ groupName: this.form.groupName,
+ groupPrivilege: this.form.groupPrivilege,
+ });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.groupName = null;
+ this.form.groupPrivilege = null;
+ this.$v.$reset();
+ this.$emit('hidden');
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/Ldap/TableRoleGroups.vue b/src/views/_sila/SecurityAndAccess/Ldap/TableRoleGroups.vue
new file mode 100644
index 00000000..5ae3e3d1
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Ldap/TableRoleGroups.vue
@@ -0,0 +1,269 @@
+<template>
+ <div>
+ <b-row>
+ <b-col md="9">
+ <alert :show="isServiceEnabled === false" variant="info">
+ {{ $t('pageLdap.tableRoleGroups.alertContent') }}
+ </alert>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col class="text-right" md="9">
+ <b-btn
+ variant="primary"
+ :disabled="!isServiceEnabled"
+ @click="initRoleGroupModal(null)"
+ >
+ <icon-add />
+ {{ $t('pageLdap.addRoleGroup') }}
+ </b-btn>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col md="9">
+ <table-toolbar
+ ref="toolbar"
+ :selected-items-count="selectedRows.length"
+ :actions="batchActions"
+ @clear-selected="clearSelectedRows($refs.table)"
+ @batch-action="onBatchAction"
+ />
+ <b-table
+ ref="table"
+ responsive
+ selectable
+ show-empty
+ no-select-on-click
+ hover
+ no-sort-reset
+ sort-icon-left
+ :busy="isBusy"
+ :items="tableItems"
+ :fields="fields"
+ :empty-text="$t('global.table.emptyMessage')"
+ @row-selected="onRowSelected($event, tableItems.length)"
+ >
+ <!-- Checkbox column -->
+ <template #head(checkbox)>
+ <b-form-checkbox
+ v-model="tableHeaderCheckboxModel"
+ :indeterminate="tableHeaderCheckboxIndeterminate"
+ :disabled="!isServiceEnabled"
+ @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"
+ :disabled="!isServiceEnabled"
+ @change="toggleSelectRow($refs.table, row.index)"
+ >
+ <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+ </b-form-checkbox>
+ </template>
+
+ <!-- table actions column -->
+ <template #cell(actions)="{ item }">
+ <table-row-action
+ v-for="(action, index) in item.actions"
+ :key="index"
+ :value="action.value"
+ :enabled="action.enabled"
+ :title="action.title"
+ @click-table-action="onTableRowAction($event, item)"
+ >
+ <template #icon>
+ <icon-edit v-if="action.value === 'edit'" />
+ <icon-trashcan v-if="action.value === 'delete'" />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </b-col>
+ </b-row>
+ <modal-add-role-group
+ :role-group="activeRoleGroup"
+ @ok="saveRoleGroup"
+ @hidden="activeRoleGroup = null"
+ />
+ </div>
+</template>
+
+<script>
+import IconEdit from '@carbon/icons-vue/es/edit/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import { mapGetters } from 'vuex';
+
+import Alert from '@/components/Global/Alert';
+import TableToolbar from '@/components/Global/TableToolbar';
+import TableRowAction from '@/components/Global/TableRowAction';
+import BVTableSelectableMixin, {
+ selectedRows,
+ tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import ModalAddRoleGroup from './ModalAddRoleGroup';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+
+export default {
+ components: {
+ Alert,
+ IconAdd,
+ IconEdit,
+ IconTrashcan,
+ ModalAddRoleGroup,
+ TableRowAction,
+ TableToolbar,
+ },
+ mixins: [BVTableSelectableMixin, BVToastMixin, LoadingBarMixin],
+ data() {
+ return {
+ isBusy: true,
+ activeRoleGroup: null,
+ fields: [
+ {
+ key: 'checkbox',
+ sortable: false,
+ },
+ {
+ key: 'groupName',
+ sortable: true,
+ label: this.$t('pageLdap.tableRoleGroups.groupName'),
+ },
+ {
+ key: 'groupPrivilege',
+ sortable: true,
+ label: this.$t('pageLdap.tableRoleGroups.groupPrivilege'),
+ },
+ {
+ key: 'actions',
+ sortable: false,
+ label: '',
+ tdClass: 'text-right',
+ },
+ ],
+ batchActions: [
+ {
+ value: 'delete',
+ label: this.$t('global.action.delete'),
+ },
+ ],
+ selectedRows: selectedRows,
+ tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+ };
+ },
+ computed: {
+ ...mapGetters('ldap', ['isServiceEnabled', 'enabledRoleGroups']),
+ tableItems() {
+ return this.enabledRoleGroups.map(({ LocalRole, RemoteGroup }) => {
+ return {
+ groupName: RemoteGroup,
+ groupPrivilege: LocalRole,
+ actions: [
+ {
+ value: 'edit',
+ title: this.$t('global.action.edit'),
+ enabled: this.isServiceEnabled,
+ },
+ {
+ value: 'delete',
+ title: this.$t('global.action.delete'),
+ enabled: this.isServiceEnabled,
+ },
+ ],
+ };
+ });
+ },
+ },
+ created() {
+ this.$store.dispatch('userManagement/getAccountRoles').finally(() => {
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ onBatchAction() {
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$tc(
+ 'pageLdap.modal.deleteRoleGroupBatchConfirmMessage',
+ this.selectedRows.length
+ ),
+ {
+ title: this.$t('pageLdap.modal.deleteRoleGroup'),
+ okTitle: this.$t('global.action.delete'),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) {
+ this.startLoader();
+ this.$store
+ .dispatch('ldap/deleteRoleGroup', {
+ roleGroups: this.selectedRows,
+ })
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ }
+ });
+ },
+ onTableRowAction(action, row) {
+ switch (action) {
+ case 'edit':
+ this.initRoleGroupModal(row);
+ break;
+ case 'delete':
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$t('pageLdap.modal.deleteRoleGroupConfirmMessage', {
+ groupName: row.groupName,
+ }),
+ {
+ title: this.$t('pageLdap.modal.deleteRoleGroup'),
+ okTitle: this.$t('global.action.delete'),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) {
+ this.startLoader();
+ this.$store
+ .dispatch('ldap/deleteRoleGroup', { roleGroups: [row] })
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ }
+ });
+ break;
+ }
+ },
+ initRoleGroupModal(roleGroup) {
+ this.activeRoleGroup = roleGroup;
+ this.$bvModal.show('modal-role-group');
+ },
+ saveRoleGroup({ addNew, groupName, groupPrivilege }) {
+ this.activeRoleGroup = null;
+ const data = { groupName, groupPrivilege };
+ this.startLoader();
+ if (addNew) {
+ this.$store
+ .dispatch('ldap/addNewRoleGroup', data)
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ } else {
+ this.$store
+ .dispatch('ldap/saveRoleGroup', data)
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ }
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/Ldap/index.js b/src/views/_sila/SecurityAndAccess/Ldap/index.js
new file mode 100644
index 00000000..6ae3abfc
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Ldap/index.js
@@ -0,0 +1,2 @@
+import Ldap from './Ldap.vue';
+export default Ldap;
diff --git a/src/views/_sila/SecurityAndAccess/Policies/Policies.vue b/src/views/_sila/SecurityAndAccess/Policies/Policies.vue
new file mode 100644
index 00000000..1dc197c7
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Policies/Policies.vue
@@ -0,0 +1,213 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row>
+ <b-col md="8">
+ <b-row v-if="!modifySSHPolicyDisabled" class="setting-section">
+ <b-col class="d-flex align-items-center justify-content-between">
+ <dl class="mr-3 w-75">
+ <dt>{{ $t('pagePolicies.ssh') }}</dt>
+ <dd>
+ {{ $t('pagePolicies.sshDescription') }}
+ </dd>
+ </dl>
+ <b-form-checkbox
+ id="sshSwitch"
+ v-model="sshProtocolState"
+ data-test-id="policies-toggle-bmcShell"
+ switch
+ @change="changeSshProtocolState"
+ >
+ <span class="sr-only">
+ {{ $t('pagePolicies.ssh') }}
+ </span>
+ <span v-if="sshProtocolState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </b-col>
+ </b-row>
+ <b-row class="setting-section">
+ <b-col class="d-flex align-items-center justify-content-between">
+ <dl class="mt-3 mr-3 w-75">
+ <dt>{{ $t('pagePolicies.ipmi') }}</dt>
+ <dd>
+ {{ $t('pagePolicies.ipmiDescription') }}
+ </dd>
+ </dl>
+ <b-form-checkbox
+ id="ipmiSwitch"
+ v-model="ipmiProtocolState"
+ data-test-id="polices-toggle-networkIpmi"
+ switch
+ @change="changeIpmiProtocolState"
+ >
+ <span class="sr-only">
+ {{ $t('pagePolicies.ipmi') }}
+ </span>
+ <span v-if="ipmiProtocolState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </b-col>
+ </b-row>
+ <b-row class="setting-section">
+ <b-col class="d-flex align-items-center justify-content-between">
+ <dl class="mt-3 mr-3 w-75">
+ <dt>{{ $t('pagePolicies.vtpm') }}</dt>
+ <dd>
+ {{ $t('pagePolicies.vtpmDescription') }}
+ </dd>
+ </dl>
+ <b-form-checkbox
+ id="vtpmSwitch"
+ v-model="vtpmState"
+ data-test-id="policies-toggle-vtpm"
+ switch
+ @change="changeVtpmState"
+ >
+ <span class="sr-only">
+ {{ $t('pagePolicies.vtpm') }}
+ </span>
+ <span v-if="vtpmState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </b-col>
+ </b-row>
+ <b-row class="setting-section">
+ <b-col class="d-flex align-items-center justify-content-between">
+ <dl class="mt-3 mr-3 w-75">
+ <dt>{{ $t('pagePolicies.rtad') }}</dt>
+ <dd>
+ {{ $t('pagePolicies.rtadDescription') }}
+ </dd>
+ </dl>
+ <b-form-checkbox
+ id="rtadSwitch"
+ v-model="rtadState"
+ data-test-id="policies-toggle-rtad"
+ switch
+ @change="changeRtadState"
+ >
+ <span class="sr-only">
+ {{ $t('pagePolicies.rtad') }}
+ </span>
+ <span v-if="rtadState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </b-col>
+ </b-row>
+ </b-col>
+ </b-row>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+export default {
+ name: 'Policies',
+ components: { PageTitle },
+ mixins: [LoadingBarMixin, BVToastMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ modifySSHPolicyDisabled:
+ process.env.VUE_APP_MODIFY_SSH_POLICY_DISABLED === 'true',
+ };
+ },
+ computed: {
+ sshProtocolState: {
+ get() {
+ return this.$store.getters['policies/sshProtocolEnabled'];
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ ipmiProtocolState: {
+ get() {
+ return this.$store.getters['policies/ipmiProtocolEnabled'];
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ rtadState: {
+ get() {
+ if (this.$store.getters['policies/rtadEnabled'] === 'Enabled') {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ vtpmState: {
+ get() {
+ if (this.$store.getters['policies/vtpmEnabled'] === 'Enabled') {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ },
+ created() {
+ this.startLoader();
+ Promise.all([
+ this.$store.dispatch('policies/getBiosStatus'),
+ this.$store.dispatch('policies/getNetworkProtocolStatus'),
+ ]).finally(() => this.endLoader());
+ },
+ methods: {
+ changeIpmiProtocolState(state) {
+ this.$store
+ .dispatch('policies/saveIpmiProtocolState', state)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ changeSshProtocolState(state) {
+ this.$store
+ .dispatch('policies/saveSshProtocolState', state)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ changeRtadState(state) {
+ this.$store
+ .dispatch('policies/saveRtadState', state ? 'Enabled' : 'Disabled')
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ changeVtpmState(state) {
+ this.$store
+ .dispatch('policies/saveVtpmState', state ? 'Enabled' : 'Disabled')
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.setting-section {
+ border-bottom: 1px solid gray('300');
+}
+</style>
diff --git a/src/views/_sila/SecurityAndAccess/Policies/index.js b/src/views/_sila/SecurityAndAccess/Policies/index.js
new file mode 100644
index 00000000..77023908
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Policies/index.js
@@ -0,0 +1,2 @@
+import Policies from './Policies.vue';
+export default Policies;
diff --git a/src/views/_sila/SecurityAndAccess/Sessions/Sessions.vue b/src/views/_sila/SecurityAndAccess/Sessions/Sessions.vue
new file mode 100644
index 00000000..07ee725d
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Sessions/Sessions.vue
@@ -0,0 +1,294 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row class="align-items-end">
+ <b-col sm="6" md="5" xl="4">
+ <search
+ :placeholder="$t('pageSessions.table.searchSessions')"
+ data-test-id="sessions-input-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"
+ :busy="isBusy"
+ :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="sessions-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="`sessions-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"
+ :btn-icon-only="false"
+ :data-test-id="`sessions-button-disconnect-${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(filteredRows)"
+ 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 {
+ isBusy: true,
+ fields: [
+ {
+ key: 'checkbox',
+ },
+ {
+ key: 'clientID',
+ label: this.$t('pageSessions.table.clientID'),
+ },
+ {
+ key: 'username',
+ label: this.$t('pageSessions.table.username'),
+ },
+ {
+ key: 'ipAddress',
+ label: this.$t('pageSessions.table.ipAddress'),
+ },
+ {
+ key: 'actions',
+ label: '',
+ },
+ ],
+ batchActions: [
+ {
+ value: 'disconnect',
+ label: this.$t('pageSessions.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['sessions/allConnections'].map((session) => {
+ return {
+ ...session,
+ actions: [
+ {
+ value: 'disconnect',
+ title: this.$t('pageSessions.action.disconnect'),
+ },
+ ],
+ };
+ });
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store.dispatch('sessions/getSessionsData').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ },
+ methods: {
+ onFiltered(filteredItems) {
+ this.searchTotalFilteredRows = filteredItems.length;
+ },
+ onChangeSearchInput(event) {
+ this.searchFilter = event;
+ },
+ disconnectSessions(uris) {
+ this.$store
+ .dispatch('sessions/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('pageSessions.modal.disconnectMessage'), {
+ title: this.$tc('pageSessions.modal.disconnectTitle'),
+ okTitle: this.$t('pageSessions.action.disconnect'),
+ cancelTitle: this.$t('global.action.cancel'),
+ })
+ .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(
+ 'pageSessions.modal.disconnectMessage',
+ this.selectedRows.length
+ ),
+ {
+ title: this.$tc(
+ 'pageSessions.modal.disconnectTitle',
+ this.selectedRows.length
+ ),
+ okTitle: this.$t('pageSessions.action.disconnect'),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .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/_sila/SecurityAndAccess/Sessions/index.js b/src/views/_sila/SecurityAndAccess/Sessions/index.js
new file mode 100644
index 00000000..aa113aff
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/Sessions/index.js
@@ -0,0 +1,2 @@
+import Sessions from './Sessions.vue';
+export default Sessions;
diff --git a/src/views/_sila/SecurityAndAccess/UserManagement/ModalSettings.vue b/src/views/_sila/SecurityAndAccess/UserManagement/ModalSettings.vue
new file mode 100644
index 00000000..0f05123c
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/UserManagement/ModalSettings.vue
@@ -0,0 +1,215 @@
+<template>
+ <b-modal
+ id="modal-settings"
+ ref="modal"
+ :title="$t('pageUserManagement.accountPolicySettings')"
+ @hidden="resetForm"
+ >
+ <b-form id="form-settings" novalidate @submit.prevent="handleSubmit">
+ <b-container>
+ <b-row>
+ <b-col>
+ <b-form-group
+ :label="$t('pageUserManagement.modal.maxFailedLoginAttempts')"
+ label-for="lockout-threshold"
+ >
+ <b-form-text id="lockout-threshold-help-block">
+ {{
+ $t('global.form.valueMustBeBetween', {
+ min: 0,
+ max: 65535,
+ })
+ }}
+ </b-form-text>
+ <b-form-input
+ id="lockout-threshold"
+ v-model.number="form.lockoutThreshold"
+ type="number"
+ aria-describedby="lockout-threshold-help-block"
+ data-test-id="userManagement-input-lockoutThreshold"
+ :state="getValidationState($v.form.lockoutThreshold)"
+ @input="$v.form.lockoutThreshold.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.lockoutThreshold.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template
+ v-if="
+ !$v.form.lockoutThreshold.minLength ||
+ !$v.form.lockoutThreshold.maxLength
+ "
+ >
+ {{
+ $t('global.form.valueMustBeBetween', {
+ min: 0,
+ max: 65535,
+ })
+ }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col>
+ <b-form-group
+ :label="$t('pageUserManagement.modal.userUnlockMethod')"
+ >
+ <b-form-radio
+ v-model="form.unlockMethod"
+ name="unlock-method"
+ class="mb-2"
+ :value="0"
+ data-test-id="userManagement-radio-manualUnlock"
+ @input="$v.form.unlockMethod.$touch()"
+ >
+ {{ $t('pageUserManagement.modal.manual') }}
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.unlockMethod"
+ name="unlock-method"
+ :value="1"
+ data-test-id="userManagement-radio-automaticUnlock"
+ @input="$v.form.unlockMethod.$touch()"
+ >
+ {{ $t('pageUserManagement.modal.automaticAfterTimeout') }}
+ </b-form-radio>
+ <div class="mt-3 ml-4">
+ <b-form-text id="lockout-duration-help-block">
+ {{ $t('pageUserManagement.modal.timeoutDurationSeconds') }}
+ </b-form-text>
+ <b-form-input
+ v-model.number="form.lockoutDuration"
+ aria-describedby="lockout-duration-help-block"
+ type="number"
+ data-test-id="userManagement-input-lockoutDuration"
+ :state="getValidationState($v.form.lockoutDuration)"
+ :readonly="$v.form.unlockMethod.$model === 0"
+ @input="$v.form.lockoutDuration.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.lockoutDuration.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-else-if="!$v.form.lockoutDuration.minvalue">
+ {{ $t('global.form.mustBeAtLeast', { value: 1 }) }}
+ </template>
+ </b-form-invalid-feedback>
+ </div>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-container>
+ </b-form>
+ <template #modal-footer="{ cancel }">
+ <b-button
+ variant="secondary"
+ data-test-id="userManagement-button-cancel"
+ @click="cancel()"
+ >
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button
+ form="form-settings"
+ type="submit"
+ variant="primary"
+ data-test-id="userManagement-button-submit"
+ @click="onOk"
+ >
+ {{ $t('global.action.save') }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import {
+ required,
+ requiredIf,
+ minValue,
+ maxValue,
+} from 'vuelidate/lib/validators';
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ form: {
+ lockoutThreshold: 0,
+ unlockMethod: 0,
+ lockoutDuration: null,
+ },
+ };
+ },
+ watch: {
+ settings: function ({ lockoutThreshold, lockoutDuration }) {
+ this.form.lockoutThreshold = lockoutThreshold;
+ this.form.unlockMethod = lockoutDuration ? 1 : 0;
+ this.form.lockoutDuration = lockoutDuration ? lockoutDuration : null;
+ },
+ },
+ validations: {
+ form: {
+ lockoutThreshold: {
+ minValue: minValue(0),
+ maxValue: maxValue(65535),
+ required,
+ },
+ unlockMethod: { required },
+ lockoutDuration: {
+ minValue: function (value) {
+ return this.form.unlockMethod === 0 || value > 0;
+ },
+ required: requiredIf(function () {
+ return this.form.unlockMethod === 1;
+ }),
+ },
+ },
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+
+ let lockoutThreshold;
+ let lockoutDuration;
+ if (this.$v.form.lockoutThreshold.$dirty) {
+ lockoutThreshold = this.form.lockoutThreshold;
+ }
+ if (this.$v.form.unlockMethod.$dirty) {
+ lockoutDuration = this.form.unlockMethod
+ ? this.form.lockoutDuration
+ : 0;
+ }
+
+ this.$emit('ok', { lockoutThreshold, lockoutDuration });
+ this.closeModal();
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ // Reset form models
+ this.form.lockoutThreshold = this.settings.lockoutThreshold;
+ this.form.unlockMethod = this.settings.lockoutDuration ? 1 : 0;
+ this.form.lockoutDuration = this.settings.lockoutDuration
+ ? this.settings.lockoutDuration
+ : null;
+ this.$v.$reset(); // clear validations
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/UserManagement/ModalUser.vue b/src/views/_sila/SecurityAndAccess/UserManagement/ModalUser.vue
new file mode 100644
index 00000000..0f8757ce
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/UserManagement/ModalUser.vue
@@ -0,0 +1,386 @@
+<template>
+ <b-modal id="modal-user" ref="modal" @hidden="resetForm">
+ <template #modal-title>
+ <template v-if="newUser">
+ {{ $t('pageUserManagement.addUser') }}
+ </template>
+ <template v-else>
+ {{ $t('pageUserManagement.editUser') }}
+ </template>
+ </template>
+ <b-form id="form-user" novalidate @submit.prevent="handleSubmit">
+ <b-container>
+ <!-- Manual unlock form control -->
+ <b-row v-if="!newUser && manualUnlockPolicy && user.Locked">
+ <b-col sm="9">
+ <alert :show="true" variant="warning" small>
+ <template v-if="!$v.form.manualUnlock.$dirty">
+ {{ $t('pageUserManagement.modal.accountLocked') }}
+ </template>
+ <template v-else>
+ {{ $t('pageUserManagement.modal.clickSaveToUnlockAccount') }}
+ </template>
+ </alert>
+ </b-col>
+ <b-col sm="3">
+ <input
+ v-model="form.manualUnlock"
+ data-test-id="userManagement-input-manualUnlock"
+ type="hidden"
+ value="false"
+ />
+ <b-button
+ variant="primary"
+ :disabled="$v.form.manualUnlock.$dirty"
+ data-test-id="userManagement-button-manualUnlock"
+ @click="$v.form.manualUnlock.$touch()"
+ >
+ {{ $t('pageUserManagement.modal.unlock') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col>
+ <b-form-group :label="$t('pageUserManagement.modal.accountStatus')">
+ <b-form-radio
+ v-model="form.status"
+ name="user-status"
+ :value="true"
+ data-test-id="userManagement-radioButton-statusEnabled"
+ @input="$v.form.status.$touch()"
+ >
+ {{ $t('global.status.enabled') }}
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.status"
+ name="user-status"
+ data-test-id="userManagement-radioButton-statusDisabled"
+ :value="false"
+ @input="$v.form.status.$touch()"
+ >
+ {{ $t('global.status.disabled') }}
+ </b-form-radio>
+ </b-form-group>
+ <b-form-group
+ :label="$t('pageUserManagement.modal.username')"
+ label-for="username"
+ >
+ <b-form-text id="username-help-block">
+ {{ $t('pageUserManagement.modal.cannotStartWithANumber') }}
+ <br />
+ {{
+ $t(
+ 'pageUserManagement.modal.noSpecialCharactersExceptUnderscore'
+ )
+ }}
+ </b-form-text>
+ <b-form-input
+ id="username"
+ v-model="form.username"
+ type="text"
+ aria-describedby="username-help-block"
+ data-test-id="userManagement-input-username"
+ :state="getValidationState($v.form.username)"
+ :disabled="!newUser && originalUsername === 'root'"
+ @input="$v.form.username.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.username.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-else-if="!$v.form.username.maxLength">
+ {{
+ $t('global.form.lengthMustBeBetween', { min: 1, max: 16 })
+ }}
+ </template>
+ <template v-else-if="!$v.form.username.pattern">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ <b-form-group
+ :label="$t('pageUserManagement.modal.privilege')"
+ label-for="privilege"
+ >
+ <b-form-select
+ id="privilege"
+ v-model="form.privilege"
+ :options="privilegeTypes"
+ data-test-id="userManagement-select-privilege"
+ :state="getValidationState($v.form.privilege)"
+ @input="$v.form.privilege.$touch()"
+ >
+ <template #first>
+ <b-form-select-option :value="null" disabled>
+ {{ $t('global.form.selectAnOption') }}
+ </b-form-select-option>
+ </template>
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.privilege.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col>
+ <b-form-group
+ :label="$t('pageUserManagement.modal.userPassword')"
+ label-for="password"
+ >
+ <b-form-text id="password-help-block">
+ {{
+ $t('pageUserManagement.modal.passwordMustBeBetween', {
+ min: passwordRequirements.minLength,
+ max: passwordRequirements.maxLength,
+ })
+ }}
+ </b-form-text>
+ <input-password-toggle>
+ <b-form-input
+ id="password"
+ v-model="form.password"
+ type="password"
+ data-test-id="userManagement-input-password"
+ aria-describedby="password-help-block"
+ :state="getValidationState($v.form.password)"
+ class="form-control-with-button"
+ @input="$v.form.password.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.password.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template
+ v-if="
+ !$v.form.password.minLength || !$v.form.password.maxLength
+ "
+ >
+ {{
+ $t('pageUserManagement.modal.passwordMustBeBetween', {
+ min: passwordRequirements.minLength,
+ max: passwordRequirements.maxLength,
+ })
+ }}
+ </template>
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ <b-form-group
+ :label="$t('pageUserManagement.modal.confirmUserPassword')"
+ label-for="password-confirmation"
+ >
+ <input-password-toggle>
+ <b-form-input
+ id="password-confirmation"
+ v-model="form.passwordConfirmation"
+ data-test-id="userManagement-input-passwordConfirmation"
+ type="password"
+ :state="getValidationState($v.form.passwordConfirmation)"
+ class="form-control-with-button"
+ @input="$v.form.passwordConfirmation.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.passwordConfirmation.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template
+ v-else-if="!$v.form.passwordConfirmation.sameAsPassword"
+ >
+ {{ $t('pageUserManagement.modal.passwordsDoNotMatch') }}
+ </template>
+ </b-form-invalid-feedback>
+ </input-password-toggle>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-container>
+ </b-form>
+ <template #modal-footer="{ cancel }">
+ <b-button
+ variant="secondary"
+ data-test-id="userManagement-button-cancel"
+ @click="cancel()"
+ >
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button
+ form="form-user"
+ data-test-id="userManagement-button-submit"
+ type="submit"
+ variant="primary"
+ @click="onOk"
+ >
+ <template v-if="newUser">
+ {{ $t('pageUserManagement.addUser') }}
+ </template>
+ <template v-else>
+ {{ $t('global.action.save') }}
+ </template>
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import {
+ required,
+ maxLength,
+ minLength,
+ sameAs,
+ helpers,
+ requiredIf,
+} from 'vuelidate/lib/validators';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+import Alert from '@/components/Global/Alert';
+
+export default {
+ components: { Alert, InputPasswordToggle },
+ mixins: [VuelidateMixin],
+ props: {
+ user: {
+ type: Object,
+ default: null,
+ },
+ passwordRequirements: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ originalUsername: '',
+ form: {
+ status: true,
+ username: '',
+ privilege: null,
+ password: '',
+ passwordConfirmation: '',
+ manualUnlock: false,
+ },
+ };
+ },
+ computed: {
+ newUser() {
+ return this.user ? false : true;
+ },
+ accountSettings() {
+ return this.$store.getters['userManagement/accountSettings'];
+ },
+ manualUnlockPolicy() {
+ return !this.accountSettings.accountLockoutDuration;
+ },
+ privilegeTypes() {
+ return this.$store.getters['userManagement/accountRoles'];
+ },
+ },
+ watch: {
+ user: function (value) {
+ if (value === null) return;
+ this.originalUsername = value.username;
+ this.form.username = value.username;
+ this.form.status = value.Enabled;
+ this.form.privilege = value.privilege;
+ },
+ },
+ validations() {
+ return {
+ form: {
+ status: {
+ required,
+ },
+ username: {
+ required,
+ maxLength: maxLength(16),
+ pattern: helpers.regex('pattern', /^([a-zA-Z_][a-zA-Z0-9_]*)/),
+ },
+ privilege: {
+ required,
+ },
+ password: {
+ required: requiredIf(function () {
+ return this.requirePassword();
+ }),
+ minLength: minLength(this.passwordRequirements.minLength),
+ maxLength: maxLength(this.passwordRequirements.maxLength),
+ },
+ passwordConfirmation: {
+ required: requiredIf(function () {
+ return this.requirePassword();
+ }),
+ sameAsPassword: sameAs('password'),
+ },
+ manualUnlock: {},
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ let userData = {};
+
+ if (this.newUser) {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ userData.username = this.form.username;
+ userData.status = this.form.status;
+ userData.privilege = this.form.privilege;
+ userData.password = this.form.password;
+ } else {
+ if (this.$v.$invalid) return;
+ userData.originalUsername = this.originalUsername;
+ if (this.$v.form.status.$dirty) {
+ userData.status = this.form.status;
+ }
+ if (this.$v.form.username.$dirty) {
+ userData.username = this.form.username;
+ }
+ if (this.$v.form.privilege.$dirty) {
+ userData.privilege = this.form.privilege;
+ }
+ if (this.$v.form.password.$dirty) {
+ userData.password = this.form.password;
+ }
+ if (this.$v.form.manualUnlock.$dirty) {
+ // If form manualUnlock control $dirty then
+ // set user Locked property to false
+ userData.locked = false;
+ }
+ if (Object.entries(userData).length === 1) {
+ this.closeModal();
+ return;
+ }
+ }
+
+ this.$emit('ok', { isNewUser: this.newUser, userData });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.originalUsername = '';
+ this.form.status = true;
+ this.form.username = '';
+ this.form.privilege = null;
+ this.form.password = '';
+ this.form.passwordConfirmation = '';
+ this.$v.$reset();
+ this.$emit('hidden');
+ },
+ requirePassword() {
+ if (this.newUser) return true;
+ if (this.$v.form.password.$dirty) return true;
+ if (this.$v.form.passwordConfirmation.$dirty) return true;
+ return false;
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/UserManagement/TableRoles.vue b/src/views/_sila/SecurityAndAccess/UserManagement/TableRoles.vue
new file mode 100644
index 00000000..61ef1ee8
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/UserManagement/TableRoles.vue
@@ -0,0 +1,92 @@
+<template>
+ <b-table stacked="sm" hover small :items="items" :fields="fields">
+ <template #cell(administrator)="data">
+ <template v-if="data.value">
+ <checkmark20 />
+ </template>
+ </template>
+ <template #cell(operator)="data">
+ <template v-if="data.value">
+ <checkmark20 />
+ </template>
+ </template>
+ <template #cell(readonly)="data">
+ <template v-if="data.value">
+ <checkmark20 />
+ </template>
+ </template>
+ <template #cell(noaccess)="data">
+ <template v-if="data.value">
+ <checkmark20 />
+ </template>
+ </template>
+ </b-table>
+</template>
+
+<script>
+import Checkmark20 from '@carbon/icons-vue/es/checkmark/20';
+
+export default {
+ components: {
+ Checkmark20,
+ },
+ data() {
+ return {
+ items: [
+ {
+ description: this.$t(
+ 'pageUserManagement.tableRoles.configureComponentsManagedByThisService'
+ ),
+ administrator: true,
+ operator: true,
+ readonly: false,
+ noaccess: false,
+ },
+ {
+ description: this.$t(
+ 'pageUserManagement.tableRoles.configureManagerResources'
+ ),
+ administrator: true,
+ operator: false,
+ readonly: false,
+ noaccess: false,
+ },
+ {
+ description: this.$t(
+ 'pageUserManagement.tableRoles.updatePasswordForCurrentUserAccount'
+ ),
+ administrator: true,
+ operator: true,
+ readonly: true,
+ noaccess: false,
+ },
+ {
+ description: this.$t(
+ 'pageUserManagement.tableRoles.configureUsersAndTheirAccounts'
+ ),
+ administrator: true,
+ operator: false,
+ readonly: false,
+ noaccess: false,
+ },
+ {
+ description: this.$t(
+ 'pageUserManagement.tableRoles.logInToTheServiceAndReadResources'
+ ),
+ administrator: true,
+ operator: true,
+ readonly: true,
+ noaccess: false,
+ },
+ ],
+ fields: [
+ { key: 'description', label: 'Privilege' },
+ { key: 'administrator', label: 'Administrator', class: 'text-center' },
+ { key: 'operator', label: 'Operator', class: 'text-center' },
+ { key: 'readonly', label: 'ReadOnly', class: 'text-center' },
+ { key: 'noaccess', label: 'NoAccess', class: 'text-center' },
+ ],
+ };
+ },
+};
+</script>
diff --git a/src/views/_sila/SecurityAndAccess/UserManagement/UserManagement.vue b/src/views/_sila/SecurityAndAccess/UserManagement/UserManagement.vue
new file mode 100644
index 00000000..c6c556c8
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/UserManagement/UserManagement.vue
@@ -0,0 +1,391 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row>
+ <b-col xl="9" class="text-right">
+ <b-button variant="link" @click="initModalSettings">
+ <icon-settings />
+ {{ $t('pageUserManagement.accountPolicySettings') }}
+ </b-button>
+ <b-button
+ variant="primary"
+ data-test-id="userManagement-button-addUser"
+ @click="initModalUser(null)"
+ >
+ <icon-add />
+ {{ $t('pageUserManagement.addUser') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="9">
+ <table-toolbar
+ ref="toolbar"
+ :selected-items-count="selectedRows.length"
+ :actions="tableToolbarActions"
+ @clear-selected="clearSelectedRows($refs.table)"
+ @batch-action="onBatchAction"
+ />
+ <b-table
+ ref="table"
+ responsive="md"
+ selectable
+ show-empty
+ no-select-on-click
+ hover
+ :busy="isBusy"
+ :fields="fields"
+ :items="tableItems"
+ :empty-text="$t('global.table.emptyMessage')"
+ @row-selected="onRowSelected($event, tableItems.length)"
+ >
+ <!-- Checkbox column -->
+ <template #head(checkbox)>
+ <b-form-checkbox
+ v-model="tableHeaderCheckboxModel"
+ data-test-id="userManagement-checkbox-tableHeaderCheckbox"
+ :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="userManagement-checkbox-toggleSelectRow"
+ @change="toggleSelectRow($refs.table, row.index)"
+ >
+ <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+ </b-form-checkbox>
+ </template>
+
+ <!-- table actions column -->
+ <template #cell(actions)="{ item }">
+ <table-row-action
+ v-for="(action, index) in item.actions"
+ :key="index"
+ :value="action.value"
+ :enabled="action.enabled"
+ :title="action.title"
+ @click-table-action="onTableRowAction($event, item)"
+ >
+ <template #icon>
+ <icon-edit
+ v-if="action.value === 'edit'"
+ :data-test-id="`userManagement-tableRowAction-edit-${index}`"
+ />
+ <icon-trashcan
+ v-if="action.value === 'delete'"
+ :data-test-id="`userManagement-tableRowAction-delete-${index}`"
+ />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col xl="8">
+ <b-button
+ v-b-toggle.collapse-role-table
+ data-test-id="userManagement-button-viewPrivilegeRoleDescriptions"
+ variant="link"
+ class="mt-3"
+ >
+ <icon-chevron />
+ {{ $t('pageUserManagement.viewPrivilegeRoleDescriptions') }}
+ </b-button>
+ <b-collapse id="collapse-role-table" class="mt-3">
+ <table-roles />
+ </b-collapse>
+ </b-col>
+ </b-row>
+ <!-- Modals -->
+ <modal-settings :settings="settings" @ok="saveAccountSettings" />
+ <modal-user
+ :user="activeUser"
+ :password-requirements="passwordRequirements"
+ @ok="saveUser"
+ @hidden="activeUser = null"
+ />
+ </b-container>
+</template>
+
+<script>
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import IconEdit from '@carbon/icons-vue/es/edit/20';
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconSettings from '@carbon/icons-vue/es/settings/20';
+import IconChevron from '@carbon/icons-vue/es/chevron--up/20';
+
+import ModalUser from './ModalUser';
+import ModalSettings from './ModalSettings';
+import PageTitle from '@/components/Global/PageTitle';
+import TableRoles from './TableRoles';
+import TableToolbar from '@/components/Global/TableToolbar';
+import TableRowAction from '@/components/Global/TableRowAction';
+
+import BVTableSelectableMixin, {
+ selectedRows,
+ tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+
+export default {
+ name: 'UserManagement',
+ components: {
+ IconAdd,
+ IconChevron,
+ IconEdit,
+ IconSettings,
+ IconTrashcan,
+ ModalSettings,
+ ModalUser,
+ PageTitle,
+ TableRoles,
+ TableRowAction,
+ TableToolbar,
+ },
+ mixins: [BVTableSelectableMixin, BVToastMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ isBusy: true,
+ activeUser: null,
+ fields: [
+ {
+ key: 'checkbox',
+ },
+ {
+ key: 'username',
+ label: this.$t('pageUserManagement.table.username'),
+ },
+ {
+ key: 'privilege',
+ label: this.$t('pageUserManagement.table.privilege'),
+ },
+ {
+ key: 'status',
+ label: this.$t('pageUserManagement.table.status'),
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'text-right text-nowrap',
+ },
+ ],
+ tableToolbarActions: [
+ {
+ value: 'delete',
+ label: this.$t('global.action.delete'),
+ },
+ {
+ value: 'enable',
+ label: this.$t('global.action.enable'),
+ },
+ {
+ value: 'disable',
+ label: this.$t('global.action.disable'),
+ },
+ ],
+ selectedRows: selectedRows,
+ tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+ tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+ };
+ },
+ computed: {
+ allUsers() {
+ return this.$store.getters['userManagement/allUsers'];
+ },
+ tableItems() {
+ // transform user data to table data
+ return this.allUsers.map((user) => {
+ return {
+ username: user.UserName,
+ privilege: user.RoleId,
+ status: user.Locked
+ ? 'Locked'
+ : user.Enabled
+ ? 'Enabled'
+ : 'Disabled',
+ actions: [
+ {
+ value: 'edit',
+ enabled: true,
+ title: this.$t('pageUserManagement.editUser'),
+ },
+ {
+ value: 'delete',
+ enabled: user.UserName === 'root' ? false : true,
+ title: this.$tc('pageUserManagement.deleteUser'),
+ },
+ ],
+ ...user,
+ };
+ });
+ },
+ settings() {
+ return this.$store.getters['userManagement/accountSettings'];
+ },
+ passwordRequirements() {
+ return this.$store.getters['userManagement/accountPasswordRequirements'];
+ },
+ },
+ created() {
+ this.startLoader();
+ this.$store.dispatch('userManagement/getUsers').finally(() => {
+ this.endLoader();
+ this.isBusy = false;
+ });
+ this.$store.dispatch('userManagement/getAccountSettings');
+ this.$store.dispatch('userManagement/getAccountRoles');
+ },
+ methods: {
+ initModalUser(user) {
+ this.activeUser = user;
+ this.$bvModal.show('modal-user');
+ },
+ initModalDelete(user) {
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$t('pageUserManagement.modal.deleteConfirmMessage', {
+ user: user.username,
+ }),
+ {
+ title: this.$tc('pageUserManagement.deleteUser'),
+ okTitle: this.$tc('pageUserManagement.deleteUser'),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) {
+ this.deleteUser(user);
+ }
+ });
+ },
+ initModalSettings() {
+ this.$bvModal.show('modal-settings');
+ },
+ saveUser({ isNewUser, userData }) {
+ this.startLoader();
+ if (isNewUser) {
+ this.$store
+ .dispatch('userManagement/createUser', userData)
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ } else {
+ this.$store
+ .dispatch('userManagement/updateUser', userData)
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ }
+ },
+ deleteUser({ username }) {
+ this.startLoader();
+ this.$store
+ .dispatch('userManagement/deleteUser', username)
+ .then((success) => this.successToast(success))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ onBatchAction(action) {
+ switch (action) {
+ case 'delete':
+ this.$bvModal
+ .msgBoxConfirm(
+ this.$tc(
+ 'pageUserManagement.modal.batchDeleteConfirmMessage',
+ this.selectedRows.length
+ ),
+ {
+ title: this.$tc(
+ 'pageUserManagement.deleteUser',
+ this.selectedRows.length
+ ),
+ okTitle: this.$tc(
+ 'pageUserManagement.deleteUser',
+ this.selectedRows.length
+ ),
+ cancelTitle: this.$t('global.action.cancel'),
+ }
+ )
+ .then((deleteConfirmed) => {
+ if (deleteConfirmed) {
+ this.startLoader();
+ this.$store
+ .dispatch('userManagement/deleteUsers', this.selectedRows)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') this.successToast(message);
+ if (type === 'error') this.errorToast(message);
+ });
+ })
+ .finally(() => this.endLoader());
+ }
+ });
+ break;
+ case 'enable':
+ this.startLoader();
+ this.$store
+ .dispatch('userManagement/enableUsers', this.selectedRows)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') this.successToast(message);
+ if (type === 'error') this.errorToast(message);
+ });
+ })
+ .finally(() => this.endLoader());
+ break;
+ case 'disable':
+ this.startLoader();
+ this.$store
+ .dispatch('userManagement/disableUsers', this.selectedRows)
+ .then((messages) => {
+ messages.forEach(({ type, message }) => {
+ if (type === 'success') this.successToast(message);
+ if (type === 'error') this.errorToast(message);
+ });
+ })
+ .finally(() => this.endLoader());
+ break;
+ }
+ },
+ onTableRowAction(action, row) {
+ switch (action) {
+ case 'edit':
+ this.initModalUser(row);
+ break;
+ case 'delete':
+ this.initModalDelete(row);
+ break;
+ default:
+ break;
+ }
+ },
+ saveAccountSettings(settings) {
+ this.startLoader();
+ this.$store
+ .dispatch('userManagement/saveAccountSettings', settings)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+.btn.collapsed {
+ svg {
+ transform: rotate(180deg);
+ }
+}
+</style>
diff --git a/src/views/_sila/SecurityAndAccess/UserManagement/index.js b/src/views/_sila/SecurityAndAccess/UserManagement/index.js
new file mode 100644
index 00000000..c3aebec3
--- /dev/null
+++ b/src/views/_sila/SecurityAndAccess/UserManagement/index.js
@@ -0,0 +1,2 @@
+import UserManagement from './UserManagement.vue';
+export default UserManagement;
diff --git a/src/views/_sila/Settings/DateTime/DateTime.vue b/src/views/_sila/Settings/DateTime/DateTime.vue
new file mode 100644
index 00000000..f5e063d1
--- /dev/null
+++ b/src/views/_sila/Settings/DateTime/DateTime.vue
@@ -0,0 +1,417 @@
+<template>
+ <b-container fluid="xl">
+ <page-title />
+ <b-row>
+ <b-col md="8" xl="6">
+ <alert variant="info" class="mb-4">
+ <span>
+ {{ $t('pageDateTime.alert.message') }}
+ <b-link to="/profile-settings">
+ {{ $t('pageDateTime.alert.link') }}</b-link
+ >
+ </span>
+ </alert>
+ </b-col>
+ </b-row>
+ <page-section>
+ <b-row>
+ <b-col lg="3">
+ <dl>
+ <dt>{{ $t('pageDateTime.form.date') }}</dt>
+ <dd v-if="bmcTime">{{ bmcTime | formatDate }}</dd>
+ <dd v-else>--</dd>
+ </dl>
+ </b-col>
+ <b-col lg="3">
+ <dl>
+ <dt>{{ $t('pageDateTime.form.time.label') }}</dt>
+ <dd v-if="bmcTime">{{ bmcTime | formatTime }}</dd>
+ <dd v-else>--</dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </page-section>
+ <page-section :section-title="$t('pageDateTime.configureSettings')">
+ <b-form novalidate @submit.prevent="submitForm">
+ <b-form-group
+ label="Configure date and time"
+ :disabled="loading"
+ label-sr-only
+ >
+ <b-form-radio
+ v-model="form.configurationSelected"
+ value="manual"
+ data-test-id="dateTime-radio-configureManual"
+ >
+ {{ $t('pageDateTime.form.manual') }}
+ </b-form-radio>
+ <b-row class="mt-3 ml-3">
+ <b-col sm="6" lg="4" xl="3">
+ <b-form-group
+ :label="$t('pageDateTime.form.date')"
+ label-for="input-manual-date"
+ >
+ <b-form-text id="date-format-help">YYYY-MM-DD</b-form-text>
+ <b-input-group>
+ <b-form-input
+ id="input-manual-date"
+ v-model="form.manual.date"
+ :state="getValidationState($v.form.manual.date)"
+ :disabled="ntpOptionSelected"
+ data-test-id="dateTime-input-manualDate"
+ class="form-control-with-button"
+ @blur="$v.form.manual.date.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <div v-if="!$v.form.manual.date.pattern">
+ {{ $t('global.form.invalidFormat') }}
+ </div>
+ <div v-if="!$v.form.manual.date.required">
+ {{ $t('global.form.fieldRequired') }}
+ </div>
+ </b-form-invalid-feedback>
+ <b-form-datepicker
+ v-model="form.manual.date"
+ class="btn-datepicker btn-icon-only"
+ button-only
+ right
+ :hide-header="true"
+ :locale="locale"
+ :label-help="
+ $t('global.calendar.useCursorKeysToNavigateCalendarDates')
+ "
+ :title="$t('global.calendar.selectDate')"
+ :disabled="ntpOptionSelected"
+ button-variant="link"
+ aria-controls="input-manual-date"
+ >
+ <template #button-content>
+ <icon-calendar />
+ <span class="sr-only">
+ {{ $t('global.calendar.selectDate') }}
+ </span>
+ </template>
+ </b-form-datepicker>
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" lg="4" xl="3">
+ <b-form-group
+ :label="$t('pageDateTime.form.time.timezone', { timezone })"
+ label-for="input-manual-time"
+ >
+ <b-form-text id="time-format-help">HH:MM</b-form-text>
+ <b-input-group>
+ <b-form-input
+ id="input-manual-time"
+ v-model="form.manual.time"
+ :state="getValidationState($v.form.manual.time)"
+ :disabled="ntpOptionSelected"
+ data-test-id="dateTime-input-manualTime"
+ @blur="$v.form.manual.time.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <div v-if="!$v.form.manual.time.pattern">
+ {{ $t('global.form.invalidFormat') }}
+ </div>
+ <div v-if="!$v.form.manual.time.required">
+ {{ $t('global.form.fieldRequired') }}
+ </div>
+ </b-form-invalid-feedback>
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-form-radio
+ v-model="form.configurationSelected"
+ value="ntp"
+ data-test-id="dateTime-radio-configureNTP"
+ >
+ NTP
+ </b-form-radio>
+ <b-row class="mt-3 ml-3">
+ <b-col sm="6" lg="4" xl="3">
+ <b-form-group
+ :label="$t('pageDateTime.form.ntpServers.server1')"
+ label-for="input-ntp-1"
+ >
+ <b-input-group>
+ <b-form-input
+ id="input-ntp-1"
+ v-model="form.ntp.firstAddress"
+ :state="getValidationState($v.form.ntp.firstAddress)"
+ :disabled="manualOptionSelected"
+ data-test-id="dateTime-input-ntpServer1"
+ @blur="$v.form.ntp.firstAddress.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <div v-if="!$v.form.ntp.firstAddress.required">
+ {{ $t('global.form.fieldRequired') }}
+ </div>
+ </b-form-invalid-feedback>
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" lg="4" xl="3">
+ <b-form-group
+ :label="$t('pageDateTime.form.ntpServers.server2')"
+ label-for="input-ntp-2"
+ >
+ <b-input-group>
+ <b-form-input
+ id="input-ntp-2"
+ v-model="form.ntp.secondAddress"
+ :disabled="manualOptionSelected"
+ data-test-id="dateTime-input-ntpServer2"
+ />
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6" lg="4" xl="3">
+ <b-form-group
+ :label="$t('pageDateTime.form.ntpServers.server3')"
+ label-for="input-ntp-3"
+ >
+ <b-input-group>
+ <b-form-input
+ id="input-ntp-3"
+ v-model="form.ntp.thirdAddress"
+ :disabled="manualOptionSelected"
+ data-test-id="dateTime-input-ntpServer3"
+ />
+ </b-input-group>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-button
+ variant="primary"
+ type="submit"
+ data-test-id="dateTime-button-saveSettings"
+ >
+ {{ $t('global.action.saveSettings') }}
+ </b-button>
+ </b-form-group>
+ </b-form>
+ </page-section>
+ </b-container>
+</template>
+
+<script>
+import Alert from '@/components/Global/Alert';
+import IconCalendar from '@carbon/icons-vue/es/calendar/20';
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import LocalTimezoneLabelMixin from '@/components/Mixins/LocalTimezoneLabelMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+import { mapState } from 'vuex';
+import { requiredIf, helpers } from 'vuelidate/lib/validators';
+
+const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
+const isoTimeRegex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/;
+
+export default {
+ name: 'DateTime',
+ components: { Alert, IconCalendar, PageTitle, PageSection },
+ mixins: [
+ BVToastMixin,
+ LoadingBarMixin,
+ LocalTimezoneLabelMixin,
+ VuelidateMixin,
+ ],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ locale: this.$store.getters['global/languagePreference'],
+ form: {
+ configurationSelected: 'manual',
+ manual: {
+ date: '',
+ time: '',
+ },
+ ntp: { firstAddress: '', secondAddress: '', thirdAddress: '' },
+ },
+ loading,
+ };
+ },
+ validations() {
+ return {
+ form: {
+ manual: {
+ date: {
+ required: requiredIf(function () {
+ return this.form.configurationSelected === 'manual';
+ }),
+ pattern: helpers.regex('pattern', isoDateRegex),
+ },
+ time: {
+ required: requiredIf(function () {
+ return this.form.configurationSelected === 'manual';
+ }),
+ pattern: helpers.regex('pattern', isoTimeRegex),
+ },
+ },
+ ntp: {
+ firstAddress: {
+ required: requiredIf(function () {
+ return this.form.configurationSelected === 'ntp';
+ }),
+ },
+ },
+ },
+ };
+ },
+ computed: {
+ ...mapState('dateTime', ['ntpServers', 'isNtpProtocolEnabled']),
+ bmcTime() {
+ return this.$store.getters['global/bmcTime'];
+ },
+ ntpOptionSelected() {
+ return this.form.configurationSelected === 'ntp';
+ },
+ manualOptionSelected() {
+ return this.form.configurationSelected === 'manual';
+ },
+ isUtcDisplay() {
+ return this.$store.getters['global/isUtcDisplay'];
+ },
+ timezone() {
+ if (this.isUtcDisplay) {
+ return 'UTC';
+ }
+ return this.localOffset();
+ },
+ },
+ watch: {
+ ntpServers() {
+ this.setNtpValues();
+ },
+ manualDate() {
+ this.emitChange();
+ },
+ bmcTime() {
+ this.form.manual.date = this.$options.filters.formatDate(
+ this.$store.getters['global/bmcTime']
+ );
+ this.form.manual.time = this.$options.filters
+ .formatTime(this.$store.getters['global/bmcTime'])
+ .slice(0, 5);
+ },
+ },
+ created() {
+ this.startLoader();
+ this.setNtpValues();
+ Promise.all([
+ this.$store.dispatch('global/getBmcTime'),
+ this.$store.dispatch('dateTime/getNtpData'),
+ ]).finally(() => this.endLoader());
+ },
+ methods: {
+ emitChange() {
+ if (this.$v.$invalid) return;
+ this.$v.$reset(); //reset to re-validate on blur
+ this.$emit('change', {
+ manualDate: this.manualDate ? new Date(this.manualDate) : null,
+ });
+ },
+ setNtpValues() {
+ this.form.configurationSelected = this.isNtpProtocolEnabled
+ ? 'ntp'
+ : 'manual';
+ [
+ this.form.ntp.firstAddress = '',
+ this.form.ntp.secondAddress = '',
+ this.form.ntp.thirdAddress = '',
+ ] = [this.ntpServers[0], this.ntpServers[1], this.ntpServers[2]];
+ },
+ submitForm() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.startLoader();
+
+ let dateTimeForm = {};
+ let isNTPEnabled = this.form.configurationSelected === 'ntp';
+
+ if (!isNTPEnabled) {
+ const isUtcDisplay = this.$store.getters['global/isUtcDisplay'];
+ let date;
+
+ dateTimeForm.ntpProtocolEnabled = false;
+
+ if (isUtcDisplay) {
+ // Create UTC Date
+ date = this.getUtcDate(this.form.manual.date, this.form.manual.time);
+ } else {
+ // Create local Date
+ date = new Date(`${this.form.manual.date} ${this.form.manual.time}`);
+ }
+
+ dateTimeForm.updatedDateTime = date.toISOString();
+ } else {
+ dateTimeForm.ntpProtocolEnabled = true;
+
+ const ntpArray = [
+ this.form.ntp.firstAddress,
+ this.form.ntp.secondAddress,
+ this.form.ntp.thirdAddress,
+ ];
+
+ // Filter the ntpArray to remove empty strings,
+ // per Redfish spec there should be no empty strings or null on the ntp array.
+ const ntpArrayFiltered = ntpArray.filter((x) => x);
+
+ dateTimeForm.ntpServersArray = [...ntpArrayFiltered];
+
+ [this.ntpServers[0], this.ntpServers[1], this.ntpServers[2]] = [
+ ...dateTimeForm.ntpServersArray,
+ ];
+
+ this.setNtpValues();
+ }
+
+ this.$store
+ .dispatch('dateTime/updateDateTime', dateTimeForm)
+ .then((success) => {
+ this.successToast(success);
+ if (!isNTPEnabled) return;
+ // Shift address up if second address is empty
+ // to avoid refreshing after delay when updating NTP
+ if (!this.form.ntp.secondAddress && this.form.ntp.thirdAddres) {
+ this.form.ntp.secondAddress = this.form.ntp.thirdAddres;
+ this.form.ntp.thirdAddress = '';
+ }
+ })
+ .then(() => {
+ this.$store.dispatch('global/getBmcTime');
+ })
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => {
+ this.$v.form.$reset();
+ this.endLoader();
+ });
+ },
+ getUtcDate(date, time) {
+ // Split user input string values to create
+ // a UTC Date object
+ const datesArray = date.split('-');
+ const timeArray = time.split(':');
+ let utcDate = Date.UTC(
+ datesArray[0], // User input year
+ //UTC expects zero-index month value 0-11 (January-December)
+ //for reference https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC#Parameters
+ parseInt(datesArray[1]) - 1, // User input month
+ datesArray[2], // User input day
+ timeArray[0], // User input hour
+ timeArray[1] // User input minute
+ );
+ return new Date(utcDate);
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/DateTime/index.js b/src/views/_sila/Settings/DateTime/index.js
new file mode 100644
index 00000000..2df21eae
--- /dev/null
+++ b/src/views/_sila/Settings/DateTime/index.js
@@ -0,0 +1,2 @@
+import DateTime from './DateTime.vue';
+export default DateTime;
diff --git a/src/views/_sila/Settings/Network/ModalDns.vue b/src/views/_sila/Settings/Network/ModalDns.vue
new file mode 100644
index 00000000..7f127173
--- /dev/null
+++ b/src/views/_sila/Settings/Network/ModalDns.vue
@@ -0,0 +1,92 @@
+<template>
+ <b-modal
+ id="modal-dns"
+ ref="modal"
+ :title="$t('pageNetwork.table.addDnsAddress')"
+ @hidden="resetForm"
+ >
+ <b-form id="form-dns" @submit.prevent="handleSubmit">
+ <b-row>
+ <b-col sm="6">
+ <b-form-group
+ :label="$t('pageNetwork.modal.staticDns')"
+ label-for="staticDns"
+ >
+ <b-form-input
+ id="staticDns"
+ v-model="form.staticDns"
+ type="text"
+ :state="getValidationState($v.form.staticDns)"
+ @input="$v.form.staticDns.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.staticDns.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-if="!$v.form.staticDns.ipAddress">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-form>
+ <template #modal-footer="{ cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button form="form-dns" type="submit" variant="primary" @click="onOk">
+ {{ $t('global.action.add') }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import { ipAddress, required } from 'vuelidate/lib/validators';
+
+export default {
+ mixins: [VuelidateMixin],
+ data() {
+ return {
+ form: {
+ staticDns: null,
+ },
+ };
+ },
+ validations() {
+ return {
+ form: {
+ staticDns: {
+ required,
+ ipAddress,
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', [this.form.staticDns]);
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.staticDns = null;
+ this.$v.$reset();
+ this.$emit('hidden');
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/ModalHostname.vue b/src/views/_sila/Settings/Network/ModalHostname.vue
new file mode 100644
index 00000000..f3221ec7
--- /dev/null
+++ b/src/views/_sila/Settings/Network/ModalHostname.vue
@@ -0,0 +1,110 @@
+<template>
+ <b-modal
+ id="modal-hostname"
+ ref="modal"
+ :title="$t('pageNetwork.modal.editHostnameTitle')"
+ @hidden="resetForm"
+ >
+ <b-form id="hostname-settings" @submit.prevent="handleSubmit">
+ <b-row>
+ <b-col sm="6">
+ <b-form-group
+ :label="$t('pageNetwork.hostname')"
+ label-for="hostname"
+ >
+ <b-form-input
+ id="hostname"
+ v-model="form.hostname"
+ type="text"
+ :state="getValidationState($v.form.hostname)"
+ @input="$v.form.hostname.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.hostname.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-if="!$v.form.hostname.validateHostname">
+ {{ $t('global.form.lengthMustBeBetween', { min: 1, max: 64 }) }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-form>
+ <template #modal-footer="{ cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button
+ form="hostname-settings"
+ type="submit"
+ variant="primary"
+ @click="onOk"
+ >
+ {{ $t('global.action.add') }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import { required, helpers } from 'vuelidate/lib/validators';
+
+const validateHostname = helpers.regex('validateHostname', /^\S{0,64}$/);
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ hostname: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ form: {
+ hostname: '',
+ },
+ };
+ },
+ watch: {
+ hostname() {
+ this.form.hostname = this.hostname;
+ },
+ },
+ validations() {
+ return {
+ form: {
+ hostname: {
+ required,
+ validateHostname,
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', { HostName: this.form.hostname });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.hostname = this.hostname;
+ this.$v.$reset();
+ this.$emit('hidden');
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/ModalIpv4.vue b/src/views/_sila/Settings/Network/ModalIpv4.vue
new file mode 100644
index 00000000..dcf4a579
--- /dev/null
+++ b/src/views/_sila/Settings/Network/ModalIpv4.vue
@@ -0,0 +1,165 @@
+<template>
+ <b-modal
+ id="modal-add-ipv4"
+ ref="modal"
+ :title="$t('pageNetwork.table.addIpv4Address')"
+ @hidden="resetForm"
+ >
+ <b-form id="form-ipv4" @submit.prevent="handleSubmit">
+ <b-row>
+ <b-col sm="6">
+ <b-form-group
+ :label="$t('pageNetwork.modal.ipAddress')"
+ label-for="ipAddress"
+ >
+ <b-form-input
+ id="ipAddress"
+ v-model="form.ipAddress"
+ type="text"
+ :state="getValidationState($v.form.ipAddress)"
+ @input="$v.form.ipAddress.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.ipAddress.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-if="!$v.form.ipAddress.ipAddress">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col sm="6">
+ <b-form-group
+ :label="$t('pageNetwork.modal.gateway')"
+ label-for="gateway"
+ >
+ <b-form-input
+ id="gateway"
+ v-model="form.gateway"
+ type="text"
+ :state="getValidationState($v.form.gateway)"
+ @input="$v.form.gateway.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.gateway.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-if="!$v.form.gateway.ipAddress">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ <b-row>
+ <b-col sm="6">
+ <b-form-group
+ :label="$t('pageNetwork.modal.subnetMask')"
+ label-for="subnetMask"
+ >
+ <b-form-input
+ id="subnetMask"
+ v-model="form.subnetMask"
+ type="text"
+ :state="getValidationState($v.form.subnetMask)"
+ @input="$v.form.subnetMask.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.subnetMask.required">
+ {{ $t('global.form.fieldRequired') }}
+ </template>
+ <template v-if="!$v.form.subnetMask.ipAddress">
+ {{ $t('global.form.invalidFormat') }}
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-form>
+ <template #modal-footer="{ cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button form="form-ipv4" type="submit" variant="primary" @click="onOk">
+ {{ $t('global.action.add') }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import { ipAddress, required } from 'vuelidate/lib/validators';
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ defaultGateway: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ form: {
+ ipAddress: '',
+ gateway: '',
+ subnetMask: '',
+ },
+ };
+ },
+ watch: {
+ defaultGateway() {
+ this.form.gateway = this.defaultGateway;
+ },
+ },
+ validations() {
+ return {
+ form: {
+ ipAddress: {
+ required,
+ ipAddress,
+ },
+ gateway: {
+ required,
+ ipAddress,
+ },
+ subnetMask: {
+ required,
+ ipAddress,
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', {
+ Address: this.form.ipAddress,
+ Gateway: this.form.gateway,
+ SubnetMask: this.form.subnetMask,
+ });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.ipAddress = null;
+ this.form.gateway = this.defaultGateway;
+ this.form.subnetMask = null;
+ this.$v.$reset();
+ this.$emit('hidden');
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/ModalMacAddress.vue b/src/views/_sila/Settings/Network/ModalMacAddress.vue
new file mode 100644
index 00000000..d563f4ce
--- /dev/null
+++ b/src/views/_sila/Settings/Network/ModalMacAddress.vue
@@ -0,0 +1,109 @@
+<template>
+ <b-modal
+ id="modal-mac-address"
+ ref="modal"
+ :title="$t('pageNetwork.modal.editMacAddressTitle')"
+ @hidden="resetForm"
+ >
+ <b-form id="mac-settings" @submit.prevent="handleSubmit">
+ <b-row>
+ <b-col sm="6">
+ <b-form-group
+ :label="$t('pageNetwork.macAddress')"
+ label-for="macAddress"
+ >
+ <b-form-input
+ id="mac-address"
+ v-model.trim="form.macAddress"
+ data-test-id="network-input-macAddress"
+ type="text"
+ :state="getValidationState($v.form.macAddress)"
+ @change="$v.form.macAddress.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <div v-if="!$v.form.macAddress.required">
+ {{ $t('global.form.fieldRequired') }}
+ </div>
+ <div v-if="!$v.form.macAddress.macAddress">
+ {{ $t('global.form.invalidFormat') }}
+ </div>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-form>
+ <template #modal-footer="{ cancel }">
+ <b-button variant="secondary" @click="cancel()">
+ {{ $t('global.action.cancel') }}
+ </b-button>
+ <b-button
+ form="mac-settings"
+ type="submit"
+ variant="primary"
+ @click="onOk"
+ >
+ {{ $t('global.action.add') }}
+ </b-button>
+ </template>
+ </b-modal>
+</template>
+
+<script>
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import { macAddress, required } from 'vuelidate/lib/validators';
+
+export default {
+ mixins: [VuelidateMixin],
+ props: {
+ macAddress: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ form: {
+ macAddress: '',
+ },
+ };
+ },
+ watch: {
+ macAddress() {
+ this.form.macAddress = this.macAddress;
+ },
+ },
+ validations() {
+ return {
+ form: {
+ macAddress: {
+ required,
+ macAddress: macAddress(),
+ },
+ },
+ };
+ },
+ methods: {
+ handleSubmit() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ this.$emit('ok', { MACAddress: this.form.macAddress });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.macAddress = this.macAddress;
+ this.$v.$reset();
+ this.$emit('hidden');
+ },
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/Network.vue b/src/views/_sila/Settings/Network/Network.vue
new file mode 100644
index 00000000..2abbcd7a
--- /dev/null
+++ b/src/views/_sila/Settings/Network/Network.vue
@@ -0,0 +1,167 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pageNetwork.pageDescription')" />
+ <!-- Global settings for all interfaces -->
+ <network-global-settings />
+ <!-- Interface tabs -->
+ <page-section v-show="ethernetData">
+ <b-row>
+ <b-col>
+ <b-card no-body>
+ <b-tabs
+ active-nav-item-class="font-weight-bold"
+ card
+ content-class="mt-3"
+ >
+ <b-tab
+ v-for="(data, index) in ethernetData"
+ :key="data.Id"
+ :title="data.Id"
+ @click="getTabIndex(index)"
+ >
+ <!-- Interface settings -->
+ <network-interface-settings :tab-index="tabIndex" />
+ <!-- IPV4 table -->
+ <table-ipv-4 :tab-index="tabIndex" />
+ <!-- Static DNS table -->
+ <table-dns :tab-index="tabIndex" />
+ </b-tab>
+ </b-tabs>
+ </b-card>
+ </b-col>
+ </b-row>
+ </page-section>
+ <!-- Modals -->
+ <modal-ipv4 :default-gateway="defaultGateway" @ok="saveIpv4Address" />
+ <modal-dns @ok="saveDnsAddress" />
+ <modal-hostname :hostname="currentHostname" @ok="saveSettings" />
+ <modal-mac-address :mac-address="currentMacAddress" @ok="saveSettings" />
+ </b-container>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import ModalMacAddress from './ModalMacAddress.vue';
+import ModalHostname from './ModalHostname.vue';
+import ModalIpv4 from './ModalIpv4.vue';
+import ModalDns from './ModalDns.vue';
+import NetworkGlobalSettings from './NetworkGlobalSettings.vue';
+import NetworkInterfaceSettings from './NetworkInterfaceSettings.vue';
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+import TableIpv4 from './TableIpv4.vue';
+import TableDns from './TableDns.vue';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'Network',
+ components: {
+ ModalHostname,
+ ModalMacAddress,
+ ModalIpv4,
+ ModalDns,
+ NetworkGlobalSettings,
+ NetworkInterfaceSettings,
+ PageSection,
+ PageTitle,
+ TableDns,
+ TableIpv4,
+ },
+ mixins: [BVToastMixin, DataFormatterMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ currentHostname: '',
+ currentMacAddress: '',
+ defaultGateway: '',
+ loading,
+ tabIndex: 0,
+ };
+ },
+ computed: {
+ ...mapState('network', ['ethernetData']),
+ },
+ watch: {
+ ethernetData() {
+ this.getModalInfo();
+ },
+ },
+ created() {
+ this.startLoader();
+ const globalSettings = new Promise((resolve) => {
+ this.$root.$on('network-global-settings-complete', () => resolve());
+ });
+ const interfaceSettings = new Promise((resolve) => {
+ this.$root.$on('network-interface-settings-complete', () => resolve());
+ });
+ const networkTableDns = new Promise((resolve) => {
+ this.$root.$on('network-table-dns-complete', () => resolve());
+ });
+ const networkTableIpv4 = new Promise((resolve) => {
+ this.$root.$on('network-table-ipv4-complete', () => resolve());
+ });
+ // Combine all child component Promises to indicate
+ // when page data load complete
+ Promise.all([
+ this.$store.dispatch('network/getEthernetData'),
+ globalSettings,
+ interfaceSettings,
+ networkTableDns,
+ networkTableIpv4,
+ ]).finally(() => this.endLoader());
+ },
+ methods: {
+ getModalInfo() {
+ this.defaultGateway = this.$store.getters[
+ 'network/globalNetworkSettings'
+ ][this.tabIndex].defaultGateway;
+
+ this.currentHostname = this.$store.getters[
+ 'network/globalNetworkSettings'
+ ][this.tabIndex].hostname;
+
+ this.currentMacAddress = this.$store.getters[
+ 'network/globalNetworkSettings'
+ ][this.tabIndex].macAddress;
+ },
+ getTabIndex(selectedIndex) {
+ this.tabIndex = selectedIndex;
+ this.$store.dispatch('network/setSelectedTabIndex', this.tabIndex);
+ this.$store.dispatch(
+ 'network/setSelectedTabId',
+ this.ethernetData[selectedIndex].Id
+ );
+ this.getModalInfo();
+ },
+ saveIpv4Address(modalFormData) {
+ this.startLoader();
+ this.$store
+ .dispatch('network/saveIpv4Address', modalFormData)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ saveDnsAddress(modalFormData) {
+ this.startLoader();
+ this.$store
+ .dispatch('network/saveDnsAddress', modalFormData)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ saveSettings(modalFormData) {
+ this.startLoader();
+ this.$store
+ .dispatch('network/saveSettings', modalFormData)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/NetworkGlobalSettings.vue b/src/views/_sila/Settings/Network/NetworkGlobalSettings.vue
new file mode 100644
index 00000000..30287673
--- /dev/null
+++ b/src/views/_sila/Settings/Network/NetworkGlobalSettings.vue
@@ -0,0 +1,161 @@
+<template>
+ <page-section
+ v-if="firstInterface"
+ :section-title="$t('pageNetwork.networkSettings')"
+ >
+ <b-row>
+ <b-col md="3">
+ <dl>
+ <dt>
+ {{ $t('pageNetwork.hostname') }}
+ <b-button variant="link" class="p-1" @click="initSettingsModal()">
+ <icon-edit :title="$t('pageNetwork.modal.editHostnameTitle')" />
+ </b-button>
+ </dt>
+ <dd>{{ dataFormatter(firstInterface.hostname) }}</dd>
+ </dl>
+ </b-col>
+ <b-col md="3">
+ <dl>
+ <dt>{{ $t('pageNetwork.useDomainName') }}</dt>
+ <dd>
+ <b-form-checkbox
+ id="useDomainNameSwitch"
+ v-model="useDomainNameState"
+ data-test-id="networkSettings-switch-useDomainName"
+ switch
+ @change="changeDomainNameState"
+ >
+ <span v-if="useDomainNameState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </dd>
+ </dl>
+ </b-col>
+ <b-col md="3">
+ <dl>
+ <dt>{{ $t('pageNetwork.useDns') }}</dt>
+ <dd>
+ <b-form-checkbox
+ id="useDnsSwitch"
+ v-model="useDnsState"
+ data-test-id="networkSettings-switch-useDns"
+ switch
+ @change="changeDnsState"
+ >
+ <span v-if="useDnsState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </dd>
+ </dl>
+ </b-col>
+ <b-col md="3">
+ <dl>
+ <dt>{{ $t('pageNetwork.useNtp') }}</dt>
+ <dd>
+ <b-form-checkbox
+ id="useNtpSwitch"
+ v-model="useNtpState"
+ data-test-id="networkSettings-switch-useNtp"
+ switch
+ @change="changeNtpState"
+ >
+ <span v-if="useNtpState">
+ {{ $t('global.status.enabled') }}
+ </span>
+ <span v-else>{{ $t('global.status.disabled') }}</span>
+ </b-form-checkbox>
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </page-section>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconEdit from '@carbon/icons-vue/es/edit/16';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import PageSection from '@/components/Global/PageSection';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'GlobalNetworkSettings',
+ components: { IconEdit, PageSection },
+ mixins: [BVToastMixin, DataFormatterMixin],
+
+ data() {
+ return {
+ hostname: '',
+ };
+ },
+ computed: {
+ ...mapState('network', ['ethernetData']),
+ firstInterface() {
+ return this.$store.getters['network/globalNetworkSettings'][0];
+ },
+ useDomainNameState: {
+ get() {
+ return this.$store.getters['network/globalNetworkSettings'][0]
+ .useDomainNameEnabled;
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ useDnsState: {
+ get() {
+ return this.$store.getters['network/globalNetworkSettings'][0]
+ .useDnsEnabled;
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ useNtpState: {
+ get() {
+ return this.$store.getters['network/globalNetworkSettings'][0]
+ .useNtpEnabled;
+ },
+ set(newValue) {
+ return newValue;
+ },
+ },
+ },
+ created() {
+ this.$store.dispatch('network/getEthernetData').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('network-global-settings-complete');
+ });
+ },
+ methods: {
+ changeDomainNameState(state) {
+ this.$store
+ .dispatch('network/saveDomainNameState', state)
+ .then((success) => {
+ this.successToast(success);
+ })
+ .catch(({ message }) => this.errorToast(message));
+ },
+ changeDnsState(state) {
+ this.$store
+ .dispatch('network/saveDnsState', state)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ changeNtpState(state) {
+ this.$store
+ .dispatch('network/saveNtpState', state)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ initSettingsModal() {
+ this.$bvModal.show('modal-hostname');
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/NetworkInterfaceSettings.vue b/src/views/_sila/Settings/Network/NetworkInterfaceSettings.vue
new file mode 100644
index 00000000..023d29bc
--- /dev/null
+++ b/src/views/_sila/Settings/Network/NetworkInterfaceSettings.vue
@@ -0,0 +1,117 @@
+<template>
+ <div>
+ <page-section>
+ <b-row>
+ <b-col md="3">
+ <dl>
+ <dt>{{ $t('pageNetwork.linkStatus') }}</dt>
+ <dd>
+ {{ dataFormatter(linkStatus) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col md="3">
+ <dl>
+ <dt>{{ $t('pageNetwork.speed') }}</dt>
+ <dd>
+ {{ dataFormatter(linkSpeed) }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </page-section>
+ <page-section :section-title="$t('pageNetwork.interfaceSection')">
+ <b-row>
+ <b-col md="3">
+ <dl>
+ <dt>
+ {{ $t('pageNetwork.fqdn') }}
+ </dt>
+ <dd>
+ {{ dataFormatter(fqdn) }}
+ </dd>
+ </dl>
+ </b-col>
+ <b-col md="3">
+ <dl class="text-nowrap">
+ <dt>
+ {{ $t('pageNetwork.macAddress') }}
+ <b-button
+ variant="link"
+ class="p-1"
+ @click="initMacAddressModal()"
+ >
+ <icon-edit
+ :title="$t('pageNetwork.modal.editMacAddressTitle')"
+ />
+ </b-button>
+ </dt>
+ <dd>
+ {{ dataFormatter(macAddress) }}
+ </dd>
+ </dl>
+ </b-col>
+ </b-row>
+ </page-section>
+ </div>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconEdit from '@carbon/icons-vue/es/edit/16';
+import PageSection from '@/components/Global/PageSection';
+import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'Ipv4Table',
+ components: {
+ IconEdit,
+ PageSection,
+ },
+ mixins: [BVToastMixin, DataFormatterMixin],
+ props: {
+ tabIndex: {
+ type: Number,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ selectedInterface: '',
+ linkStatus: '',
+ linkSpeed: '',
+ fqdn: '',
+ macAddress: '',
+ };
+ },
+ computed: {
+ ...mapState('network', ['ethernetData']),
+ },
+ watch: {
+ // Watch for change in tab index
+ tabIndex() {
+ this.getSettings();
+ },
+ },
+ created() {
+ this.getSettings();
+ this.$store.dispatch('network/getEthernetData').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('network-interface-settings-complete');
+ });
+ },
+ methods: {
+ getSettings() {
+ this.selectedInterface = this.tabIndex;
+ this.linkStatus = this.ethernetData[this.selectedInterface].LinkStatus;
+ this.linkSpeed = this.ethernetData[this.selectedInterface].SpeedMbps;
+ this.fqdn = this.ethernetData[this.selectedInterface].FQDN;
+ this.macAddress = this.ethernetData[this.selectedInterface].MACAddress;
+ },
+ initMacAddressModal() {
+ this.$bvModal.show('modal-mac-address');
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/TableDns.vue b/src/views/_sila/Settings/Network/TableDns.vue
new file mode 100644
index 00000000..569109f1
--- /dev/null
+++ b/src/views/_sila/Settings/Network/TableDns.vue
@@ -0,0 +1,145 @@
+<template>
+ <page-section :section-title="$t('pageNetwork.staticDns')">
+ <b-row>
+ <b-col lg="6">
+ <div class="text-right">
+ <b-button variant="primary" @click="initDnsModal()">
+ <icon-add />
+ {{ $t('pageNetwork.table.addDnsAddress') }}
+ </b-button>
+ </div>
+ <b-table
+ responsive="md"
+ hover
+ :fields="dnsTableFields"
+ :items="form.dnsStaticTableItems"
+ :empty-text="$t('global.table.emptyMessage')"
+ class="mb-0"
+ show-empty
+ >
+ <template #cell(actions)="{ item, index }">
+ <table-row-action
+ v-for="(action, actionIndex) in item.actions"
+ :key="actionIndex"
+ :value="action.value"
+ :title="action.title"
+ :enabled="action.enabled"
+ @click-table-action="onDnsTableAction(action, $event, index)"
+ >
+ <template #icon>
+ <icon-edit v-if="action.value === 'edit'" />
+ <icon-trashcan v-if="action.value === 'delete'" />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </b-col>
+ </b-row>
+ </page-section>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconEdit from '@carbon/icons-vue/es/edit/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import PageSection from '@/components/Global/PageSection';
+import TableRowAction from '@/components/Global/TableRowAction';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'DNSTable',
+ components: {
+ IconAdd,
+ IconEdit,
+ IconTrashcan,
+ PageSection,
+ TableRowAction,
+ },
+ mixins: [BVToastMixin],
+ props: {
+ tabIndex: {
+ type: Number,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ form: {
+ dnsStaticTableItems: [],
+ },
+ actions: [
+ {
+ value: 'edit',
+ title: this.$t('global.action.edit'),
+ },
+ {
+ value: 'delete',
+ title: this.$t('global.action.delete'),
+ },
+ ],
+ dnsTableFields: [
+ {
+ key: 'address',
+ label: this.$t('pageNetwork.table.ipAddress'),
+ },
+ { key: 'actions', label: '', tdClass: 'text-right' },
+ ],
+ };
+ },
+ computed: {
+ ...mapState('network', ['ethernetData']),
+ },
+ watch: {
+ // Watch for change in tab index
+ tabIndex() {
+ this.getStaticDnsItems();
+ },
+ ethernetData() {
+ this.getStaticDnsItems();
+ },
+ },
+ created() {
+ this.getStaticDnsItems();
+ this.$store.dispatch('network/getEthernetData').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('network-table-dns-complete');
+ });
+ },
+ methods: {
+ getStaticDnsItems() {
+ const index = this.tabIndex;
+ const dns = this.ethernetData[index].StaticNameServers || [];
+ this.form.dnsStaticTableItems = dns.map((server) => {
+ return {
+ address: server,
+ actions: [
+ {
+ value: 'delete',
+ title: this.$t('pageNetwork.table.deleteDns'),
+ },
+ ],
+ };
+ });
+ },
+ onDnsTableAction(action, $event, index) {
+ if ($event === 'delete') {
+ this.deleteDnsTableRow(index);
+ }
+ },
+ deleteDnsTableRow(index) {
+ this.form.dnsStaticTableItems.splice(index, 1);
+ const newDnsArray = this.form.dnsStaticTableItems.map((dns) => {
+ return dns.address;
+ });
+ this.$store
+ .dispatch('network/editDnsAddress', newDnsArray)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ initDnsModal() {
+ this.$bvModal.show('modal-dns');
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/TableIpv4.vue b/src/views/_sila/Settings/Network/TableIpv4.vue
new file mode 100644
index 00000000..75870031
--- /dev/null
+++ b/src/views/_sila/Settings/Network/TableIpv4.vue
@@ -0,0 +1,169 @@
+<template>
+ <page-section :section-title="$t('pageNetwork.ipv4')">
+ <b-row>
+ <b-col>
+ <h3 class="h5">
+ {{ $t('pageNetwork.ipv4Addresses') }}
+ </h3>
+ </b-col>
+ <b-col class="text-right">
+ <b-button variant="primary" @click="initAddIpv4Address()">
+ <icon-add />
+ {{ $t('pageNetwork.table.addIpv4Address') }}
+ </b-button>
+ </b-col>
+ </b-row>
+ <b-table
+ responsive="md"
+ hover
+ :fields="ipv4TableFields"
+ :items="form.ipv4TableItems"
+ :empty-text="$t('global.table.emptyMessage')"
+ class="mb-0"
+ show-empty
+ >
+ <template #cell(actions)="{ item, index }">
+ <table-row-action
+ v-for="(action, actionIndex) in item.actions"
+ :key="actionIndex"
+ :value="action.value"
+ :title="action.title"
+ :enabled="action.enabled"
+ @click-table-action="onIpv4TableAction(action, $event, index)"
+ >
+ <template #icon>
+ <icon-edit v-if="action.value === 'edit'" />
+ <icon-trashcan v-if="action.value === 'delete'" />
+ </template>
+ </table-row-action>
+ </template>
+ </b-table>
+ </page-section>
+</template>
+
+<script>
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconEdit from '@carbon/icons-vue/es/edit/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import PageSection from '@/components/Global/PageSection';
+import TableRowAction from '@/components/Global/TableRowAction';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'Ipv4Table',
+ components: {
+ IconAdd,
+ IconEdit,
+ IconTrashcan,
+ PageSection,
+ TableRowAction,
+ },
+ mixins: [BVToastMixin, LoadingBarMixin],
+ props: {
+ tabIndex: {
+ type: Number,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ form: {
+ ipv4TableItems: [],
+ },
+ actions: [
+ {
+ value: 'edit',
+ title: this.$t('global.action.edit'),
+ },
+ {
+ value: 'delete',
+ title: this.$t('global.action.delete'),
+ },
+ ],
+ ipv4TableFields: [
+ {
+ key: 'Address',
+ label: this.$t('pageNetwork.table.ipAddress'),
+ },
+ {
+ key: 'Gateway',
+ label: this.$t('pageNetwork.table.gateway'),
+ },
+ {
+ key: 'SubnetMask',
+ label: this.$t('pageNetwork.table.subnet'),
+ },
+ {
+ key: 'AddressOrigin',
+ label: this.$t('pageNetwork.table.addressOrigin'),
+ },
+ { key: 'actions', label: '', tdClass: 'text-right' },
+ ],
+ };
+ },
+ computed: {
+ ...mapState('network', ['ethernetData']),
+ },
+ watch: {
+ // Watch for change in tab index
+ tabIndex() {
+ this.getIpv4TableItems();
+ },
+ ethernetData() {
+ this.getIpv4TableItems();
+ },
+ },
+ created() {
+ this.getIpv4TableItems();
+ this.$store.dispatch('network/getEthernetData').finally(() => {
+ // Emit initial data fetch complete to parent component
+ this.$root.$emit('network-table-ipv4-complete');
+ });
+ },
+ methods: {
+ getIpv4TableItems() {
+ const index = this.tabIndex;
+ const addresses = this.ethernetData[index].IPv4Addresses || [];
+ this.form.ipv4TableItems = addresses.map((ipv4) => {
+ return {
+ Address: ipv4.Address,
+ SubnetMask: ipv4.SubnetMask,
+ Gateway: ipv4.Gateway,
+ AddressOrigin: ipv4.AddressOrigin,
+ actions: [
+ {
+ value: 'delete',
+ title: this.$t('pageNetwork.table.deleteIpv4'),
+ },
+ ],
+ };
+ });
+ },
+ onIpv4TableAction(action, $event, index) {
+ if ($event === 'delete') {
+ this.deleteIpv4TableRow(index);
+ }
+ },
+ deleteIpv4TableRow(index) {
+ this.form.ipv4TableItems.splice(index, 1);
+ const newIpv4Array = this.form.ipv4TableItems.map((ipv4) => {
+ const { Address, SubnetMask, Gateway } = ipv4;
+ return {
+ Address,
+ SubnetMask,
+ Gateway,
+ };
+ });
+ this.$store
+ .dispatch('network/editIpv4Address', newIpv4Array)
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message));
+ },
+ initAddIpv4Address() {
+ this.$bvModal.show('modal-add-ipv4');
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/Network/index.js b/src/views/_sila/Settings/Network/index.js
new file mode 100644
index 00000000..97bf0397
--- /dev/null
+++ b/src/views/_sila/Settings/Network/index.js
@@ -0,0 +1,2 @@
+import Network from './Network.vue';
+export default Network;
diff --git a/src/views/_sila/Settings/PowerRestorePolicy/PowerRestorePolicy.vue b/src/views/_sila/Settings/PowerRestorePolicy/PowerRestorePolicy.vue
new file mode 100644
index 00000000..06e30f3e
--- /dev/null
+++ b/src/views/_sila/Settings/PowerRestorePolicy/PowerRestorePolicy.vue
@@ -0,0 +1,91 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pagePowerRestorePolicy.description')" />
+
+ <b-row>
+ <b-col sm="8" md="6" xl="12">
+ <b-form-group :label="$t('pagePowerRestorePolicy.powerPoliciesLabel')">
+ <b-form-radio-group
+ v-model="currentPowerRestorePolicy"
+ :options="options"
+ name="power-restore-policy"
+ ></b-form-radio-group>
+ </b-form-group>
+ </b-col>
+ </b-row>
+
+ <b-button variant="primary" type="submit" @click="submitForm">
+ {{ $t('global.action.saveSettings') }}
+ </b-button>
+ </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+export default {
+ name: 'PowerRestorePolicy',
+ components: { PageTitle },
+ mixins: [VuelidateMixin, BVToastMixin, LoadingBarMixin],
+ beforeRouteLeave(to, from, next) {
+ this.hideLoader();
+ next();
+ },
+ data() {
+ return {
+ policyValue: null,
+ options: [],
+ };
+ },
+ computed: {
+ powerRestorePolicies() {
+ return this.$store.getters['powerPolicy/powerRestorePolicies'];
+ },
+ currentPowerRestorePolicy: {
+ get() {
+ return this.$store.getters['powerPolicy/powerRestoreCurrentPolicy'];
+ },
+ set(policy) {
+ this.policyValue = policy;
+ },
+ },
+ },
+ created() {
+ this.startLoader();
+ this.renderPowerRestoreSettings();
+ },
+ methods: {
+ renderPowerRestoreSettings() {
+ Promise.all([
+ this.$store.dispatch('powerPolicy/getPowerRestorePolicies'),
+ this.$store.dispatch('powerPolicy/getPowerRestoreCurrentPolicy'),
+ ]).finally(() => {
+ this.options.length = 0;
+ this.powerRestorePolicies.map((item) => {
+ this.options.push({
+ text: this.$t(`pagePowerRestorePolicy.policiesDesc.${item.state}`),
+ value: `${item.state}`,
+ });
+ });
+ this.endLoader();
+ });
+ },
+ submitForm() {
+ this.startLoader();
+ this.$store
+ .dispatch(
+ 'powerPolicy/setPowerRestorePolicy',
+ this.policyValue || this.currentPowerRestorePolicy
+ )
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => {
+ this.renderPowerRestoreSettings();
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/_sila/Settings/PowerRestorePolicy/index.js b/src/views/_sila/Settings/PowerRestorePolicy/index.js
new file mode 100644
index 00000000..fab0d477
--- /dev/null
+++ b/src/views/_sila/Settings/PowerRestorePolicy/index.js
@@ -0,0 +1,2 @@
+import PowerRestorePolicy from './PowerRestorePolicy.vue';
+export default PowerRestorePolicy;