Update the default firmware page

- Minor updates made to the general layout and styles
  - Changes to some page copy
  - Moves update firmware form to bottom of page
- Adds dynamic TFTP upload option
- Adds dynamic card layout for BMC and host firmwre
  - 2 cards for combined
  - 4 cards for separate
- Removes FirmwareSingleImage components that were used for IBM builds

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Ib5465ecc30dd1505824bf41c82d33b7655d5e598
diff --git a/src/views/Configuration/Firmware/Firmware.vue b/src/views/Configuration/Firmware/Firmware.vue
index 5001702..e27c8a2 100644
--- a/src/views/Configuration/Firmware/Firmware.vue
+++ b/src/views/Configuration/Firmware/Firmware.vue
@@ -1,378 +1,93 @@
 <template>
   <b-container fluid="xl">
-    <page-title :description="$t('pageFirmware.pageDescription')" />
-    <b-row class="mb-4">
-      <b-col md="10" lg="12" xl="8" class="pr-xl-4">
-        <!-- Firmware on BMC -->
-        <page-section :section-title="$t('pageFirmware.firmwareOnBmc')">
-          <b-card-group deck>
-            <!-- Current FW -->
-            <b-card header-bg-variant="success">
-              <template #header>
-                <dl class="mb-0">
-                  <dt>{{ $t('pageFirmware.current') }}</dt>
-                  <dd class="mb-0">{{ bmcFirmwareCurrentVersion }}</dd>
-                </dl>
-              </template>
-              <dl class="my-0">
-                <dt>{{ $t('pageFirmware.state') }}:</dt>
-                <dd>{{ bmcFirmwareCurrentState }}</dd>
-              </dl>
-              <template #footer></template>
-            </b-card>
+    <page-title />
+    <alerts-server-power
+      v-if="isServerPowerOffRequired"
+      :is-host-off="isHostOff"
+    />
 
-            <!-- Backup FW -->
-            <b-card footer-class="p-0">
-              <template #header>
-                <dl class="mb-0">
-                  <dt>{{ $t('pageFirmware.backup') }}</dt>
-                  <dd class="mb-0">{{ bmcFirmwareBackupVersion }}</dd>
-                </dl>
-              </template>
-              <dl class="my-0">
-                <dt>{{ $t('pageFirmware.state') }}:</dt>
-                <dd>{{ bmcFirmwareBackupState }}</dd>
-              </dl>
-              <template #footer>
-                <b-btn
-                  v-b-modal.modal-reboot-backup-bmc
-                  :disabled="!bmcFirmwareBackupVersion"
-                  variant="link"
-                  size="sm"
-                >
-                  <icon-switch class="d-none d-sm-inline-block" />
-                  {{ $t('pageFirmware.makeCurrentVersion') }}</b-btn
-                >
-              </template>
-            </b-card>
-          </b-card-group>
-        </page-section>
+    <!-- Firmware cards -->
+    <b-row>
+      <b-col xl="10">
+        <!-- BMC Firmware -->
+        <bmc-cards :is-page-disabled="isPageDisabled" />
 
