summaryrefslogtreecommitdiff
path: root/src/views/Operations
diff options
context:
space:
mode:
authorSandeepa Singh <sandeepa.singh@ibm.com>2021-07-14 13:32:22 +0300
committerDerick Montague <derick.montague@ibm.com>2021-08-10 22:20:42 +0300
commit68cbbe9014cbdcf7229a878f564d38f6d6199f25 (patch)
treecd7138959f405cb44b5d62000da9d364ed238b91 /src/views/Operations
parent7affc529b7fba41193c4d48764707e9961cdd22d (diff)
downloadwebui-vue-68cbbe9014cbdcf7229a878f564d38f6d6199f25.tar.xz
IA update: Update control section to operations
This is the third update to the information architecture changes and has the following changes: - The control section has been updated to operations - The server led page has been removed - The firmware page is moved to operations section Signed-off-by: Sandeepa Singh <sandeepa.singh@ibm.com> Change-Id: I2e23da447890d7bee51892e1f782d5f2db6dded4
Diffstat (limited to 'src/views/Operations')
-rw-r--r--src/views/Operations/FactoryReset/FactoryReset.vue117
-rw-r--r--src/views/Operations/FactoryReset/FactoryResetModal.vue113
-rw-r--r--src/views/Operations/FactoryReset/index.js2
-rw-r--r--src/views/Operations/Firmware/Firmware.vue93
-rw-r--r--src/views/Operations/Firmware/FirmwareAlertServerPower.vue50
-rw-r--r--src/views/Operations/Firmware/FirmwareCardsBmc.vue136
-rw-r--r--src/views/Operations/Firmware/FirmwareCardsHost.vue73
-rw-r--r--src/views/Operations/Firmware/FirmwareFormUpdate.vue200
-rw-r--r--src/views/Operations/Firmware/FirmwareModalSwitchToRunning.vue31
-rw-r--r--src/views/Operations/Firmware/FirmwareModalUpdateFirmware.vue44
-rw-r--r--src/views/Operations/Firmware/index.js2
-rw-r--r--src/views/Operations/Kvm/Kvm.vue24
-rw-r--r--src/views/Operations/Kvm/KvmConsole.vue170
-rw-r--r--src/views/Operations/Kvm/index.js2
-rw-r--r--src/views/Operations/ManagePowerUsage/ManagePowerUsage.vue165
-rw-r--r--src/views/Operations/ManagePowerUsage/index.js2
-rw-r--r--src/views/Operations/PowerRestorePolicy/PowerRestorePolicy.vue80
-rw-r--r--src/views/Operations/PowerRestorePolicy/index.js2
-rw-r--r--src/views/Operations/RebootBmc/RebootBmc.vue83
-rw-r--r--src/views/Operations/RebootBmc/index.js2
-rw-r--r--src/views/Operations/SerialOverLan/SerialOverLan.vue24
-rw-r--r--src/views/Operations/SerialOverLan/SerialOverLanConsole.vue148
-rw-r--r--src/views/Operations/SerialOverLan/index.js2
-rw-r--r--src/views/Operations/ServerPowerOperations/BootSettings.vue140
-rw-r--r--src/views/Operations/ServerPowerOperations/ServerPowerOperations.vue260
-rw-r--r--src/views/Operations/ServerPowerOperations/index.js2
-rw-r--r--src/views/Operations/VirtualMedia/ModalConfigureConnection.vue145
-rw-r--r--src/views/Operations/VirtualMedia/VirtualMedia.vue221
-rw-r--r--src/views/Operations/VirtualMedia/index.js2
29 files changed, 2335 insertions, 0 deletions
diff --git a/src/views/Operations/FactoryReset/FactoryReset.vue b/src/views/Operations/FactoryReset/FactoryReset.vue
new file mode 100644
index 00000000..897348fc
--- /dev/null
+++ b/src/views/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/Operations/FactoryReset/FactoryResetModal.vue b/src/views/Operations/FactoryReset/FactoryResetModal.vue
new file mode 100644
index 00000000..170bf284
--- /dev/null
+++ b/src/views/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/Operations/FactoryReset/index.js b/src/views/Operations/FactoryReset/index.js
new file mode 100644
index 00000000..eae747e0
--- /dev/null
+++ b/src/views/Operations/FactoryReset/index.js
@@ -0,0 +1,2 @@
+import FactoryReset from './FactoryReset.vue';
+export default FactoryReset;
diff --git a/src/views/Operations/Firmware/Firmware.vue b/src/views/Operations/Firmware/Firmware.vue
new file mode 100644
index 00000000..a2acb9b0
--- /dev/null
+++ b/src/views/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/Operations/Firmware/FirmwareAlertServerPower.vue b/src/views/Operations/Firmware/FirmwareAlertServerPower.vue
new file mode 100644
index 00000000..2a3bcba1
--- /dev/null
+++ b/src/views/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="/control/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/Operations/Firmware/FirmwareCardsBmc.vue b/src/views/Operations/Firmware/FirmwareCardsBmc.vue
new file mode 100644
index 00000000..d79a8769
--- /dev/null
+++ b/src/views/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/Operations/Firmware/FirmwareCardsHost.vue b/src/views/Operations/Firmware/FirmwareCardsHost.vue
new file mode 100644
index 00000000..b4a8e90d
--- /dev/null
+++ b/src/views/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/Operations/Firmware/FirmwareFormUpdate.vue b/src/views/Operations/Firmware/FirmwareFormUpdate.vue
new file mode 100644
index 00000000..04b28a5c
--- /dev/null
+++ b/src/views/Operations/Firmware/FirmwareFormUpdate.vue
@@ -0,0 +1,200 @@
+<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>
+ <alert
+ v-if="isServerPowerOffRequired && !isServerOff"
+ variant="warning"
+ :small="true"
+ class="mt-4"
+ >
+ <p class="col-form-label">
+ {{
+ $t('pageFirmware.alert.serverMustBePoweredOffToUpdateFirmware')
+ }}
+ </p>
+ </alert>
+ </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 Alert from '@/components/Global/Alert';
+import FormFile from '@/components/Global/FormFile';
+import ModalUpdateFirmware from './FirmwareModalUpdateFirmware';
+
+export default {
+ components: { Alert, 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/Operations/Firmware/FirmwareModalSwitchToRunning.vue b/src/views/Operations/Firmware/FirmwareModalSwitchToRunning.vue
new file mode 100644
index 00000000..dc4a4973
--- /dev/null
+++ b/src/views/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/Operations/Firmware/FirmwareModalUpdateFirmware.vue b/src/views/Operations/Firmware/FirmwareModalUpdateFirmware.vue
new file mode 100644
index 00000000..18355217
--- /dev/null
+++ b/src/views/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/Operations/Firmware/index.js b/src/views/Operations/Firmware/index.js
new file mode 100644
index 00000000..ad15cc03
--- /dev/null
+++ b/src/views/Operations/Firmware/index.js
@@ -0,0 +1,2 @@
+import Firmware from './Firmware.vue';
+export default Firmware;
diff --git a/src/views/Operations/Kvm/Kvm.vue b/src/views/Operations/Kvm/Kvm.vue
new file mode 100644
index 00000000..1a41baaf
--- /dev/null
+++ b/src/views/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/Operations/Kvm/KvmConsole.vue b/src/views/Operations/Kvm/KvmConsole.vue
new file mode 100644
index 00000000..c028a9fc
--- /dev/null
+++ b/src/views/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/Operations/Kvm/index.js b/src/views/Operations/Kvm/index.js
new file mode 100644
index 00000000..ac4f9667
--- /dev/null
+++ b/src/views/Operations/Kvm/index.js
@@ -0,0 +1,2 @@
+import Kvm from './Kvm.vue';
+export default Kvm;
diff --git a/src/views/Operations/ManagePowerUsage/ManagePowerUsage.vue b/src/views/Operations/ManagePowerUsage/ManagePowerUsage.vue
new file mode 100644
index 00000000..38dbf0b8
--- /dev/null
+++ b/src/views/Operations/ManagePowerUsage/ManagePowerUsage.vue
@@ -0,0 +1,165 @@
+<template>
+ <b-container fluid="xl">
+ <page-title :description="$t('pageManagePowerUsage.description')" />
+
+ <b-row>
+ <b-col sm="8" md="6" xl="12">
+ <dl>
+ <dt>{{ $t('pageManagePowerUsage.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('pageManagePowerUsage.powerCapSettingLabel')"
+ >
+ <b-form-checkbox
+ v-model="isPowerCapFieldEnabled"
+ data-test-id="managePowerUsage-checkbox-togglePowerCapField"
+ name="power-cap-setting"
+ >
+ {{ $t('pageManagePowerUsage.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('pageManagePowerUsage.powerCapLabel')"
+ label-for="input-1"
+ >
+ <b-form-text id="power-help-text">
+ {{
+ $t('pageManagePowerUsage.powerCapLabelTextInfo', {
+ min: 1,
+ max: 10000,
+ })
+ }}
+ </b-form-text>
+
+ <b-form-input
+ id="input-1"
+ v-model.number="powerCapValue"
+ :disabled="!isPowerCapFieldEnabled"
+ data-test-id="managePowerUsage-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="managePowerUsage-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: 'ManagePowerUsage',
+ 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) {
+ let newValue = value ? '' : null;
+ this.$v.$reset();
+ 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/Operations/ManagePowerUsage/index.js b/src/views/Operations/ManagePowerUsage/index.js
new file mode 100644
index 00000000..f3e95ac1
--- /dev/null
+++ b/src/views/Operations/ManagePowerUsage/index.js
@@ -0,0 +1,2 @@
+import ManagePowerUsage from './ManagePowerUsage.vue';
+export default ManagePowerUsage;
diff --git a/src/views/Operations/PowerRestorePolicy/PowerRestorePolicy.vue b/src/views/Operations/PowerRestorePolicy/PowerRestorePolicy.vue
new file mode 100644
index 00000000..8589aed3
--- /dev/null
+++ b/src/views/Operations/PowerRestorePolicy/PowerRestorePolicy.vue
@@ -0,0 +1,80 @@
+<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
+ v-for="policy in powerRestorePolicies"
+ :key="policy.state"
+ v-model="currentPowerRestorePolicy"
+ :value="policy.state"
+ name="power-restore-policy"
+ >
+ {{ policy.desc }}
+ </b-form-radio>
+ </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,
+ };
+ },
+ computed: {
+ powerRestorePolicies() {
+ return this.$store.getters['powerPolicy/powerRestorePolicies'];
+ },
+ currentPowerRestorePolicy: {
+ get() {
+ return this.$store.getters['powerPolicy/powerRestoreCurrentPolicy'];
+ },
+ set(policy) {
+ this.policyValue = policy;
+ },
+ },
+ },
+ created() {
+ this.startLoader();
+ Promise.all([
+ this.$store.dispatch('powerPolicy/getPowerRestorePolicies'),
+ this.$store.dispatch('powerPolicy/getPowerRestoreCurrentPolicy'),
+ ]).finally(() => this.endLoader());
+ },
+ methods: {
+ submitForm() {
+ this.startLoader();
+ this.$store
+ .dispatch(
+ 'powerPolicy/setPowerRestorePolicy',
+ this.policyValue || this.currentPowerRestorePolicy
+ )
+ .then((message) => this.successToast(message))
+ .catch(({ message }) => this.errorToast(message))
+ .finally(() => this.endLoader());
+ },
+ },
+};
+</script>
diff --git a/src/views/Operations/PowerRestorePolicy/index.js b/src/views/Operations/PowerRestorePolicy/index.js
new file mode 100644
index 00000000..fab0d477
--- /dev/null
+++ b/src/views/Operations/PowerRestorePolicy/index.js
@@ -0,0 +1,2 @@
+import PowerRestorePolicy from './PowerRestorePolicy.vue';
+export default PowerRestorePolicy;
diff --git a/src/views/Operations/RebootBmc/RebootBmc.vue b/src/views/Operations/RebootBmc/RebootBmc.vue
new file mode 100644
index 00000000..900619cd
--- /dev/null
+++ b/src/views/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/Operations/RebootBmc/index.js b/src/views/Operations/RebootBmc/index.js
new file mode 100644
index 00000000..ac31417e
--- /dev/null
+++ b/src/views/Operations/RebootBmc/index.js
@@ -0,0 +1,2 @@
+import RebootBmc from './RebootBmc.vue';
+export default RebootBmc;
diff --git a/src/views/Operations/SerialOverLan/SerialOverLan.vue b/src/views/Operations/SerialOverLan/SerialOverLan.vue
new file mode 100644
index 00000000..48a68345
--- /dev/null
+++ b/src/views/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/Operations/SerialOverLan/SerialOverLanConsole.vue b/src/views/Operations/SerialOverLan/SerialOverLanConsole.vue
new file mode 100644
index 00000000..0bda43db
--- /dev/null
+++ b/src/views/Operations/SerialOverLan/SerialOverLanConsole.vue
@@ -0,0 +1,148 @@
+<template>
+ <div :class="isFullWindow ? 'full-window-container' : 'terminal-container'">
+ <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 { 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: {
+ 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);
+ },
+ methods: {
+ openTerminal() {
+ const token = this.$store.getters['authentication/token'];
+
+ const ws = new WebSocket(`wss://${window.location.host}/console0`, [
+ token,
+ ]);
+
+ // Refer https://github.com/xtermjs/xterm.js/ for xterm implementation and addons.
+
+ const term = new Terminal({
+ fontSize: 15,
+ fontFamily:
+ 'SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
+ });
+
+ const attachAddon = new AttachAddon(ws);
+ term.loadAddon(attachAddon);
+
+ const fitAddon = new FitAddon();
+ term.loadAddon(fitAddon);
+
+ const SOL_THEME = {
+ background: '#19273c',
+ cursor: 'rgba(83, 146, 255, .5)',
+ scrollbar: 'rgba(83, 146, 255, .5)',
+ };
+ term.setOption('theme', SOL_THEME);
+
+ term.open(this.$refs.panel);
+ fitAddon.fit();
+
+ this.resizeConsoleWindow = throttle(() => {
+ fitAddon.fit();
+ }, 1000);
+ window.addEventListener('resize', this.resizeConsoleWindow);
+
+ try {
+ ws.onopen = function () {
+ console.log('websocket console0/ opened');
+ };
+ ws.onclose = function (event) {
+ console.log(
+ 'websocket console0/ closed. code: ' +
+ event.code +
+ ' reason: ' +
+ event.reason
+ );
+ };
+ } catch (error) {
+ console.log(error);
+ }
+ },
+ 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/Operations/SerialOverLan/index.js b/src/views/Operations/SerialOverLan/index.js
new file mode 100644
index 00000000..7c8bc7c0
--- /dev/null
+++ b/src/views/Operations/SerialOverLan/index.js
@@ -0,0 +1,2 @@
+import SerialOverLan from './SerialOverLan.vue';
+export default SerialOverLan;
diff --git a/src/views/Operations/ServerPowerOperations/BootSettings.vue b/src/views/Operations/ServerPowerOperations/BootSettings.vue
new file mode 100644
index 00000000..efd8d347
--- /dev/null
+++ b/src/views/Operations/ServerPowerOperations/BootSettings.vue
@@ -0,0 +1,140 @@
+<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 bootSettingsChanged =
+ this.$v.form.bootOption.$dirty || this.$v.form.oneTimeBoot.$dirty;
+ const tpmPolicyChanged = this.$v.form.tpmPolicyOn.$dirty;
+ let settings;
+ let bootSource = null;
+ let overrideEnabled = null;
+ let tpmEnabled = null;
+
+ if (bootSettingsChanged) {
+ // If bootSource or overrideEnabled changed get
+ // both current values to send with request
+ bootSource = this.form.bootOption;
+ overrideEnabled = this.form.oneTimeBoot;
+ }
+ 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/Operations/ServerPowerOperations/ServerPowerOperations.vue b/src/views/Operations/ServerPowerOperations/ServerPowerOperations.vue
new file mode 100644
index 00000000..9e030837
--- /dev/null
+++ b/src/views/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/Operations/ServerPowerOperations/index.js b/src/views/Operations/ServerPowerOperations/index.js
new file mode 100644
index 00000000..10430047
--- /dev/null
+++ b/src/views/Operations/ServerPowerOperations/index.js
@@ -0,0 +1,2 @@
+import ServerPowerOperations from './ServerPowerOperations.vue';
+export default ServerPowerOperations;
diff --git a/src/views/Operations/VirtualMedia/ModalConfigureConnection.vue b/src/views/Operations/VirtualMedia/ModalConfigureConnection.vue
new file mode 100644
index 00000000..b0bcfb2b
--- /dev/null
+++ b/src/views/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/Operations/VirtualMedia/VirtualMedia.vue b/src/views/Operations/VirtualMedia/VirtualMedia.vue
new file mode 100644
index 00000000..8a3d5add
--- /dev/null
+++ b/src/views/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/Operations/VirtualMedia/index.js b/src/views/Operations/VirtualMedia/index.js
new file mode 100644
index 00000000..4573e865
--- /dev/null
+++ b/src/views/Operations/VirtualMedia/index.js
@@ -0,0 +1,2 @@
+import VirtualMedia from './VirtualMedia.vue';
+export default VirtualMedia;