-        <!-- Firmware on Host -->
-        <page-section :section-title="$t('pageFirmware.firmwareOnHost')">
-          <b-card-group deck>
-            <!-- Current FW -->
-            <b-card header-bg-variant="success">
-              <template #header>
-                <dl class="mb-0">
-                  <dt>{{ $t('pageFirmware.current') }}</dt>
-                  <dd class="mb-0">{{ hostFirmwareCurrentVersion }}</dd>
-                </dl>
-              </template>
-              <!-- State -->
-              <dl class="my-0">
-                <dt>{{ $t('pageFirmware.state') }}:</dt>
-                <dd>{{ hostFirmwareCurrentState }}</dd>
-              </dl>
-            </b-card>
-
-            <!-- Backup FW -->
-            <b-card>
-              <template #header>
-                <dl class="mb-0">
-                  <dt>{{ $t('pageFirmware.backup') }}</dt>
-                  <dd class="mb-0">{{ hostFirmwareBackupVersion }}</dd>
-                </dl>
-              </template>
-              <!-- State -->
-              <dl class="my-0">
-                <dt>{{ $t('pageFirmware.state') }}:</dt>
-                <dd>{{ hostFirmwareBackupState }}</dd>
-              </dl>
-            </b-card>
-          </b-card-group>
-        </page-section>
-      </b-col>
-
-      <!-- Update code -->
-      <b-col sm="8" xl="4" class="update-code pl-xl-4">
-        <page-section :section-title="$t('pageFirmware.updateCode')">
-          <b-form @submit.prevent="onSubmitUpload">
-            <b-form-group :label="$t('pageFirmware.form.uploadLocation')">
-              <b-form-radio v-model="isWorkstationSelected" :value="true">
-                {{ $t('pageFirmware.form.workstation') }}
-              </b-form-radio>
-              <b-form-radio v-model="isWorkstationSelected" :value="false">
-                {{ $t('pageFirmware.form.tftpServer') }}
-              </b-form-radio>
-            </b-form-group>
-
-            <!-- Workstation Upload -->
-            <template v-if="isWorkstationSelected">
-              <b-form-group :label="$t('pageFirmware.form.imageFile')">
-                <b-form-text id="image-file-help-block">
-                  {{ $t('pageFirmware.form.onlyTarFilesAccepted') }}
-                </b-form-text>
-                <form-file
-                  id="image-file"
-                  accept=".tar"
-                  :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.tftpServerAddress')"
-                label-for="tftp-ip"
-              >
-                <b-form-text id="server-address-help-block">
-                  {{ $t('pageFirmware.form.tftpServerAddressHelper') }}
-                </b-form-text>
-                <b-form-input
-                  id="tftp-id"
-                  v-model="tftpIpAddress"
-                  type="text"
-                  :state="getValidationState($v.tftpIpAddress)"
-                  aria-describedby="server-address-help-block"
-                  @input="$v.tftpIpAddress.$touch()"
-                />
-                <b-form-invalid-feedback role="alert">
-                  {{ $t('global.form.fieldRequired') }}
-                </b-form-invalid-feedback>
-              </b-form-group>
-              <b-form-group
-                :label="$t('pageFirmware.form.imageFileName')"
-                label-for="tftp-file-name"
-              >
-                <b-form-input
-                  id="tftp-file-name"
-                  v-model="tftpFileName"
-                  type="text"
-                  :state="getValidationState($v.tftpFileName)"
-                  @input="$v.tftpFileName.$touch()"
-                />
-                <b-form-invalid-feedback role="alert">
-                  {{ $t('global.form.fieldRequired') }}
-                </b-form-invalid-feedback>
-              </b-form-group>
-            </template>
-
-            <!-- Info alert -->
-            <alert variant="info" class="mt-4 mb-5">
-              <p class="font-weight-bold mb-1">
-                {{ $t('pageFirmware.alert.updateProcess') }}
-              </p>
-              <p>{{ $t('pageFirmware.alert.updateProcessInfo') }}</p>
-            </alert>
-
-            <b-btn type="submit" variant="primary">
-              {{ $t('pageFirmware.form.uploadAndRebootBmcOrHost') }}
-            </b-btn>
-          </b-form>
-        </page-section>
+        <!-- Host Firmware -->
+        <host-cards v-if="!isSingleFileUploadEnabled" />
       </b-col>
     </b-row>
 
-    <!-- Modals -->
-    <modal-upload @ok="uploadFirmware" />
-    <modal-reboot-backup-bmc
-      :current="bmcFirmwareCurrentVersion || '--'"
-      :backup="bmcFirmwareBackupVersion || '--'"
-      @ok="switchBmcFirmware"
-    />
+    <!-- Update firmware-->
+    <page-section
+      :section-title="$t('pageFirmware.sectionTitleUpdateFirmware')"
+    >
+      <b-row>
+        <b-col sm="8" md="6" xl="4">
+          <!-- Update form -->
+          <form-update
+            :is-host-off="isHostOff"
+            :is-page-disabled="isPageDisabled"
+          />
+        </b-col>
+      </b-row>
+    </page-section>
   </b-container>
 </template>
 
 <script>
-import { requiredIf } from 'vuelidate/lib/validators';
-import { mapGetters } from 'vuex';
-import IconSwitch from '@carbon/icons-vue/es/arrows--horizontal/20';
-
+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 Alert from '@/components/Global/Alert';
-import ModalUpload from './FirmwareModalUpload';
-import ModalRebootBackupBmc from './FirmwareModalRebootBackupBmc';
-import FormFile from '@/components/Global/FormFile';
 
-import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
 import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
-import BVToastMixin from '@/components/Mixins/BVToastMixin';
 
 export default {
-  name: 'Firmware',
+  name: 'FirmwareSingleImage',
   components: {
-    Alert,
-    IconSwitch,
-    ModalRebootBackupBmc,
-    ModalUpload,
+    AlertsServerPower,
+    BmcCards,
+    FormUpdate,
+    HostCards,
     PageSection,
     PageTitle,
-    FormFile,
   },
-  mixins: [BVToastMixin, LoadingBarMixin, VuelidateMixin],
+  mixins: [LoadingBarMixin],
   beforeRouteLeave(to, from, next) {
     this.hideLoader();
-    this.clearRebootTimeout();
     next();
   },
   data() {
     return {
-      isWorkstationSelected: true,
-      file: null,
-      tftpIpAddress: null,
-      tftpFileName: null,
-      timeoutId: null,
-      loading: loading,
+      loading,
+      isServerPowerOffRequired:
+        process.env.VUE_APP_SERVER_OFF_REQUIRED === 'true',
     };
   },
   computed: {
-    ...mapGetters('firmware', [
-      'bmcFirmwareCurrentVersion',
-      'bmcFirmwareCurrentState',
-      'bmcFirmwareBackupVersion',
-      'bmcFirmwareBackupState',
-      'hostFirmwareCurrentVersion',
-      'hostFirmwareCurrentState',
-      'hostFirmwareBackupVersion',
-      'hostFirmwareBackupState',
-    ]),
-  },
-  watch: {
-    isWorkstationSelected: function () {
-      this.$v.$reset();
-      this.file = null;
-      this.tftpIpAddress = null;
-      this.tftpFileName = null;
+    hostStatus() {
+      return this.$store.getters['global/hostStatus'];
+    },
+    isHostOff() {
+      return this.hostStatus === 'off' ? true : false;
+    },
+    isSingleFileUploadEnabled() {
+      return this.$store.getters['firmware/isSingleFileUploadEnabled'];
+    },
+    isPageDisabled() {
+      if (this.isServerPowerOffRequired) {
+        return !this.isHostOff || this.loading || this.isOperationInProgress;
+      }
+      return this.loading || this.isOperationInProgress;
     },
   },
   created() {
     this.startLoader();
-    this.$store.dispatch('firmware/getUpdateServiceApplyTime');
     this.$store
       .dispatch('firmware/getFirmwareInformation')
       .finally(() => this.endLoader());
   },
-  validations() {
-    return {
-      file: {
-        required: requiredIf(function () {
-          return this.isWorkstationSelected;
-        }),
-      },
-      tftpIpAddress: {
-        required: requiredIf(function () {
-          return !this.isWorkstationSelected;
-        }),
-      },
-      tftpFileName: {
-        required: requiredIf(function () {
-          return !this.isWorkstationSelected;
-        }),
-      },
-    };
-  },
-  methods: {
-    onFileUpload(file) {
-      this.file = file;
-      this.$v.file.$touch();
-    },
-    uploadFirmware() {
-      const startTime = this.$options.filters.formatTime(new Date());
-      this.setRebootTimeout(360000); //6 minute timeout
-      this.infoToast(
-        this.$t('pageFirmware.toast.infoUploadStartTimeMessage', { startTime }),
-        { title: this.$t('pageFirmware.toast.infoUploadStartTimeTitle') }
-      );
-      if (this.isWorkstationSelected) {
-        this.dispatchWorkstationUpload();
-      } else {
-        this.dispatchTftpUpload();
-      }
-    },
-    dispatchWorkstationUpload() {
-      this.$store
-        .dispatch('firmware/uploadFirmware', this.file)
-        .then((success) =>
-          this.infoToast(success, {
-            title: this.$t('pageFirmware.toast.successUploadTitle'),
-          })
-        )
-        .catch(({ message }) => {
-          this.errorToast(message);
-          this.clearRebootTimeout();
-        });
-    },
-    dispatchTftpUpload() {
-      const data = {
-        address: this.tftpIpAddress,
-        filename: this.tftpFileName,
-      };
-      this.$store
-        .dispatch('firmware/uploadFirmwareTFTP', data)
-        .then((success) =>
-          this.infoToast(success, {
-            title: this.$t('pageFirmware.toast.successUploadTitle'),
-          })
-        )
-        .catch(({ message }) => {
-          this.errorToast(message);
-          this.clearRebootTimeout();
-        });
-    },
-    switchBmcFirmware() {
-      this.setRebootTimeout();
-      this.$store
-        .dispatch('firmware/switchBmcFirmware')
-        .then((success) =>
-          this.infoToast(success, { title: this.$t('global.status.success') })
-        )
-        .catch(({ message }) => {
-          this.errorToast(message);
-          this.clearRebootTimeout();
-        });
-    },
-    setRebootTimeout(timeoutMs = 60000) {
-      // Set a timeout to disable page interactions while
-      // an upload or BMC reboot is in progress
-      this.startLoader();
-      this.timeoutId = setTimeout(() => {
-        this.endLoader();
-        this.infoToast(
-          this.$t('pageFirmware.toast.infoRefreshApplicationMessage'),
-          {
-            title: this.$t('pageFirmware.toast.infoRefreshApplicationTitle'),
-            refreshAction: true,
-          }
-        );
-      }, timeoutMs);
-    },
-    clearRebootTimeout() {
-      if (this.timeoutId) {
-        clearTimeout(this.timeoutId);
-        this.endLoader();
-      }
-    },
-    onSubmitUpload() {
-      this.$v.$touch();
-      if (this.$v.$invalid) return;
-      this.$bvModal.show('modal-upload');
-    },
-  },
 };
 </script>
-
-<style lang="scss" scoped>
-.update-code {
-  border-left: none;
-  @include media-breakpoint-up(xl) {
-    border-left: 1px solid gray('300');
-  }
-}
-.card-footer {
-  height: 40px;
-}
-.card-body {
-  padding: 0.75rem 1.25rem;
-}
-</style>
diff --git a/src/views/Configuration/Firmware/FirmwareAlertServerPower.vue b/src/views/Configuration/Firmware/FirmwareAlertServerPower.vue
new file mode 100644
index 0000000..0471311
--- /dev/null
+++ b/src/views/Configuration/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="!isHostOff" 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: {
+    isHostOff: {
+      required: true,
+      type: Boolean,
+    },
+  },
+  computed: {
+    isOperationInProgress() {
+      return this.$store.getters['controls/isOperationInProgress'];
+    },
+  },
+};
+</script>
diff --git a/src/views/Configuration/Firmware/FirmwareCardsBmc.vue b/src/views/Configuration/Firmware/FirmwareCardsBmc.vue
new file mode 100644
index 0000000..2f038b9
--- /dev/null
+++ b/src/views/Configuration/Firmware/FirmwareCardsBmc.vue
@@ -0,0 +1,133 @@
+<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-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,
+    };
+  },
+  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/Configuration/Firmware/FirmwareCardsHost.vue b/src/views/Configuration/Firmware/FirmwareCardsHost.vue
new file mode 100644
index 0000000..b4a8e90
--- /dev/null
+++ b/src/views/Configuration/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/Configuration/Firmware/FirmwareFormUpdate.vue b/src/views/Configuration/Firmware/FirmwareFormUpdate.vue
new file mode 100644
index 0000000..9f67e8d
--- /dev/null
+++ b/src/views/Configuration/Firmware/FirmwareFormUpdate.vue
@@ -0,0 +1,204 @@
+<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"
+          >
+            <b-form-text id="image-file-help-block">
+              {{ $t('pageFirmware.form.updateFirmware.imageFileHelperText') }}
+            </b-form-text>
+            <form-file
+              id="image-file"
+              accept=".tar"
+              :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 && !isHostOff"
+          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,
+    },
+    isHostOff: {
+      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/Configuration/Firmware/FirmwareModalRebootBackupBmc.vue b/src/views/Configuration/Firmware/FirmwareModalRebootBackupBmc.vue
deleted file mode 100644
index 305bb6c..0000000
--- a/src/views/Configuration/Firmware/FirmwareModalRebootBackupBmc.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-  <b-modal
-    id="modal-reboot-backup-bmc"
-    :ok-title="$t('pageFirmware.modal.rebootFromBackup.primaryAction')"
-    :cancel-title="$t('global.action.cancel')"
-    :title="$t('pageFirmware.modal.rebootFromBackup.title')"
-    @ok="$emit('ok')"
-  >
-    <p>
-      {{ $t('pageFirmware.modal.rebootFromBackup.message1', { backup }) }}
-    </p>
-    <p>
-      {{ $t('pageFirmware.modal.rebootFromBackup.message2', { current }) }}
-    </p>
-    <p class="font-weight-bold">
-      {{ $t('pageFirmware.modal.rebootFromBackup.message3', { backup }) }}
-    </p>
-  </b-modal>
-</template>
-
-<script>
-export default {
-  props: {
-    current: {
-      type: String,
-      required: true,
-    },
-    backup: {
-      type: String,
-      required: true,
-    },
-  },
-};
-</script>
diff --git a/src/views/Configuration/Firmware/FirmwareModalSwitchToRunning.vue b/src/views/Configuration/Firmware/FirmwareModalSwitchToRunning.vue
new file mode 100644
index 0000000..dc4a497
--- /dev/null
+++ b/src/views/Configuration/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/Configuration/Firmware/FirmwareModalUpdateFirmware.vue b/src/views/Configuration/Firmware/FirmwareModalUpdateFirmware.vue
new file mode 100644
index 0000000..1835521
--- /dev/null
+++ b/src/views/Configuration/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/Configuration/Firmware/FirmwareModalUpload.vue b/src/views/Configuration/Firmware/FirmwareModalUpload.vue
deleted file mode 100644
index 3425904..0000000
--- a/src/views/Configuration/Firmware/FirmwareModalUpload.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<template>
-  <b-modal
-    id="modal-upload"
-    ok-variant="danger"
-    :title="$t('pageFirmware.modal.uploadAndRebootBmcOrHost.title')"
-    :ok-title="$t('pageFirmware.modal.uploadAndRebootBmcOrHost.primaryAction')"
-    :cancel-title="$t('global.action.cancel')"
-    @ok="$emit('ok')"
-  >
-    <p>
-      {{ $t('pageFirmware.modal.uploadAndRebootBmcOrHost.message1') }}
-    </p>
-    <p>
-      {{ $t('pageFirmware.modal.uploadAndRebootBmcOrHost.message2') }}
-    </p>
-    <p class="font-weight-bold">
-      {{ $t('pageFirmware.modal.uploadAndRebootBmcOrHost.message3') }}
-    </p>
-  </b-modal>
-</template>