Repository: arenadata/adcm (language: python)

Issue:
Bump and cleanup

3006 sonartest fix
https://tracker.yandex.ru/ADCM-3006


Discussion / hints:

Fix patch:
diff --git a/web/src/app/core/services/cluster.service.ts b/web/src/app/core/services/cluster.service.ts
index b7a29b6dc9..a09134688e 100644
--- a/web/src/app/core/services/cluster.service.ts
+++ b/web/src/app/core/services/cluster.service.ts
@@ -145,11 +145,11 @@ export class ClusterService {
   }
 
   getHosts(p: ParamMap) {
-    return this.api.getList<Host>(this.Cluster.host, p);
+    return this.api.getList<Host>(this.Cluster?.host, p);
   }
 
   addHost(host_id: number) {
-    return this.api.post(this.Cluster.host, { host_id });
+    return this.api.post(this.Cluster?.host, { host_id });
   }
 
   reset(): Observable<WorkerInstance> {
diff --git a/web/src/app/core/services/stack.service.ts b/web/src/app/core/services/stack.service.ts
index 01492ab184..36c2e3c221 100644
--- a/web/src/app/core/services/stack.service.ts
+++ b/web/src/app/core/services/stack.service.ts
@@ -16,10 +16,10 @@ import { Bundle } from '@app/core/types';
 import { environment } from '@env/environment';
 import { select, Store } from '@ngrx/store';
 import { combineLatest, Observable } from 'rxjs';
-import { filter, map, mergeMap, switchMap } from 'rxjs/operators';
+import {filter, map, mergeMap, switchMap, tap} from 'rxjs/operators';
 import { ListResult } from '@app/models/list-result';
 
-export type StackInfo = 'cluster' | 'host' | 'provider' | 'service' | 'bundle';
+export type StackInfo = 'cluster' | 'host' | 'provider' | 'service' | 'bundle' | 'prototype';
 
 const UPLOAD_URL = `${environment.apiRoot}stack/upload/`,
   LOAD_URL = `${environment.apiRoot}stack/load/`;
@@ -33,7 +33,8 @@ export class StackService {
     return this.store.pipe(
       select(getStack),
       filter((a) => a && !!Object.keys(a).length),
-      switchMap((s) => this.api.get<ListResult<T>>(s[name], params).pipe(map((a) => a.results)))
+      switchMap((s) => this.api.get<ListResult<T>>(s[name], params).pipe(
+        map((a) => a.results)))
     );
   }
 
diff --git a/web/src/app/factories/list.factory.ts b/web/src/app/factories/list.factory.ts
index b94da0b5d3..9e4e407f51 100644
--- a/web/src/app/factories/list.factory.ts
+++ b/web/src/app/factories/list.factory.ts
@@ -83,6 +83,16 @@ export class ListFactory {
     };
   }
 
+  static updateColumn(): IComponentColumn<any> {
+    return {
+      label: 'Upgrade',
+      type: 'component',
+      className: 'list-control',
+      headerClassName: 'list-control',
+      component: UpgradeComponent,
+    };
+  }
+
   static importColumn<T>(listDirective: AdwpListDirective<T>): IButtonsColumn<T> {
     return {
       label: 'Import',
@@ -117,16 +127,6 @@ export class ListFactory {
     };
   }
 
-  static updateColumn(): IComponentColumn<any> {
-    return {
-      label: 'Upgrade',
-      type: 'component',
-      className: 'list-control',
-      headerClassName: 'list-control',
-      component: UpgradeComponent,
-    };
-  }
-
   static deleteColumn<T>(listDirective: AdwpListDirective<T>): IButtonsColumn<T> {
     return {
       type: 'buttons',
diff --git a/web/src/app/shared/add-component/add-button.component.ts b/web/src/app/shared/add-component/add-button.component.ts
index 0e3087a70d..f3b23bd36b 100644
--- a/web/src/app/shared/add-component/add-button.component.ts
+++ b/web/src/app/shared/add-component/add-button.component.ts
@@ -49,6 +49,7 @@ export class AddButtonComponent extends BaseDirective implements OnDestroy {
   @Input() name: string;
   @Input() component: Type<BaseFormDirective>;
   @Input() dialogConfig: AddButtonDialogConfig = {};
+  @Input() clusterId: number;
   @Output() added = new EventEmitter();
 
   constructor(@Inject(ADD_SERVICE_PROVIDER) private service: IAddService,
@@ -58,11 +59,15 @@ export class AddButtonComponent extends BaseDirective implements OnDestroy {
 
   showForm(data?: FormModel): void {
 
-    const model = data || this.service?.model(this.name);
+    const model: any = data || this.service?.model(this.name) || this.service.Cluster;
     if (this.component) {
       model.component = this.component;
     }
 
+    if (this.clusterId) {
+      model.clusterId = this.clusterId;
+    }
+
     const name = model.title || model.name;
     let title;
 
diff --git a/web/src/app/shared/add-component/add-form.component.ts b/web/src/app/shared/add-component/add-form.component.ts
index 81bff3a335..dfa76cf7a9 100644
--- a/web/src/app/shared/add-component/add-form.component.ts
+++ b/web/src/app/shared/add-component/add-form.component.ts
@@ -35,7 +35,7 @@ import { RbacFormDirective } from '@app/shared/add-component/rbac-form.directive
           <app-add-service #cc></app-add-service>
         </ng-container>
         <ng-container *ngSwitchCase="'host2cluster'">
-          <app-add-host2cluster (event)="message($event)" #cc></app-add-host2cluster>
+          <app-add-host2cluster (event)="message($event)" [clusterId]="model?.clusterId" #cc></app-add-host2cluster>
         </ng-container>
       </ng-container>
     </div>
diff --git a/web/src/app/shared/add-component/add-service-model.ts b/web/src/app/shared/add-component/add-service-model.ts
index e93c6f6905..347afc1e0d 100644
--- a/web/src/app/shared/add-component/add-service-model.ts
+++ b/web/src/app/shared/add-component/add-service-model.ts
@@ -16,6 +16,7 @@ export interface FormModel<T = any> {
   form?: FormGroup;
   success?: EventEmitter<{ flag: boolean; obj: any }>;
   component?: Type<BaseFormDirective>;
+  clusterId?: number;
 }
 
 export interface IAddService {
diff --git a/web/src/app/shared/add-component/add.service.ts b/web/src/app/shared/add-component/add.service.ts
index ac365ae67e..f8062fe67f 100644
--- a/web/src/app/shared/add-component/add.service.ts
+++ b/web/src/app/shared/add-component/add.service.ts
@@ -162,7 +162,7 @@ export class AddService implements IAddService {
   }
 
   getProtoServiceForCurrentCluster() {
-    return this.api.get<StackBase[]>(this.cluster.Cluster.serviceprototype).pipe(
+    return this.api.get<StackBase[]>(this.cluster.Cluster?.serviceprototype).pipe(
       map((a: ServicePrototype[]) =>
         a
           .filter((b) => !b.selected)
@@ -179,7 +179,7 @@ export class AddService implements IAddService {
   }
 
   getHostListForCurrentCluster() {
-    return this.api.get<Host[]>(this.cluster.Cluster.host).pipe(
+    return this.api.get<Host[]>(this.cluster.Cluster?.host).pipe(
       map((hosts) =>
         hosts
           .map((host) => ({
diff --git a/web/src/app/shared/add-component/host.component.ts b/web/src/app/shared/add-component/host.component.ts
index 686ef93ba8..063fd57ec6 100644
--- a/web/src/app/shared/add-component/host.component.ts
+++ b/web/src/app/shared/add-component/host.component.ts
@@ -134,7 +134,7 @@ export class HostComponent extends BaseFormDirective implements OnInit {
 
     if (provider && provider.actions) {
       const actions = provider.actions.filter((a) => a.button === ACTION_NAME);
-      if (actions && actions.length) {
+      if (actions && actions?.length) {
         this.action.inputData = { actions };
         this.onCancel();
         this.action.onClick();
@@ -144,7 +144,7 @@ export class HostComponent extends BaseFormDirective implements OnInit {
 
   save() {
     const data = clearEmptyField(this.form.value) as Host;
-    if (this.displayMode !== 0) data.cluster_id = this.service.Cluster.id;
+    if (this.displayMode !== 0) data.cluster_id = this.service.Cluster?.id;
     this.service
       .addHost(data)
       .pipe(
@@ -163,7 +163,7 @@ export class HostComponent extends BaseFormDirective implements OnInit {
   }
 
   getNextPageClusters() {
-    const count = this.clusters$.getValue().length;
+    const count = this.clusters$.getValue()?.length;
     if (count === this.pageCluster * this.limit) {
       this.pageCluster++;
       this.getClusters();
@@ -171,7 +171,7 @@ export class HostComponent extends BaseFormDirective implements OnInit {
   }
 
   getNextPageProvider() {
-    const count = this.providers$.getValue().length;
+    const count = this.providers$.getValue()?.length;
     if (count === this.pageProvider * this.limit) {
       this.pageProvider++;
       this.getProviders();
diff --git a/web/src/app/shared/add-component/host2cluster.component.ts b/web/src/app/shared/add-component/host2cluster.component.ts
index 8ae0859513..c9d16642e2 100644
--- a/web/src/app/shared/add-component/host2cluster.component.ts
+++ b/web/src/app/shared/add-component/host2cluster.component.ts
@@ -9,16 +9,17 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
+import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
 import { MatCheckbox } from '@angular/material/checkbox';
 import { MatSelectionList, MatSelectionListChange } from '@angular/material/list';
 import { MatPaginator, PageEvent } from '@angular/material/paginator';
 import { openClose } from '@app/core/animations';
 import { Host } from '@app/core/types';
-
 import { BaseFormDirective } from './base-form.directive';
 import { HostComponent } from './host.component';
 import { DisplayMode } from './provider.component';
+import { ICluster } from "@app/models/cluster";
+import { tap } from "rxjs/operators";
 
 @Component({
   selector: 'app-add-host2cluster',
@@ -67,6 +68,7 @@ export class Host2clusterComponent extends BaseFormDirective implements OnInit,
   showForm = false;
   Count = 0;
   displayMode: DisplayMode = DisplayMode.default;
+  @Input() clusterId: number;
   @Output() event = new EventEmitter();
   @ViewChild('form') hostForm: HostComponent;
   @ViewChild('listHosts') listHosts: MatSelectionList;
@@ -76,11 +78,18 @@ export class Host2clusterComponent extends BaseFormDirective implements OnInit,
   selected: { [key: number]: boolean } = {};
 
   get disabled() {
-    return !Object.keys(this.selected).length;
+    return !Object.keys(this.selected)?.length;
   }
 
   ngOnInit() {
-    this.getAvailableHosts();
+    if (!this.service.Cluster) {
+      const cluster$ = this.service['api'].getOne<ICluster>('cluster', this.clusterId);
+      cluster$.pipe(
+        tap((cluster: ICluster) => (this.service['cluster'].Cluster = cluster))
+      ).subscribe(() => this.getAvailableHosts());
+    } else {
+      this.getAvailableHosts();
+    }
   }
 
   getAvailableHosts(pageIndex = 0, pageSize = 10) {
@@ -92,7 +101,7 @@ export class Host2clusterComponent extends BaseFormDirective implements OnInit,
         this.showForm = !r.count;
         this.displayMode = r.count > 0 ? 2 : 1;
         this.list = r.results;
-        if (this.listHosts?.options.length) this.allCbx.checked = false;
+        if (this.listHosts?.options?.length) this.allCbx.checked = false;
       });
   }
 
@@ -114,8 +123,8 @@ export class Host2clusterComponent extends BaseFormDirective implements OnInit,
 
   save() {
     if (this.hostForm.form.valid) {
-      const host = this.hostForm.form.value;
-      host.cluster_id = this.service.Cluster.id;
+      const host = this.hostForm?.form?.value;
+      host.cluster_id = this.service?.Cluster?.id || this.clusterId;
       this.service
         .addHost(host)
         .pipe(this.takeUntil())
diff --git a/web/src/app/shared/components/actions/actions.component.spec.ts b/web/src/app/shared/components/actions/actions.component.spec.ts
index c070bba953..5570616a7e 100644
--- a/web/src/app/shared/components/actions/actions.component.spec.ts
+++ b/web/src/app/shared/components/actions/actions.component.spec.ts
@@ -39,7 +39,7 @@ describe('ActionsComponent', () => {
   it('Func `calcWidth` when the width of the window is greater than the buttons width sum', () => {
     const arr = [40, 30, 60];
     const result = component.calcWidth(1200, arr);
-    expect(result).toEqual([arr.length, 130, false]);
+    expect(result).toEqual([arr?.length, 130, false]);
   });
 
   it('Func `calcWidth` when the width of the window is less than the buttons width sum', () => {
diff --git a/web/src/app/shared/components/actions/actions.component.ts b/web/src/app/shared/components/actions/actions.component.ts
index 5c0eb312f3..64778ef990 100644
--- a/web/src/app/shared/components/actions/actions.component.ts
+++ b/web/src/app/shared/components/actions/actions.component.ts
@@ -61,7 +61,7 @@ export class ActionsComponent extends BaseDirective implements OnInit, AfterView
   @Input() cluster: { id: number; hostcomponent: string };
   @Input() set source(actions: IAction[]) {
     this.actions = actions;
-    if (!actions.length) this.render.setStyle(this.more.nativeElement, 'display', 'none');
+    if (!actions?.length) this.render.setStyle(this.more.nativeElement, 'display', 'none');
   }
 
   stateButtons = 0;
@@ -87,8 +87,8 @@ export class ActionsComponent extends BaseDirective implements OnInit, AfterView
   }
 
   ngAfterViewChecked(): void {
-    if (this.stateButtons !== this.buttons.length) {
-      this.stateButtons = this.buttons.length;
+    if (this.stateButtons !== this.buttons?.length) {
+      this.stateButtons = this.buttons?.length;
       setTimeout(() => this.onresize(), 0);
     }
   }
diff --git a/web/src/app/shared/components/actions/actions.directive.ts b/web/src/app/shared/components/actions/actions.directive.ts
index 67cdee0307..eeaadbbe01 100644
--- a/web/src/app/shared/components/actions/actions.directive.ts
+++ b/web/src/app/shared/components/actions/actions.directive.ts
@@ -46,10 +46,10 @@ export class ActionsDirective {
     if (!model.actions?.length) return { data: { title: 'No parameters for run the action', model: null, component: null } };
 
     const act = model.actions[0];
-    const isMulty = model.actions.length > 1;
+    const isMulty = model.actions?.length > 1;
 
-    const width = isMulty || act.config?.config.length || act.hostcomponentmap?.length ? '90%' : '400px';
-    const title = act.ui_options?.disclaimer ? act.ui_options.disclaimer : isMulty ? 'Run an actions?' : `Run an action [ ${act.display_name} ]?`;
+    const width = isMulty || act.config?.config?.length || act.hostcomponentmap?.length ? '90%' : '400px';
+    const title = act.ui_options?.disclaimer ? act.ui_options.disclaimer : isMulty ? 'Run actions?' : `Run an action [ ${act.display_name} ]?`;
 
     return {
       width,
diff --git a/web/src/app/shared/components/actions/master/master.component.html b/web/src/app/shared/components/actions/master/master.component.html
index 6ef51411eb..08da7c21b7 100644
--- a/web/src/app/shared/components/actions/master/master.component.html
+++ b/web/src/app/shared/components/actions/master/master.component.html
@@ -1,4 +1,4 @@
-<ng-container *ngIf="model?.actions.length === 0">
+<ng-container *ngIf="model?.actions?.length === 0">
   <p>No data for the action!</p>
 </ng-container>
 
@@ -54,7 +54,7 @@
     <mat-checkbox [(ngModel)]="verbose">Verbose</mat-checkbox>
 
     <div class="controls">
-      <ng-container *ngIf="model.actions.length > 1">
+      <ng-container *ngIf="model.actions?.length > 1">
         <button mat-raised-button color="primary" (click)="action = null">Back</button>
         <span [style.flex]="1"></span>
       </ng-container>
diff --git a/web/src/app/shared/components/actions/master/master.component.spec.ts b/web/src/app/shared/components/actions/master/master.component.spec.ts
index 06d8335877..c416c08858 100644
--- a/web/src/app/shared/components/actions/master/master.component.spec.ts
+++ b/web/src/app/shared/components/actions/master/master.component.spec.ts
@@ -94,7 +94,7 @@ describe('MasterComponent', () => {
     fixture.detectChanges();
     const compHost: HTMLElement = fixture.debugElement.nativeElement;
     // expect(compHost.querySelector('i').textContent).toBe("Let's choose first");
-    const buttonsLength = compHost.querySelector('mat-action-list').getElementsByTagName('button').length;
+    const buttonsLength = compHost.querySelector('mat-action-list').getElementsByTagName('button')?.length;
     expect(buttonsLength).toBeGreaterThan(1);
   });
 
diff --git a/web/src/app/shared/components/actions/master/master.component.ts b/web/src/app/shared/components/actions/master/master.component.ts
index bbc5878d07..8b23e7f309 100644
--- a/web/src/app/shared/components/actions/master/master.component.ts
+++ b/web/src/app/shared/components/actions/master/master.component.ts
@@ -56,7 +56,7 @@ export class ActionMasterComponent extends BaseDirective implements DynamicCompo
   }
 
   ngOnInit(): void {
-    if (this.model.actions.length === 1) this.choose(this.model.actions[0]);
+    if (this.model?.actions?.length === 1) this.choose(this.model.actions[0]);
   }
 
   choose(action: IAction) {
diff --git a/web/src/app/shared/components/index.ts b/web/src/app/shared/components/index.ts
index 35a4a4f93b..915718197d 100644
--- a/web/src/app/shared/components/index.ts
+++ b/web/src/app/shared/components/index.ts
@@ -16,7 +16,7 @@ export * from './status-info.component';
 export * from './crumbs.component';
 export * from './dialog.component';
 export * from './main-info.component';
-export * from './upgrade.component';
+export * from './upgrades/upgrade.component';
 export * from './actions/actions.component';
 export * from './actions/master/master.component';
 export * from './import/import.component';
diff --git a/web/src/app/shared/components/upgrades/master/master.component.html b/web/src/app/shared/components/upgrades/master/master.component.html
new file mode 100644
index 0000000000..1e33a0346e
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/master/master.component.html
@@ -0,0 +1,70 @@
+<ng-container *ngIf="model?.upgrades?.length === 0">
+  <p>No data for the upgrades!</p>
+</ng-container>
+
+<ng-container *ngIf="!upgrade; else simple">
+  <em>Let's choose first</em>
+  <mat-action-list>
+    <button class="action-button warn" color="primary" mat-list-item *ngFor="let upgrade of model.upgrades"
+      (click)="choose(upgrade)">{{ upgrade.name }}</button>
+  </mat-action-list>
+  <p><button mat-raised-button (click)="cancel()" color="primary">Cancel</button></p>
+</ng-container>
+
+<ng-template #simple>
+  <div *ngIf="show === 'none'">
+    <ng-container *ngTemplateOutlet="controls; context: { value: undefined }"></ng-container>
+  </div>
+
+  <div *ngIf="show === 'config'">
+    <app-upgrade-master-config [upgrade]="upgrade" #wc></app-upgrade-master-config>
+    <ng-container *ngTemplateOutlet="controls; context: { value: { config: wc.fields } }"></ng-container>
+  </div>
+
+  <div *ngIf="show === 'hostmap'">
+    <app-service-host [tabIndex]="2" [cluster]="model.cluster" [hideButton]="true" #hostmap
+      [actionParameters]="upgrade.hostcomponentmap" [fixedButton]="false"></app-service-host>
+    <ng-container *ngTemplateOutlet="controls; context: { value: { hostmap: hostmap } }"></ng-container>
+  </div>
+
+  <mat-horizontal-stepper #stepper linear *ngIf="show === 'stepper'">
+    <mat-step [stepControl]="wcs.fields.form">
+      <ng-template matStepLabel>Configuration</ng-template>
+      <app-upgrade-master-config [upgrade]="upgrade" #wcs></app-upgrade-master-config>
+      <div class="controls-container" [class.margin]="needMargin()">
+        <mat-checkbox [(ngModel)]="verbose">Verbose</mat-checkbox>
+        <div class="controls">
+          <button mat-raised-button (click)="cancel()" color="primary">Cancel</button>
+          <button mat-raised-button matStepperNext color="primary">Next</button>
+        </div>
+      </div>
+    </mat-step>
+    <mat-step>
+      <ng-template matStepLabel>Host Components</ng-template>
+      <app-service-host [tabIndex]="2" [cluster]="model.cluster" #hms [hideButton]="true"
+        [actionParameters]="upgrade.hostcomponentmap" [fixedButton]="false"></app-service-host>
+      <ng-container *ngTemplateOutlet="controls; context: { value: { config: wcs.fields, hostmap: hms } }">
+      </ng-container>
+    </mat-step>
+    <mat-step label="Done"></mat-step>
+  </mat-horizontal-stepper>
+
+</ng-template>
+
+<ng-template #controls let-value="value">
+  <div class="controls-container" [class.margin]="needMargin()">
+
+    <mat-checkbox [(ngModel)]="verbose">Verbose</mat-checkbox>
+
+    <div class="controls">
+      <ng-container *ngIf="model['upgradable']">
+        <button mat-raised-button color="primary" (click)="upgrade = null">Back</button>
+        <span [style.flex]="1"></span>
+      </ng-container>
+
+      <button mat-raised-button (click)="cancel()" color="primary" tabindex="-1">Cancel</button>
+      <button mat-raised-button #runBtn (click)="run(value)" color="accent" [disabled]="isDisabled(value)" tabindex="1">Run</button>
+    </div>
+
+  </div>
+</ng-template>
diff --git a/web/src/app/shared/components/upgrades/master/master.component.scss b/web/src/app/shared/components/upgrades/master/master.component.scss
new file mode 100644
index 0000000000..96d0fd8a14
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/master/master.component.scss
@@ -0,0 +1,18 @@
+.action-button {
+  background: none !important;
+  margin: 6px 0;
+
+  &:hover {
+    background: rgba(255, 255, 255, 0.04) !important;
+  }
+}
+
+.controls-container {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  &.margin {
+    margin-top: 26px !important;
+  }
+}
diff --git a/web/src/app/shared/components/upgrades/master/master.component.spec.ts b/web/src/app/shared/components/upgrades/master/master.component.spec.ts
new file mode 100644
index 0000000000..7ebd618889
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/master/master.component.spec.ts
@@ -0,0 +1,295 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { MatListModule } from '@angular/material/list';
+import { ApiService } from '@app/core/api/api.service';
+import { FieldService } from '@app/shared/configuration/services/field.service';
+import { ConfigFieldsComponent } from '@app/shared/configuration/fields/fields.component';
+import { ServiceHostComponent } from '@app/shared/host-components-map/services2hosts/service-host.component';
+import { UpgradeMasterConfigComponent } from './upgrade-master-config.component';
+import { UpgradeMasterComponent as MasterComponent } from './master.component';
+import { MasterService } from './master.service';
+import {IConfig} from "@app/shared/configuration/types";
+import {IActionParameter, IUIOptions} from "@app/core/types";
+
+describe('MasterComponent', () => {
+  let component: MasterComponent;
+  let fixture: ComponentFixture<MasterComponent>;
+
+  let ApiServiceStub: Partial<ApiService>;
+  let FieldServiceStub: Partial<FieldService>;
+
+  beforeEach(async () => {
+    ApiServiceStub = {};
+    FieldServiceStub = new FieldService({} as FormBuilder);
+
+    TestBed.configureTestingModule({
+      imports: [MatListModule],
+      declarations: [MasterComponent, UpgradeMasterConfigComponent],
+      providers: [MasterService, {provide: ApiService, useValue: ApiServiceStub}, {
+        provide: FieldService,
+        useValue: FieldServiceStub
+      }],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MasterComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('input data for model property must be defined', () => {
+    component.model = {upgrades: []};
+    fixture.detectChanges();
+    expect(component.model).toBeDefined();
+  });
+
+  it('input data for model.actons property must be array', () => {
+    component.model = {upgrades: []};
+    fixture.detectChanges();
+    expect(component.model.upgrades).toBeDefined();
+    expect(component.model.upgrades).toEqual(jasmine.any(Array));
+  });
+
+  it('should be show template if model.upgrades.length === 0', () => {
+    component.model = {upgrades: []};
+    fixture.detectChanges();
+    const compHost: HTMLElement = fixture.debugElement.nativeElement;
+    expect(compHost.querySelector('p').textContent).toBe('No data for the upgrades!');
+  });
+
+  it('should be show template for current upgrade if model.upgrades.length === 1 and config = null and host-map = null', () => {
+    component.model = {
+      upgrades: [{
+        bundle_id: 1,
+        config: null,
+        description: '',
+        do: 'url',
+        from_edition: ['community'],
+        hostcomponentmap: null,
+        id: 1,
+        license: '',
+        license_url: '',
+        max_strict: true,
+        max_version: '',
+        min_strict: false,
+        min_version: '',
+        name: '',
+        state_available: '',
+        state_on_success: '',
+        ui_options: null,
+        upgradable: true,
+        url: ''
+      }]
+    };
+    fixture.detectChanges();
+    const compHost: HTMLElement = fixture.debugElement.nativeElement;
+    const cancelButton = compHost.querySelector('button[color="primary"]');
+    const runButton = compHost.querySelector('button[color="accent"]');
+    expect(runButton).toBeTruthy();
+    expect(cancelButton).toBeTruthy();
+    expect(cancelButton.textContent).toContain('Cancel');
+    expect(runButton.textContent).toContain('Run');
+  });
+
+  it('should be show upgrades list for choose current upgrade if model.upgrades.length > 1', () => {
+    component.model = {
+      upgrades: [
+        {
+          bundle_id: 1,
+          config: {} as IConfig,
+          description: '',
+          do: 'url',
+          from_edition: ['community'],
+          hostcomponentmap: [] as IActionParameter[],
+          id: 1,
+          license: '',
+          license_url: '',
+          max_strict: true,
+          max_version: '',
+          min_strict: false,
+          min_version: '',
+          name: '',
+          state_available: '',
+          state_on_success: '',
+          ui_options: {} as IUIOptions,
+          upgradable: true,
+          url: ''
+        },
+        {
+          bundle_id: 1,
+          config: {} as IConfig,
+          description: '',
+          do: 'url',
+          from_edition: ['community'],
+          hostcomponentmap: [] as IActionParameter[],
+          id: 1,
+          license: '',
+          license_url: '',
+          max_strict: true,
+          max_version: '',
+          min_strict: false,
+          min_version: '',
+          name: '',
+          state_available: '',
+          state_on_success: '',
+          ui_options: {} as IUIOptions,
+          upgradable: true,
+          url: ''
+        }
+      ]
+    };
+    fixture.detectChanges();
+    const compHost: HTMLElement = fixture.debugElement.nativeElement;
+    // expect(compHost.querySelector('i').textContent).toBe("Let's choose first");
+    const buttonsLength = compHost.querySelector('mat-action-list').getElementsByTagName('button').length;
+    expect(buttonsLength).toBeGreaterThan(1);
+  });
+
+  it('should be show template for current upgrade if config exist only', () => {
+    component.model = {
+      upgrades: [
+        {
+          bundle_id: 1,
+          config: {
+            config: [
+              {
+                type: 'string',
+                name: 'test',
+                display_name: 'display name test',
+                subname: '',
+                default: null,
+                value: null,
+                required: false,
+                description: '',
+                read_only: false,
+                activatable: false,
+                group_config: null,
+              }
+            ]
+          } as IConfig,
+          description: '',
+          do: 'url',
+          from_edition: ['community'],
+          hostcomponentmap: null,
+          id: 1,
+          license: '',
+          license_url: '',
+          max_strict: true,
+          max_version: '',
+          min_strict: false,
+          min_version: '',
+          name: '',
+          state_available: '',
+          state_on_success: '',
+          ui_options: {} as IUIOptions,
+          upgradable: true,
+          url: ''
+        }
+      ]
+    };
+    fixture.detectChanges();
+    const de = fixture.debugElement.nativeElement;
+    const config = de.querySelector('app-upgrade-master-config');
+    const hm = de.querySelector('app-service-host');
+    expect(hm).toBeNull();
+    expect(config).toBeTruthy();
+  });
+
+  it('should be show template for current upgrade if host-map exist only', () => {
+    component.model = {
+      upgrades: [{
+        bundle_id: 1, config: null, description: '', do: 'url', from_edition: ['community'],
+        hostcomponentmap: [{component: '', action: 'add', service: ''}] as IActionParameter[], id: 1, license: '',
+        license_url: '', max_strict: true, max_version: '', min_strict: false, min_version: '', name: '',
+        state_available: '', state_on_success: '', ui_options: {} as IUIOptions, upgradable: true, url: ''
+      }]
+    };
+    fixture.detectChanges();
+    const de = fixture.debugElement.nativeElement;
+    const config = de.querySelector('app-upgrade-master-config');
+    const hm = de.querySelector('app-service-host');
+    expect(config).toBeNull();
+    expect(hm).toBeTruthy();
+  });
+
+  it('should be show template for current upgrade with config and host-map', () => {
+    component.model = {
+      upgrades: [{
+        bundle_id: 1, config: {
+          config: [
+            {
+              type: 'string',
+              name: 'test',
+              display_name: 'display name test',
+              subname: '',
+              default: null,
+              value: null,
+              required: false,
+              description: '',
+              read_only: false,
+              activatable: false,
+              group_config: null,
+            }
+          ]
+        } as IConfig, description: '', do: 'url', from_edition: ['community'],
+        hostcomponentmap: [{component: '', action: 'add', service: ''}] as IActionParameter[], id: 1, license: '',
+        license_url: '', max_strict: true, max_version: '', min_strict: false, min_version: '', name: '',
+        state_available: '', state_on_success: '', ui_options: {} as IUIOptions, upgradable: true, url: ''
+      }]
+    };
+    fixture.detectChanges();
+    const de = fixture.debugElement.nativeElement;
+    const config = de.querySelector('app-upgrade-master-config');
+    const hm = de.querySelector('app-service-host');
+    expect(config).toBeTruthy();
+    expect(hm).toBeTruthy();
+  });
+
+  it('should be undefined value if ServiceHostComponent ConfigFieldComponent not exist', () => {
+    const service = fixture.debugElement.injector.get(MasterService);
+    const result = service.parseData(undefined);
+    expect(result).toBeUndefined();
+  });
+
+  it('check value when ConfigFieldComponent exist', () => {
+    const service = fixture.debugElement.injector.get(MasterService);
+
+    const config = {
+      form: new FormGroup({string_ctr: new FormControl('string_test'), bool_ctr: new FormControl(true)}),
+      rawConfig: {
+        config: [
+          {name: 'string_ctr', type: 'string', value: 'string_test'},
+          {name: 'bool_ctr', type: 'boolean', value: true}
+        ]
+      }
+    } as ConfigFieldsComponent;
+
+    const result = service.parseData({config});
+    expect(result).toEqual({config: {string_ctr: 'string_test', bool_ctr: true}, hc: undefined, attr: undefined});
+  });
+
+  it('check value when ServiceHostComponent exist', () => {
+    const service = fixture.debugElement.injector.get(MasterService);
+    const hc = [{host_id: 1, service_id: 4, component_id: 1, id: 9}];
+    const hostmap = {statePost: {data: hc}} as ServiceHostComponent;
+    const result = service.parseData({hostmap});
+    expect(result).toEqual({config: undefined, hc, attr: undefined});
+  });
+});
diff --git a/web/src/app/shared/components/upgrades/master/master.component.ts b/web/src/app/shared/components/upgrades/master/master.component.ts
new file mode 100644
index 0000000000..3f56dabc38
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/master/master.component.ts
@@ -0,0 +1,76 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { Component, ElementRef, EventEmitter, OnInit, ViewChild } from '@angular/core';
+import { DynamicComponent, DynamicEvent } from '@app/shared/directives/dynamic/dynamic.directive';
+import { BaseDirective } from '@app/shared/directives';
+import { IMasterData, IValue, MasterService, whatShow } from './master.service';
+import { IUpgrade } from "@app/shared/components";
+import { UpgradeParameters } from "@app/shared/components/upgrades/upgrade.directive";
+
+@Component({
+  selector: 'app-master',
+  templateUrl: './master.component.html',
+  styleUrls: ['./master.component.scss'],
+  providers: [MasterService],
+})
+export class UpgradeMasterComponent extends BaseDirective implements DynamicComponent, OnInit {
+  event: EventEmitter<DynamicEvent> = new EventEmitter();
+  model: UpgradeParameters;
+  upgrade: IUpgrade;
+  show: whatShow;
+
+  verbose = false;
+
+  @ViewChild('runBtn', { read: ElementRef }) runBtn: ElementRef;
+
+  constructor(private service: MasterService) {
+    super();
+  }
+
+  ngOnInit(): void {
+    if (this.model.upgrades?.length === 1) this.choose(this.model.upgrades[0]);
+  }
+
+  choose(upgrade: IUpgrade) {
+    this.upgrade = upgrade;
+    this.show = this.service.spotShow(upgrade);
+  }
+
+  isDisabled(value: IValue) {
+    return value && ((value.hostmap && value.hostmap.noValid) || (value.config && !value.config.form?.valid));
+  }
+
+  run(value: IValue = {}) {
+    const data: IMasterData = this.service.parseData(value);
+
+    if (data) {
+      data.verbose = this.verbose;
+    }
+
+    this.service
+      .send(this.upgrade.do, data)
+      .pipe(this.takeUntil())
+      .subscribe(() => this.cancel());
+  }
+
+  onEnterKey() {
+    this.runBtn.nativeElement.click();
+  }
+
+  cancel() {
+    this.event.emit({ name: 'cancel' });
+  }
+
+  needMargin() {
+    return !(this?.model?.upgrades[0]?.config?.config?.length > 0 && this?.model?.upgrades[0]?.hostcomponentmap?.length > 0);
+  }
+}
diff --git a/web/src/app/shared/components/upgrades/master/master.service.ts b/web/src/app/shared/components/upgrades/master/master.service.ts
new file mode 100644
index 0000000000..d99e6f599f
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/master/master.service.ts
@@ -0,0 +1,63 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { Injectable } from '@angular/core';
+import { ApiService } from '@app/core/api';
+import { FieldService } from '@app/shared/configuration/services/field.service';
+import { ConfigFieldsComponent } from '@app/shared/configuration/fields/fields.component';
+import { ServiceHostComponent } from '@app/shared/host-components-map/services2hosts/service-host.component';
+import { Post } from '@app/shared/host-components-map/types';
+import { IConfigAttr } from '@app/shared/configuration/types';
+import { IOutput } from '@app/shared/configuration/services/field.service';
+import { IUpgrade } from "@app/shared/components";
+
+export interface IValue {
+  config?: ConfigFieldsComponent;
+  hostmap?: ServiceHostComponent;
+}
+
+export enum whatShow {
+  none = 'none',
+  config = 'config',
+  hostMap = 'hostmap',
+  stepper = 'stepper',
+}
+
+export interface IMasterData {
+  attr: IConfigAttr;
+  config: IOutput;
+  hc: Post[];
+  verbose?: boolean;
+}
+
+@Injectable()
+export class MasterService {
+  constructor(private api: ApiService, private configService: FieldService) {}
+
+  spotShow(upgrade: IUpgrade): whatShow {
+    const config = upgrade.config?.config?.length;
+    const hm = upgrade.hostcomponentmap?.length;
+    return config ? (hm ? whatShow.stepper : whatShow.config) : hm ? whatShow.hostMap : whatShow.none;
+  }
+
+  parseData(v: IValue): IMasterData {
+    const getData = (attr: IConfigAttr, c: ConfigFieldsComponent, h: ServiceHostComponent) => {
+      const config = c ? this.configService.parseValue(c.form.value, c.rawConfig.config) : undefined;
+      const hc = h?.statePost.data;
+      return { attr, config, hc };
+    };
+    return v ? getData(v.config?.attr, v.config, v.hostmap) : undefined;
+  }
+
+  send(url: string, value: { config: any; hc: Post[] }) {
+    return this.api.post(url, value);
+  }
+}
diff --git a/web/src/app/shared/components/upgrades/master/upgrade-master-config.component.ts b/web/src/app/shared/components/upgrades/master/upgrade-master-config.component.ts
new file mode 100644
index 0000000000..c8c515ee29
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/master/upgrade-master-config.component.ts
@@ -0,0 +1,36 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { Component, Input, ViewChild } from '@angular/core';
+import { FieldService } from '@app/shared/configuration/services/field.service';
+import { ConfigFieldsComponent } from '@app/shared/configuration/fields/fields.component';
+import { IUpgrade } from "@app/shared/components";
+
+@Component({
+  selector: 'app-upgrade-master-config',
+  template: `
+    <div class="config-tools">
+      <mat-checkbox [(ngModel)]="advanced" *ngIf="fields.isAdvanced">Advanced</mat-checkbox>
+    </div>
+    <app-config-fields #fields [model]="upgrade?.config" [ngStyle]="{ display: 'inherit' }"></app-config-fields>
+  `,
+})
+export class UpgradeMasterConfigComponent {
+  @Input() upgrade: IUpgrade;
+
+  @ViewChild('fields') fields: ConfigFieldsComponent = {} as ConfigFieldsComponent;
+
+  set advanced(value: boolean) {
+    this.config.filterApply(this.fields.dataOptions, { advanced: value, search: '' });
+  }
+
+  constructor(private config: FieldService) {}
+}
diff --git a/web/src/app/shared/components/upgrade.component.ts b/web/src/app/shared/components/upgrades/upgrade.component.ts
similarity index 54%
rename from web/src/app/shared/components/upgrade.component.ts
rename to web/src/app/shared/components/upgrades/upgrade.component.ts
index 3e7a7d9eba..6dd895e43d 100644
--- a/web/src/app/shared/components/upgrade.component.ts
+++ b/web/src/app/shared/components/upgrades/upgrade.component.ts
@@ -10,17 +10,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { MatDialog } from '@angular/material/dialog';
-import { ApiService } from '@app/core/api';
-import { EmmitRow } from '@app/core/types';
-import { concat, Observable, of } from 'rxjs';
-import { filter, map, switchMap } from 'rxjs/operators';
+import { ApiService } from '../../../core/api';
+import { EmmitRow, IActionParameter, IUIOptions } from '../../../core/types';
+import { Observable } from 'rxjs';
+import { filter } from 'rxjs/operators';
 import { EventHelper } from '@adwp-ui/widgets';
-
-import { BaseDirective } from '../directives';
-import { DialogComponent } from './dialog.component';
-import { IIssues } from '@app/models/issue';
-import { IssueHelper } from '@app/helpers/issue-helper';
+import { IIssues } from '../../../models/issue';
+import { IssueHelper } from '../../../helpers/issue-helper';
+import { IConfig } from "../../configuration/types";
 
 export interface UpgradeItem {
   upgradable: boolean;
@@ -28,22 +25,25 @@ export interface UpgradeItem {
   issue: IIssues;
 }
 
-export interface Upgrade {
-  id: number;
+export interface IUpgrade {
   bundle_id: number;
-  name: string;
+  config: IConfig;
   description: string;
   do: string;
-  upgradable: boolean;
-  from_edition: string[];
-  license: 'unaccepted' | 'absent';
+  from_edition: string[]
+  hostcomponentmap:IActionParameter[];
+  id: number;
+  license: string;
   license_url: string;
   max_strict: boolean;
   max_version: string;
   min_strict: boolean;
   min_version: string;
+  name: string;
   state_available: string;
   state_on_success: string;
+  ui_options: IUIOptions;
+  upgradable: boolean;
   url: string;
 }
 
@@ -62,17 +62,16 @@ export interface Upgrade {
     </button>
     <mat-menu #menu="matMenu" [overlapTrigger]="false" [xPosition]="xPosition" yPosition="below">
       <ng-template matMenuContent>
-        <button *ngFor="let item of list$ | async" mat-menu-item (click)="runUpgrade(item)">
+        <button *ngFor="let item of list$ | async" mat-menu-item [appUpgrades]="item" [clusterId]="pRow['id']">
           <span>{{ item.name || 'No name' }}</span>
         </button>
       </ng-template>
     </mat-menu>
   `
 })
-export class UpgradeComponent extends BaseDirective {
+export class UpgradeComponent {
   EventHelper = EventHelper;
-
-  list$: Observable<Upgrade[]>;
+  list$: Observable<IUpgrade[]>;
   pRow: UpgradeItem = { upgradable: false, upgrade: '', issue: null };
 
   @Input() xPosition = 'before';
@@ -86,51 +85,15 @@ export class UpgradeComponent extends BaseDirective {
   @Output()
   refresh: EventEmitter<EmmitRow> = new EventEmitter<EmmitRow>();
 
-  constructor(private api: ApiService, private dialog: MatDialog) {
-    super();
-  }
+  constructor(private api: ApiService) {}
 
   checkIssue() {
     return this.pRow.upgradable && !IssueHelper.isIssue(this.pRow.issue);
   }
 
-  runUpgrade(item: Upgrade) {
-    const license$ = item.license === 'unaccepted' ? this.api.put(`${item.license_url}accept/`, {}) : of();
-    const do$ = this.api.post<{ id: number }>(item.do, {});
-    this.fork(item)
-      .pipe(
-        switchMap(text =>
-          this.dialog
-            .open(DialogComponent, {
-              data: {
-                title: 'Are you sure you want to upgrade?',
-                text,
-                disabled: !item.upgradable,
-                controls: item.license === 'unaccepted' ? {
-                  label: 'Do you accept the license agreement?',
-                  buttons: ['Yes', 'No']
-                } : ['Yes', 'No']
-              }
-            })
-            .beforeClosed()
-            .pipe(
-              this.takeUntil(),
-              filter(yes => yes),
-              switchMap(() => concat(license$, do$))
-            )
-        )
-      )
-      .subscribe(row => this.refresh.emit({ cmd: 'refresh', row }));
-  }
-
-  fork(item: Upgrade) {
-    const flag = item.license === 'unaccepted';
-    return flag ? this.api.get<{ text: string }>(item.license_url).pipe(map(a => a.text)) : of(item.description);
-  }
-
   getUpgrades(upgrade: string): Observable<any> {
     return this.api.get(`${upgrade}?ordering=-name`).pipe(
-      filter((list: Upgrade[]) => !!list.length)
+      filter((list: IUpgrade[]) => !!list.length)
     );
   }
 }
diff --git a/web/src/app/shared/components/upgrades/upgrade.directive.ts b/web/src/app/shared/components/upgrades/upgrade.directive.ts
new file mode 100644
index 0000000000..e6808b5063
--- /dev/null
+++ b/web/src/app/shared/components/upgrades/upgrade.directive.ts
@@ -0,0 +1,221 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
+import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
+import { DialogComponent } from '../dialog.component';
+import { IUpgrade } from "./upgrade.component";
+import { concat, Observable, of } from "rxjs";
+import { filter, map, switchMap, tap } from "rxjs/operators";
+import { ApiService } from "@app/core/api";
+import { EmmitRow } from "@app/core/types";
+import { BaseDirective } from "../../directives";
+import { UpgradeMasterComponent as component } from "../upgrades/master/master.component";
+import { AddService } from "@app/shared/add-component/add.service";
+import { IRawHosComponent } from "@app/shared/host-components-map/types";
+
+export interface UpgradeParameters {
+  cluster?: {
+    id: number;
+    hostcomponent: IRawHosComponent;
+  };
+  upgrades: IUpgrade[];
+}
+
+@Directive({
+  selector: '[appUpgrades]'
+})
+export class UpgradesDirective extends BaseDirective {
+  @Input('appUpgrades') inputData: IUpgrade;
+  @Input() clusterId: number;
+  @Output() refresh: EventEmitter<EmmitRow> = new EventEmitter<EmmitRow>();
+
+  hc: IRawHosComponent;
+  needPrototype = false;
+
+  constructor(private api: ApiService, private dialog: MatDialog, private service: AddService) {
+    super();
+  }
+
+  @HostListener('click')
+  onClick() {
+    this.dialog.closeAll();
+    if (this.hasHostComponent) {
+      this.checkHostComponents();
+    } else {
+      this.prepare();
+    }
+  }
+
+  get hasConfig(): boolean {
+    return this?.inputData?.config?.config?.length > 0
+  }
+
+  get hasHostComponent(): boolean {
+    return this?.inputData?.hostcomponentmap?.length > 0
+  }
+
+  get hasDisclaimer(): boolean {
+    return !!this?.inputData?.ui_options['disclaimer']
+  }
+
+  prepare(): void {
+    let dialogModel: MatDialogConfig
+    const maxWidth = '1400px';
+    const isMulty = this?.inputData.upgradable;
+    const width = isMulty || this.hasConfig || this.hasHostComponent ? '90%' : '400px';
+    const title = this?.inputData.ui_options['disclaimer'] ? this?.inputData.ui_options['disclaimer'] : isMulty ? 'Run upgrades?' : `Run upgrade [ ${this?.inputData.name} ]?`;
+    const data: IUpgrade = this.inputData as IUpgrade;
+    const model: UpgradeParameters = {
+      cluster: {
+        id: this.clusterId,
+        hostcomponent: this.hc,
+      },
+      upgrades: [this.inputData],
+    }
+
+    dialogModel =  {
+      width,
+      maxWidth,
+      data: {
+        title,
+        model,
+        component,
+      }
+    };
+
+    if (this.hasDisclaimer) {
+      if (this.hasConfig || this.hasHostComponent) {
+        this.runUpgrade(data, dialogModel);
+      } else if (!this.hasConfig && !this.hasHostComponent) {
+        this.runOldUpgrade(data);
+      }
+    } else {
+      if (!this.hasConfig && !this.hasHostComponent) {
+        dialogModel.maxWidth = '1400px';
+        dialogModel.width = '400px';
+        dialogModel.height = '180px';
+        dialogModel.data.title = 'Are you sure you want to upgrade?'
+        dialogModel.data.text = 'The cluster will be prepared for upgrade';
+      }
+
+      this.dialog.open(DialogComponent, dialogModel);
+    }
+  }
+
+  runUpgrade(item: IUpgrade, dialogModel: MatDialogConfig) {
+    this.fork(item)
+      .pipe(
+        tap(text => {
+            return this.dialog
+              .open(DialogComponent, {
+                data: {
+                  title: 'Are you sure you want to upgrade?',
+                  text: item.ui_options['disclaimer'] || text,
+                  disabled: !item.upgradable,
+                  controls: item.license === 'unaccepted' ? {
+                    label: 'Do you accept the license agreement?',
+                    buttons: ['Yes', 'No']
+                  } : ['Yes', 'No']
+                }
+              })
+              .afterClosed()
+              .subscribe((res) => {
+                if (res) this.dialog.open(DialogComponent, dialogModel);
+              })
+          }
+        )
+      ).subscribe();
+  }
+
+  runOldUpgrade(item: IUpgrade) {
+    const license$ = item.license === 'unaccepted' ? this.api.put(`${item.license_url}accept/`, {}) : of();
+    const do$ = this.api.post<{ id: number }>(item.do, {});
+
+    this.fork(item)
+      .pipe(
+        switchMap(text =>
+          this.dialog
+            .open(DialogComponent, {
+              data: {
+                title: 'Are you sure you want to upgrade?',
+                text: item.ui_options['disclaimer'] || text,
+                disabled: !item.upgradable,
+                controls: item.license === 'unaccepted' ? {
+                  label: 'Do you accept the license agreement?',
+                  buttons: ['Yes', 'No']
+                } : ['Yes', 'No']
+              }
+            })
+            .beforeClosed()
+            .pipe(
+              filter(yes => yes),
+              switchMap(() => concat(license$, do$))
+            )
+        )
+      )
+      .subscribe(row => this.refresh.emit({cmd: 'refresh', row}));
+  }
+
+  fork(item: IUpgrade) {
+    const flag = item.license === 'unaccepted';
+    return flag ? this.api.get<{ text: string }>(item.license_url).pipe(map(a => a.text)) : of(item.description);
+  }
+
+  checkHostComponents() {
+    this.getClusterInfo()
+      .pipe(
+        tap((cluster): any => {
+          const hostComponentMap = this.inputData.hostcomponentmap;
+
+          hostComponentMap.forEach((hc, index) => {
+            if (!cluster.component.find(c => c.name === hc.component)) {
+              this.needPrototype = true;
+              const params = {
+                bundle_id: this.inputData.bundle_id,
+                type: 'component',
+                name: hc.component,
+                parent_name: hc.service,
+                limit: 50,
+                offset: 0
+              };
+
+              // fix later
+              this.service.getPrototype('prototype', params).subscribe((prototype): any => {
+                if (prototype[0]) {
+                  cluster.component.push(prototype[0]);
+                  this.hc = cluster;
+                }
+
+                if (hostComponentMap.length === index + 1) {
+                  if (!this.hc) this.hc = cluster;
+                  this.prepare();
+                }
+              })
+            }
+          })
+
+          if (!this.needPrototype) {
+            this.hc = cluster;
+            this.prepare();
+          }
+        }),
+      ).subscribe();
+  }
+
+  getClusterInfo(): Observable<any> {
+    return this.api.get(`api/v1/cluster/${this.clusterId}/hostcomponent/`)
+  }
+
+  getPrototype(params): Observable<any> {
+    return this.service.getPrototype('prototype', params)
+  }
+}
diff --git a/web/src/app/shared/configuration/field/field.component.html b/web/src/app/shared/configuration/field/field.component.html
index 0fede65b88..c381e6e613 100644
--- a/web/src/app/shared/configuration/field/field.component.html
+++ b/web/src/app/shared/configuration/field/field.component.html
@@ -48,7 +48,7 @@
     </span>
   </ng-container>
 </div>
-<mat-list class="compare-list-container" dense *ngIf="!options.hidden && options.compare.length">
+<mat-list class="compare-list-container" dense *ngIf="!options.hidden && options.compare?.length">
   <mat-list-item *ngFor="let item of options.compare" class="compare-list">
     <mat-icon matListIcon [style.backgroundColor]="item.color">more_vert</mat-icon>
     <span class="label">
diff --git a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html
index 7b8f77d3f1..af63450331 100644
--- a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html
+++ b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.html
@@ -6,7 +6,7 @@
         [style.color]="model.color">{{ model.name }}</span>
     </button>
     <button mat-raised-button color="primary"
-      (click)="toggleRelations()">{{ model.limit?.length ? setNotify() : model.relations.length }}</button>
+      (click)="toggleRelations()">{{ model.limit?.length ? setNotify() : model.relations?.length }}</button>
   </div>
   <div [ngClass]="{'relations-list':true, 'show': model.relations.length && isShow, 'disabled': isHostDisabled()}">
     <div *ngFor="let rel of model.relations">
diff --git a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts
index 845648b266..0eefcc724f 100644
--- a/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts
+++ b/web/src/app/shared/host-components-map/much-2-many/much-2-many.component.ts
@@ -37,8 +37,8 @@ export class Much2ManyComponent implements AfterViewChecked {
   }
 
   isDisabled() {
-    if (this.model.actions) return !this.model.actions.length;
-    return this.model.disabled;
+    if (this.model?.actions) return !this.model.actions.length;
+    return this.model?.disabled;
   }
 
   isHostDisabled() {
diff --git a/web/src/app/shared/host-components-map/services2hosts/service-host.component.html b/web/src/app/shared/host-components-map/services2hosts/service-host.component.html
index ce0c56252a..4fd26a919b 100644
--- a/web/src/app/shared/host-components-map/services2hosts/service-host.component.html
+++ b/web/src/app/shared/host-components-map/services2hosts/service-host.component.html
@@ -17,7 +17,7 @@ <h3><span>Components</span></h3>
   </div>
   <div>
     <h3><span>Hosts</span>
-      <app-add-button [name]="'host2cluster'" [asIcon]="true" [style.lineHeight.px]="16"></app-add-button>
+      <app-add-button [name]="'host2cluster'" [asIcon]="true" [style.lineHeight.px]="16" [clusterId]="cluster?.id"></app-add-button>
     </h3>
     <div class="wrapper">
       <div *ngIf="!Hosts?.length">
diff --git a/web/src/app/shared/host-components-map/services2hosts/service-host.component.spec.ts b/web/src/app/shared/host-components-map/services2hosts/service-host.component.spec.ts
index 5b26c73042..2d84023761 100644
--- a/web/src/app/shared/host-components-map/services2hosts/service-host.component.spec.ts
+++ b/web/src/app/shared/host-components-map/services2hosts/service-host.component.spec.ts
@@ -93,7 +93,7 @@ describe('Service Host Map Component', () => {
     expect(component).toBeDefined();
   });
 
-  it('should display compnents and hosts as button', () => {
+  it('should display components and hosts as button', () => {
     initDefault(genData());
     const dom = domFirstElems(fixture.nativeElement);
     const components = dom.components;
@@ -133,7 +133,7 @@ describe('Service Host Map Component', () => {
     const host_isLinked = () => host.querySelector('.m2m').classList.contains('linked');
 
     // start: host - select
-    host_btn.click();
+    host_btn.click(); // added class .selected
     fixture.detectChanges();
     expect(host_isSelect()).toBeTrue();
 
diff --git a/web/src/app/shared/host-components-map/services2hosts/service-host.component.ts b/web/src/app/shared/host-components-map/services2hosts/service-host.component.ts
index a716cf4d74..aab21be83e 100644
--- a/web/src/app/shared/host-components-map/services2hosts/service-host.component.ts
+++ b/web/src/app/shared/host-components-map/services2hosts/service-host.component.ts
@@ -16,10 +16,11 @@ import { keyChannelStrim } from '@app/core/services';
 import { EventMessage, IEMObject, SocketState } from '@app/core/store';
 import { IActionParameter } from '@app/core/types';
 import { Store } from '@ngrx/store';
-
 import { SocketListenerDirective } from '@app/shared/directives';
 import { getSelected, TakeService } from '../take.service';
 import { CompTile, HostTile, IRawHosComponent, Post, StatePost, Tile } from '../types';
+import { ApiService } from "@app/core/api";
+import { Observable } from "rxjs";
 
 @Component({
   selector: 'app-service-host',
@@ -39,7 +40,7 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
   form = new FormGroup({});
 
   @Input()
-  cluster: { id: number; hostcomponent: string };
+  cluster: { id: number; hostcomponent: string | IRawHosComponent };
 
   /**
    * fixed position buttons for the scrolling
@@ -79,7 +80,7 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
     this.sourceMap.set('compo', v);
   }
 
-  constructor(public service: TakeService, private channel: ChannelService, socket: Store<SocketState>) {
+  constructor(public service: TakeService, private channel: ChannelService, socket: Store<SocketState>, private api: ApiService) {
     super(socket);
   }
 
@@ -97,11 +98,15 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
       .subscribe((e) => (this.scrollEventData = e));
   }
 
+  getClusterInfo(): Observable<any> {
+    return this.api.get(`api/v1/cluster/${this.cluster.id}/hostcomponent/`);
+  }
+
   socketListener(m: EventMessage) {
-    const isCurrent = (type: string, id: number) => type === 'cluster' && id === this.cluster.id;
+    const isCurrent = (type: string, id: number) => type === 'cluster' && id === this.cluster?.id;
     if (
       (m.event === 'change_hostcomponentmap' || m.event === 'change_state') &&
-      isCurrent(m.object.type, m.object.id) &&
+      isCurrent(m.object.type, m.object?.id) &&
       !this.saveFlag
     ) {
       this.reset().load();
@@ -126,16 +131,16 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
 
   add(io: IEMObject) {
     const { id, type, details } = io;
-    if (details.type === 'cluster' && +details.value === this.cluster.id) {
+    if (details.type === 'cluster' && +details.value === this.cluster?.id && typeof this.cluster.hostcomponent === 'string') {
       this.service
-        .load(this.cluster.hostcomponent)
+        .load(this.cluster.hostcomponent as string)
         .pipe(this.takeUntil())
         .subscribe((raw: IRawHosComponent) => {
           if (type === 'host')
             this.Hosts = [
               ...this.Hosts,
               ...this.service.fillHost(
-                raw.host.map((h) => new HostTile(h)).filter((h) => h.id === id),
+                raw.host.map((h) => new HostTile(h)).filter((h) => h?.id === id),
                 this.actionParameters
               ),
             ];
@@ -143,11 +148,23 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
             this.Components = [
               ...this.Components,
               ...this.service.fillComponent(
-                raw.component.filter((a) => a.service_id === id && this.Components.every((b) => b.id !== a.id)),
+                raw.component.filter((a) => a.service_id === id && this.Components.every((b) => b?.id !== a?.id)),
                 this.actionParameters
               ),
             ];
         });
+    } else if (typeof this.cluster.hostcomponent !== 'string') {
+      this.getClusterInfo()
+        .pipe(this.takeUntil())
+        .subscribe((res) => {
+          this.Hosts = [
+            ...this.Hosts,
+            ...this.service.fillHost(
+              res.host.map((h) => new HostTile(h)).filter((h) => h?.id === id),
+              this.actionParameters
+            ),
+          ];
+        });
     }
   }
 
@@ -155,7 +172,7 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
   remove(io: IEMObject) {
     if (io.type === 'host') {
       const { id } = io;
-      this.Hosts = this.Hosts.filter((a) => a.id !== id);
+      this.Hosts = this.Hosts.filter((a) => a?.id !== id);
     }
   }
 
@@ -164,10 +181,14 @@ export class ServiceHostComponent extends SocketListenerDirective implements OnI
       if (this.initFlag) return;
       this.initFlag = true;
 
-      this.service
-        .load(this.cluster.hostcomponent)
-        .pipe(this.takeUntil())
-        .subscribe((raw: IRawHosComponent) => this.init(raw));
+      if (typeof this.cluster.hostcomponent === 'string' ) {
+        this.service
+          .load(this.cluster.hostcomponent)
+          .pipe(this.takeUntil())
+          .subscribe((raw: IRawHosComponent) => this.init(raw));
+      } else {
+        this.init(this.cluster.hostcomponent);
+      }
     }
   }
 
diff --git a/web/src/app/shared/host-components-map/take.service.ts b/web/src/app/shared/host-components-map/take.service.ts
index 1d2730f6a7..7ed2a93f1a 100644
--- a/web/src/app/shared/host-components-map/take.service.ts
+++ b/web/src/app/shared/host-components-map/take.service.ts
@@ -19,7 +19,17 @@ import { filter, map, switchMap, take } from 'rxjs/operators';
 import { AddService } from '../add-component/add.service';
 import { DialogComponent } from '../components';
 import { DependenciesComponent } from './dependencies.component';
-import { CompTile, TConstraint, HostTile, IRawHosComponent, IStream, Post, StatePost, Tile } from './types';
+import {
+  CompTile,
+  TConstraint,
+  HostTile,
+  IRawHosComponent,
+  IStream,
+  Post,
+  StatePost,
+  Tile,
+  PrototypePost
+} from './types';
 
 export const getSelected = (from: Tile[]): Tile => from.find((a) => a.isSelected);
 
@@ -82,9 +92,9 @@ export class TakeService {
     return this.api.get<IRawHosComponent>(url);
   }
 
-  save(cluster_id: number, hostcomponent: string, hc: Post[]) {
+  save(cluster_id: number, hostcomponent: string | IRawHosComponent, hc: Post[]) {
     const send = { cluster_id, hc };
-    return this.api.post<Post[]>(hostcomponent, send).pipe(take(1));
+    return this.api.post<Post[]>(hostcomponent as string, send).pipe(take(1));
   }
   //#endregion
 
@@ -102,7 +112,7 @@ export class TakeService {
       (c) =>
         new CompTile(
           c,
-          ap ? ap.filter((a) => a.service === c.service_name && a.component === c.name).map((b) => b.action) : null
+          ap ? ap.filter((a) => a?.service === c?.service_name && a?.component === c?.name).map((b) => b.action) : null
         )
     );
   }
@@ -236,7 +246,7 @@ export class TakeService {
     host.isLink = false;
     host.relations = host.relations.filter((r) => r.id !== component.id);
 
-    state.delete(new Post(host.id, component.service_id, component.id));
+    state.delete(this.containsPrototype(component, host));
     this.clearDependencies(component, state, cs, hs, form);
     this.setFormValue(component, form);
   }
@@ -294,7 +304,7 @@ export class TakeService {
       link.relations.push(target);
       target.relations.push(link);
       target.isLink = true;
-      state.add(new Post(host.id, component.service_id, component.id));
+      state.add(this.containsPrototype(component, host));
     }
     this.setFormValue(component, form);
   }
@@ -317,5 +327,13 @@ export class TakeService {
       )
       .subscribe();
   }
+
+  containsPrototype(component, host) {
+    if (component?.is_prototype) {
+      return new PrototypePost(host.id, component.service_id, component.id);
+    }
+
+    return new Post(host.id, component.service_id, component.id)
+  }
   //#endregion
 }
diff --git a/web/src/app/shared/host-components-map/types.ts b/web/src/app/shared/host-components-map/types.ts
index 4286bc2bee..ae7259310a 100644
--- a/web/src/app/shared/host-components-map/types.ts
+++ b/web/src/app/shared/host-components-map/types.ts
@@ -25,11 +25,19 @@ export interface Post {
   id?: number;
   host_id: number;
   service_id: number;
-  component_id: number;
+  component_id?: number;
+}
+
+export interface PrototypePost extends Post {
+  component_prototype_id: number;
 }
 
 export class Post implements Post {
-  constructor(public host_id: number, public service_id: number, public component_id: number, public id?: number) {}
+  constructor(public host_id: number, public service_id: number, public component_id?: number,  public id?: number ) {}
+}
+
+export class PrototypePost implements Post {
+  constructor(public host_id: number, public service_id: number, public component_prototype_id: number, public id?: number ) {}
 }
 
 /**
@@ -61,6 +69,7 @@ export class Tile {
   color?: 'none' | 'white' | 'gray' | 'yellow';
   notification?: string;
   mm?: string;
+  is_prototype?: boolean;
 }
 
 export class HostTile extends Tile {
@@ -79,14 +88,21 @@ export class CompTile extends Tile {
   requires: IRequires[];
   constructor(rawComponent: IComponent, public actions?: ActionParam[]) {
     super();
-    this.id = rawComponent.id;
-    this.service_id = rawComponent.service_id;
-    this.component = `${rawComponent.service_name}/${rawComponent.name}`;
-    this.name = rawComponent.display_name;
-    this.disabled = rawComponent.service_state !== 'created';
-    this.limit = rawComponent.constraint;
-    this.requires = rawComponent.requires;
-    this.prototype_id = rawComponent.prototype_id;
+    if (rawComponent) {
+      this.id = rawComponent.id;
+      this.service_id = rawComponent.service_id;
+      this.component = `${rawComponent.service_name}/${rawComponent.name}`;
+      this.name = rawComponent.display_name;
+      this.disabled = rawComponent.service_state !== 'created';
+      this.limit = rawComponent.constraint;
+      this.requires = rawComponent.requires;
+      this.prototype_id = rawComponent.prototype_id;
+    }
+    this.is_prototype = this.isPrototype(rawComponent);
+  }
+
+  isPrototype(component): boolean {
+    return component?.url?.includes('prototype')
   }
 }
 
@@ -125,7 +141,15 @@ export class StatePost {
   }
 
   update(data: Post[]) {
-    data.forEach((a) => this.add(new Post(a.host_id, a.service_id, a.component_id, a.id)));
+    data.forEach((a) => this.add(this.containsPrototype(a)));
+  }
+
+  containsPrototype(component): Post | PrototypePost {
+   if (component?.url?.includes('prototype')) {
+     return new PrototypePost(component.host_id, component.service_id, component.id);
+   }
+
+   return new Post(component.host_id, component.service_id, component.component_id, component.id);
   }
 }
 /**
diff --git a/web/src/app/shared/shared.module.ts b/web/src/app/shared/shared.module.ts
index d4e5cb6134..2d7109eab4 100644
--- a/web/src/app/shared/shared.module.ts
+++ b/web/src/app/shared/shared.module.ts
@@ -17,6 +17,7 @@ import { TranslateModule } from '@ngx-translate/core';
 import { MatTreeModule } from '@angular/material/tree';
 
 import { AddingModule } from './add-component/adding.module';
+import { UpgradeMasterComponent } from "@app/shared/components/upgrades/master/master.component";
 import {
   ActionMasterComponent,
   ButtonSpinnerComponent,
@@ -61,6 +62,7 @@ import { RbacRoleService } from '@app/services/rbac-role.service';
 import { RbacPolicyService } from '@app/services/rbac-policy.service';
 import { DynamicModule } from '@app/shared/directives/dynamic/dynamic.module';
 import { RbacObjectCandidateService } from '@app/services/rbac-object-candidate.service';
+import {UpgradeMasterConfigComponent} from "@app/shared/components/upgrades/master/upgrade-master-config.component";
 
 @NgModule({
   imports: [
@@ -96,6 +98,8 @@ import { RbacObjectCandidateService } from '@app/services/rbac-object-candidate.
     ExportComponent,
     ActionMasterComponent,
     ActionMasterConfigComponent,
+    UpgradeMasterComponent,
+    UpgradeMasterConfigComponent,
     StatusColumnComponent,
     StateColumnComponent,
     EditionColumnComponent,
diff --git a/web/src/app/shared/stuff.module.ts b/web/src/app/shared/stuff.module.ts
index d91f775cc8..38e89fa780 100644
--- a/web/src/app/shared/stuff.module.ts
+++ b/web/src/app/shared/stuff.module.ts
@@ -16,6 +16,7 @@ import { RouterModule } from '@angular/router';
 import { ActionsComponent, CrumbsComponent, UpgradeComponent } from './components';
 import { ActionListComponent } from './components/actions/action-list/action-list.component';
 import { ActionsDirective } from './components/actions/actions.directive';
+import { UpgradesDirective } from './components/upgrades/upgrade.directive';
 import { TooltipDirective } from '@app/shared/components/tooltip';
 import { PopoverDirective } from '@app/directives/popover.directive';
 import {
@@ -53,6 +54,7 @@ import { HasSelectedPipe } from '@app/pipes/has-selected.pipe';
     InfinityScrollDirective,
     ActionsComponent,
     ActionsDirective,
+    UpgradesDirective,
     ActionListComponent,
     MenuItemComponent,
     PopoverDirective,
@@ -85,6 +87,7 @@ import { HasSelectedPipe } from '@app/pipes/has-selected.pipe';
     InfinityScrollDirective,
     ActionsComponent,
     ActionsDirective,
+    UpgradesDirective,
     ActionListComponent,
     MenuItemComponent,
     PopoverDirective,


Test patch:
diff --git a/tests/ui_tests/app/page/cluster/locators.py b/tests/ui_tests/app/page/cluster/locators.py
index 49e0cf4d48..c4e814183c 100644
--- a/tests/ui_tests/app/page/cluster/locators.py
+++ b/tests/ui_tests/app/page/cluster/locators.py
@@ -17,6 +17,7 @@
 from tests.ui_tests.app.helpers.locator import (
     Locator,
 )
+from tests.ui_tests.app.page.common.host_components.locators import HostComponentsLocators
 from tests.ui_tests.app.page.host_list.locators import HostListLocators
 
 
@@ -69,38 +70,10 @@ class HostTable(HostListLocators.HostTable):
         ...
 
 
-class ClusterComponentsLocators:
+class ClusterComponentsLocators(HostComponentsLocators):
     """Cluster components page elements locators"""
 
-    restore_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Restore')]]", "Restore button")
-    save_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Save')]]", "Save button")
-
-    components_title = Locator(By.XPATH, "//h3[./span[contains(text(), 'Components')]]", "Title for Components block")
-    service_page_link = Locator(By.CSS_SELECTOR, "mat-card-content a[href*='service']", "Link to service page")
-
-    hosts_title = Locator(By.XPATH, "//h3[./span[contains(text(), 'Hosts')]]", "Title for Hosts block")
-    hosts_page_link = Locator(By.CSS_SELECTOR, "mat-card-content a[href*='host']", "Link to hosts page")
-    create_hosts_btn = Locator(By.CSS_SELECTOR, "button[adcm_test='create-btn']", "Create hosts button")
-
-    host_row = Locator(By.XPATH, "//div[./h3/span[contains(text(), 'Host')]]//app-much-2-many", "Host row")
-    component_row = Locator(
-        By.XPATH,
-        "//div[./h3/span[contains(text(), 'Components')]]//app-much-2-many",
-        "Component row",
-    )
-
-    class Row:
-        """Cluster components page row elements locators"""
-
-        name = Locator(By.XPATH, ".//button[@mat-button]/span/span[not(contains(@class, 'warn'))]", "Item name")
-        number = Locator(By.CSS_SELECTOR, "button[mat-raised-button] span:first-of-type", "Amount of links")
-        relations_row = Locator(By.CSS_SELECTOR, "div[class*='relations-list']", "Row with relations")
-
-        class RelationsRow:
-            """Cluster components page relations row elements locators"""
-
-            name = Locator(By.CSS_SELECTOR, "div>span", "Related item name")
-            delete_btn = Locator(By.CSS_SELECTOR, "button", "Delete item button")
+    pass
 
 
 class ClusterActionLocators:
diff --git a/tests/ui_tests/app/page/cluster/page.py b/tests/ui_tests/app/page/cluster/page.py
index a660bf71ec..c046f1557c 100644
--- a/tests/ui_tests/app/page/cluster/page.py
+++ b/tests/ui_tests/app/page/cluster/page.py
@@ -36,6 +36,7 @@
 from tests.ui_tests.app.page.common.group_config.page import CommonGroupConfigMenu
 from tests.ui_tests.app.page.common.group_config_list.locators import GroupConfigListLocators
 from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigList
+from tests.ui_tests.app.page.common.host_components.page import HostComponentsPage
 from tests.ui_tests.app.page.common.import_page.locators import ImportLocators
 from tests.ui_tests.app.page.common.import_page.page import ImportPage
 from tests.ui_tests.app.page.common.popups.locator import HostAddPopupLocators
@@ -49,14 +50,6 @@
 from tests.ui_tests.app.page.host_list.page import HostRowInfo
 
 
-@dataclass
-class ComponentsHostRowInfo:
-    """Information from host row about host on Components page"""
-
-    name: str
-    components: str
-
-
 @dataclass
 class StatusGroupInfo:
     """Information from group on Status page"""
@@ -493,7 +486,7 @@ def _check_mm_state(page: ClusterHostPage, row: WebElement):
         wait_until_step_succeeds(_check_mm_state, timeout=4, period=0.5, page=self, row=host_row)
 
 
-class ClusterComponentsPage(ClusterPageMixin):
+class ClusterComponentsPage(ClusterPageMixin, HostComponentsPage):
     """Cluster page components menu"""
 
     MENU_SUFFIX = 'host_component'
@@ -508,90 +501,6 @@ class ClusterComponentsPage(ClusterPageMixin):
         ClusterComponentsLocators.hosts_page_link,
     ]
 
-    def click_service_page_link(self):
-        """Click on Service page link"""
-        self.find_and_click(ClusterComponentsLocators.service_page_link)
-
-    def click_hosts_page_link(self):
-        """Click on Hosts page link"""
-        self.find_and_click(ClusterComponentsLocators.hosts_page_link)
-
-    def click_add_host_btn(self):
-        """Click on Add Host button"""
-        self.find_and_click(ClusterComponentsLocators.create_hosts_btn)
-        self.wait_element_visible(HostCreationLocators.block)
-
-    def get_host_rows(self):
-        """Get all hosts rows"""
-        return self.find_elements(ClusterComponentsLocators.host_row)
-
-    def get_components_rows(self):
-        """Get all components rows"""
-        return self.find_elements(ClusterComponentsLocators.component_row)
-
-    def get_row_info(self, row: WebElement):
-        """Get components row info"""
-        return ComponentsHostRowInfo(
-            name=self.find_child(row, ClusterComponentsLocators.Row.name).text,
-            components=self.find_child(row, ClusterComponentsLocators.Row.number).text,
-        )
-
-    def find_host_row_by_name(self, host_name: str):
-        """Find Host row by name"""
-        for host_row in self.get_host_rows():
-            host_name_element = self.find_child(host_row, ClusterComponentsLocators.Row.name)
-            if host_name_element.text == host_name:
-                return host_row
-        raise AssertionError(f"There are no host with name '{host_name}'")
-
-    def find_component_row_by_name(self, component_name: str):
-        """Find Component row by name"""
-        for component_row in self.get_components_rows():
-            component_name_element = self.find_child(component_row, ClusterComponentsLocators.Row.name)
-            if component_name_element.text == component_name:
-                return component_row
-        raise AssertionError(f"There are no component with name '{component_name}'")
-
-    @allure.step("Click on host row")
-    def click_host(self, host_row: WebElement):
-        """Click on Host row"""
-        self.find_child(host_row, ClusterComponentsLocators.Row.name).click()
-
-    @allure.step("Click on component row")
-    def click_component(self, component_row: WebElement):
-        """Click on Component row"""
-        self.find_child(component_row, ClusterComponentsLocators.Row.name).click()
-
-    @allure.step("Click on row number in component row")
-    def click_number_in_component(self, component_row: WebElement):
-        """Click on Component row number"""
-        self.find_child(component_row, ClusterComponentsLocators.Row.number).click()
-
-    @allure.step("Click on save button")
-    def click_save_btn(self):
-        """Click on Save button"""
-        self.find_and_click(ClusterComponentsLocators.save_btn)
-
-    @allure.step("Click on restore button")
-    def click_restore_btn(self):
-        """Click on Restore button"""
-        self.find_and_click(ClusterComponentsLocators.restore_btn)
-
-    @allure.step("Delete item {item_name} from row")
-    def delete_related_item_in_row_by_name(self, row: WebElement, item_name: str):
-        """Delete related item by button from row"""
-        self.wait_element_visible(ClusterComponentsLocators.Row.relations_row)
-        for item_row in self.find_children(row, ClusterComponentsLocators.Row.relations_row):
-            item_name_element = self.find_child(item_row, ClusterComponentsLocators.Row.RelationsRow.name)
-            if item_name_element.text == item_name:
-                self.find_child(item_row, ClusterComponentsLocators.Row.RelationsRow.delete_btn).click()
-                return
-        raise AssertionError(f"There are no item with name '{item_name}'")
-
-    def check_that_save_btn_disabled(self):
-        """Get Save button available state"""
-        return self.find_element(ClusterComponentsLocators.save_btn).get_attribute("disabled") == "true"
-
 
 class ClusterStatusPage(ClusterPageMixin, StatusPage):
     """Cluster page status menu"""
diff --git a/tests/ui_tests/app/page/cluster_list/page.py b/tests/ui_tests/app/page/cluster_list/page.py
index 7e56cee2d6..678d500686 100644
--- a/tests/ui_tests/app/page/cluster_list/page.py
+++ b/tests/ui_tests/app/page/cluster_list/page.py
@@ -11,25 +11,35 @@
 # limitations under the License.
 
 """Cluster List page PageObjects classes"""
-
 from contextlib import contextmanager
+from typing import Optional
 
 import allure
 from adcm_pytest_plugin.utils import wait_until_step_succeeds
 from selenium.common.exceptions import TimeoutException
 from selenium.webdriver.remote.webdriver import WebElement
 
+from tests.ui_tests.app.page.cluster.locators import (
+    ClusterComponentsLocators,
+)
 from tests.ui_tests.app.page.cluster_list.locators import ClusterListLocators
 from tests.ui_tests.app.page.common.base_page import (
     BasePageObject,
     PageHeader,
     PageFooter,
 )
+from tests.ui_tests.app.page.common.configuration.locators import CommonConfigMenu
+from tests.ui_tests.app.page.common.configuration.page import CommonConfigMenuObj
 from tests.ui_tests.app.page.common.dialogs_locators import (
     ActionDialog,
     DeleteDialog,
 )
+from tests.ui_tests.app.page.common.host_components.page import HostComponentsPage
+from tests.ui_tests.app.page.common.popups.locator import (
+    HostCreationLocators,
+)
 from tests.ui_tests.app.page.common.popups.locator import ListConcernPopupLocators
+from tests.ui_tests.app.page.common.popups.page import HostCreatePopupObj
 from tests.ui_tests.app.page.common.table.page import CommonTableObj
 
 
@@ -40,7 +50,9 @@ def __init__(self, driver, base_url):
         super().__init__(driver, base_url, "/cluster")
         self.header = PageHeader(self.driver, self.base_url)
         self.footer = PageFooter(self.driver, self.base_url)
+        self.config = CommonConfigMenuObj(self.driver, self.base_url)
         self.table = CommonTableObj(self.driver, self.base_url, ClusterListLocators.ClusterTable)
+        self.host_popup = HostCreatePopupObj(self.driver, self.base_url)
 
     @allure.step("Create cluster")
     def create_cluster(self, bundle: str, description: str = None, is_license: bool = False):
@@ -160,13 +172,56 @@ def delete_cluster_by_row(self, row: WebElement):
         self.wait_element_hide(DeleteDialog.body)
 
     @allure.step("Run upgrade {upgrade_name} for cluster from row")
-    def run_upgrade_in_cluster_row(self, row: WebElement, upgrade_name: str):
-        """Run upgrade for cluster from row"""
+    def run_upgrade_in_cluster_row(
+        self,
+        row: WebElement,
+        upgrade_name: str,
+        config: Optional[dict] = None,
+        hc_acl: bool = False,
+        is_new_host: bool = False,
+        disclaimer_text: Optional[str] = None,
+    ):
+        """
+        Run upgrade for cluster from row
+
+        :param row: row with upgrade
+        :param upgrade_name: upgrade action name
+        :param config: config options
+        :param hc_acl: hc_acl options
+        :param is_new_host: condition if new host is needed, new host will be created and added in upgrade popup.
+                            only with hc_acl option
+        :param disclaimer_text: disclaimer text in first popup
+        """
 
         self.find_child(row, self.table.locators.ClusterRow.upgrade).click()
         self.wait_element_visible(self.table.locators.UpgradePopup.block)
         self.find_and_click(self.table.locators.UpgradePopup.button(upgrade_name))
         self.wait_element_visible(ActionDialog.body)
+        if disclaimer_text:
+            assert self.find_element(ActionDialog.text).text == disclaimer_text, "Different text in disclaimer dialog"
+            self.find_and_click(ActionDialog.run)
+            self.wait_element_visible(ActionDialog.body)
+        if config:
+            for key, value in config.items():
+                self.wait_element_visible(CommonConfigMenu.config_row)
+                self.config.type_in_field_with_few_inputs(
+                    row=self.config.get_config_row(display_name=key), values=[value]
+                )
+            if self.is_element_displayed(ActionDialog.next_btn):
+                self.find_and_click(ActionDialog.next_btn)
+        if hc_acl:
+            if is_new_host:
+                components_page = HostComponentsPage(self.driver, self.base_url)
+                components_page.click_add_host_btn()
+                self.host_popup.create_host("Test_host")
+                self.wait_element_hide(HostCreationLocators.block)
+            for comp_row in self.find_elements(ClusterComponentsLocators.component_row):
+                comp_row_name = self.find_child(comp_row, ClusterComponentsLocators.Row.name)
+                self.wait_element_visible(comp_row_name)
+                comp_row_name.click()
+            self.find_child(
+                self.find_elements(ClusterComponentsLocators.host_row)[0], ClusterComponentsLocators.Row.name
+            ).click()
         self.find_and_click(ActionDialog.run)
         self.wait_element_hide(ActionDialog.body)
 
diff --git a/tests/ui_tests/app/page/common/base_page.py b/tests/ui_tests/app/page/common/base_page.py
index 5e68561f12..8227f20829 100644
--- a/tests/ui_tests/app/page/common/base_page.py
+++ b/tests/ui_tests/app/page/common/base_page.py
@@ -591,7 +591,7 @@ def _wait_job():
                 int(self.get_success_job_amount_from_header()) == expected_job_amount
             ), f"Should be {expected_job_amount} tasks in popup header"
 
-        wait_until_step_succeeds(_wait_job, period=1, timeout=70)
+        wait_until_step_succeeds(_wait_job, period=1, timeout=90)
 
     @allure.step('Open profile using account popup in header')
     def open_profile(self):
diff --git a/tests/ui_tests/app/page/common/dialogs_locators.py b/tests/ui_tests/app/page/common/dialogs_locators.py
index 0a3e859fbf..aa4cf2827b 100644
--- a/tests/ui_tests/app/page/common/dialogs_locators.py
+++ b/tests/ui_tests/app/page/common/dialogs_locators.py
@@ -28,4 +28,6 @@ class ActionDialog:
     """Action Dialog class"""
 
     body = Locator(By.CSS_SELECTOR, "mat-dialog-container", "Dialog with choices")
+    text = Locator(By.CSS_SELECTOR, "app-dialog mat-dialog-content", "Dialog content")
+    next_btn = Locator(By.CSS_SELECTOR, ".mat-stepper-next", "Next button in action dialog")
     run = Locator(By.CSS_SELECTOR, "app-dialog button[color='accent']", "Run button in action dialog")
diff --git a/tests/ui_tests/app/page/common/host_components/locators.py b/tests/ui_tests/app/page/common/host_components/locators.py
new file mode 100644
index 0000000000..3dd339b6d8
--- /dev/null
+++ b/tests/ui_tests/app/page/common/host_components/locators.py
@@ -0,0 +1,53 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Cluster List page locators"""
+
+from selenium.webdriver.common.by import By
+
+from tests.ui_tests.app.helpers.locator import Locator
+
+
+class HostComponentsLocators:
+    """Common host components page locators"""
+
+    restore_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Restore')]]", "Restore button")
+    save_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Save')]]", "Save button")
+
+    components_title = Locator(By.XPATH, "//h3[./span[contains(text(), 'Components')]]", "Title for Components block")
+    service_page_link = Locator(By.CSS_SELECTOR, "mat-card-content a[href*='service']", "Link to service page")
+
+    hosts_title = Locator(By.XPATH, "//h3[./span[contains(text(), 'Hosts')]]", "Title for Hosts block")
+    hosts_page_link = Locator(By.CSS_SELECTOR, "mat-card-content a[href*='host']", "Link to hosts page")
+    create_hosts_btn = Locator(
+        By.CSS_SELECTOR, "app-service-host button[adcm_test='create-btn']", "Create hosts button"
+    )
+
+    host_row = Locator(By.XPATH, "//div[./h3/span[contains(text(), 'Host')]]//app-much-2-many", "Host row")
+    component_row = Locator(
+        By.XPATH,
+        "//div[./h3/span[contains(text(), 'Components')]]//app-much-2-many",
+        "Component row",
+    )
+
+    class Row:
+        """Components page row elements locators"""
+
+        name = Locator(By.XPATH, ".//button[@mat-button]/span/span[not(contains(@class, 'warn'))]", "Item name")
+        number = Locator(By.CSS_SELECTOR, "button[mat-raised-button] span:first-of-type", "Amount of links")
+        relations_row = Locator(By.CSS_SELECTOR, "div[class*='relations-list']", "Row with relations")
+
+        class RelationsRow:
+            """Components page relations row elements locators"""
+
+            name = Locator(By.CSS_SELECTOR, "div>span", "Related item name")
+            delete_btn = Locator(By.CSS_SELECTOR, "button", "Delete item button")
diff --git a/tests/ui_tests/app/page/common/host_components/page.py b/tests/ui_tests/app/page/common/host_components/page.py
new file mode 100644
index 0000000000..2f78c5c146
--- /dev/null
+++ b/tests/ui_tests/app/page/common/host_components/page.py
@@ -0,0 +1,120 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Common Host Component classes"""
+
+from dataclasses import dataclass
+
+import allure
+from selenium.webdriver.remote.webdriver import WebElement
+
+from tests.ui_tests.app.page.common.base_page import (
+    BasePageObject,
+)
+from tests.ui_tests.app.page.common.host_components.locators import HostComponentsLocators
+from tests.ui_tests.app.page.common.popups.locator import HostCreationLocators
+
+
+@dataclass
+class ComponentsHostRowInfo:
+    """Information from host row about host on Components page"""
+
+    name: str
+    components: str
+
+
+class HostComponentsPage(BasePageObject):
+    """HostComponentsPage page components menu"""
+
+    def click_service_page_link(self):
+        """Click on Service page link"""
+        self.find_and_click(HostComponentsLocators.service_page_link)
+
+    def click_hosts_page_link(self):
+        """Click on Hosts page link"""
+        self.find_and_click(HostComponentsLocators.hosts_page_link)
+
+    def click_add_host_btn(self):
+        """Click on Add Host button"""
+        self.find_and_click(HostComponentsLocators.create_hosts_btn)
+        self.wait_element_visible(HostCreationLocators.block)
+
+    def get_host_rows(self):
+        """Get all hosts rows"""
+        return self.find_elements(HostComponentsLocators.host_row)
+
+    def get_components_rows(self):
+        """Get all components rows"""
+        return self.find_elements(HostComponentsLocators.component_row)
+
+    def get_row_info(self, row: WebElement):
+        """Get components row info"""
+        return ComponentsHostRowInfo(
+            name=self.find_child(row, HostComponentsLocators.Row.name).text,
+            components=self.find_child(row, HostComponentsLocators.Row.number).text,
+        )
+
+    def find_host_row_by_name(self, host_name: str):
+        """Find Host row by name"""
+        for host_row in self.get_host_rows():
+            host_name_element = self.find_child(host_row, HostComponentsLocators.Row.name)
+            if host_name_element.text == host_name:
+                return host_row
+        raise AssertionError(f"There are no host with name '{host_name}'")
+
+    def find_component_row_by_name(self, component_name: str):
+        """Find Component row by name"""
+        for component_row in self.get_components_rows():
+            component_name_element = self.find_child(component_row, HostComponentsLocators.Row.name)
+            if component_name_element.text == component_name:
+                return component_row
+        raise AssertionError(f"There are no component with name '{component_name}'")
+
+    @allure.step("Click on host row")
+    def click_host(self, host_row: WebElement):
+        """Click on Host row"""
+        self.find_child(host_row, HostComponentsLocators.Row.name).click()
+
+    @allure.step("Click on component row")
+    def click_component(self, component_row: WebElement):
+        """Click on Component row"""
+        self.find_child(component_row, HostComponentsLocators.Row.name).click()
+
+    @allure.step("Click on row number in component row")
+    def click_number_in_component(self, component_row: WebElement):
+        """Click on Component row number"""
+        self.find_child(component_row, HostComponentsLocators.Row.number).click()
+
+    @allure.step("Click on save button")
+    def click_save_btn(self):
+        """Click on Save button"""
+        self.find_and_click(HostComponentsLocators.save_btn)
+
+    @allure.step("Click on restore button")
+    def click_restore_btn(self):
+        """Click on Restore button"""
+        self.find_and_click(HostComponentsLocators.restore_btn)
+
+    @allure.step("Delete item {item_name} from row")
+    def delete_related_item_in_row_by_name(self, row: WebElement, item_name: str):
+        """Delete related item by button from row"""
+        self.wait_element_visible(HostComponentsLocators.Row.relations_row)
+        for item_row in self.find_children(row, HostComponentsLocators.Row.relations_row):
+            item_name_element = self.find_child(item_row, HostComponentsLocators.Row.RelationsRow.name)
+            if item_name_element.text == item_name:
+                self.find_child(item_row, HostComponentsLocators.Row.RelationsRow.delete_btn).click()
+                return
+        raise AssertionError(f"There are no item with name '{item_name}'")
+
+    def check_that_save_btn_disabled(self):
+        """Get Save button available state"""
+        return self.find_element(HostComponentsLocators.save_btn).get_attribute("disabled") == "true"
diff --git a/tests/ui_tests/app/page/common/popups/locator.py b/tests/ui_tests/app/page/common/popups/locator.py
index 2e42163bf3..7d0e1052ae 100644
--- a/tests/ui_tests/app/page/common/popups/locator.py
+++ b/tests/ui_tests/app/page/common/popups/locator.py
@@ -30,6 +30,7 @@ class CommonPopupLocators:
     hide_btn_by_text = TemplateLocator(
         By.XPATH, "//div[./div/div[text()='{}']]/button", "Hide pop up button with text {}"
     )
+    verbose_chbx = Locator(By.XPATH, "//mat-checkbox[.//span[text()='Verbose']]", "Verbose checkbox")
 
 
 class HostCreationLocators:
@@ -37,8 +38,8 @@ class HostCreationLocators:
 
     block = Locator(By.CSS_SELECTOR, "mat-dialog-container", "Host creation popup block")
     fqdn_input = Locator(By.CSS_SELECTOR, "input[data-placeholder='Fully qualified domain name']", "Host FQDN input")
-    create_btn = Locator(By.XPATH, "//button[./span[contains(text(), 'Create')]]", "Create button")
-    cancel_btn = Locator(By.XPATH, "//button[./span[text()='Cancel']]", "Cancel button")
+    create_btn = Locator(By.XPATH, "//app-add-controls//button[./span[contains(text(), 'Create')]]", "Create button")
+    cancel_btn = Locator(By.XPATH, "//app-add-controls//button[./span[text()='Cancel']]", "Cancel button")
 
     class Provider:
         """Provider creation and selection section"""
diff --git a/tests/ui_tests/test_cluster_list_page.py b/tests/ui_tests/test_cluster_list_page.py
index b6ffb41a90..6d64bba787 100644
--- a/tests/ui_tests/test_cluster_list_page.py
+++ b/tests/ui_tests/test_cluster_list_page.py
@@ -49,7 +49,6 @@
     ClusterHostPage,
     ClusterServicesPage,
     ClusterComponentsPage,
-    ComponentsHostRowInfo,
     ClusterStatusPage,
     ClusterGroupConfigConfig,
     ClusterGroupConfigHosts,
@@ -57,6 +56,7 @@
 from tests.ui_tests.app.page.cluster_list.page import ClusterListPage
 from tests.ui_tests.app.page.common.configuration.page import CONFIG_ITEMS
 from tests.ui_tests.app.page.common.group_config_list.page import GroupConfigRowInfo
+from tests.ui_tests.app.page.common.host_components.page import ComponentsHostRowInfo
 from tests.ui_tests.app.page.common.import_page.page import ImportItemInfo
 from tests.ui_tests.app.page.common.status.page import (
     SUCCESS_COLOR,
@@ -91,6 +91,7 @@
 BUNDLE_ENTERPRISE = "cluster_enterprise"
 BUNDLE_IMPORT = "cluster_to_import"
 BUNDLE_UPGRADE = "upgradable_cluster"
+BUNDLE_UPGRADE_V2 = "upgradable_cluster_v2"
 BUNDLE_REQUIRED_FIELDS = "cluster_and_service_with_required_string"
 BUNDLE_DEFAULT_FIELDS = "cluster_and_service_with_default_string"
 BUNDLE_WITH_SERVICES = "cluster_with_services"
@@ -104,10 +105,11 @@
 BUNDLE_WITH_DESCRIPTION_FIELDS = "cluster_with_all_config_params"
 BUNDLE_WITH_REQUIRED_IMPORT = "cluster_required_import"
 BUNDLE_WITH_REQUIRED_COMPONENT = "cluster_required_hostcomponent"
+DISCLAIMER_TEXT = "Are you really want to click me?"
 
 
 # pylint: disable=redefined-outer-name,unused-argument,too-many-lines,too-many-public-methods
-# pylint: disable=too-many-arguments,too-many-boolean-expressions,too-many-branches,too-many-nested-blocks
+# pylint: disable=too-many-arguments,too-many-boolean-expressions,too-many-branches,too-many-nested-blocks,too-many-locals
 
 pytestmark = pytest.mark.usefixtures("login_to_adcm_over_api")
 
@@ -391,6 +393,64 @@ def test_run_upgrade_on_cluster_list_page(self, sdk_client_fs, app_fs):
                 cluster_page.get_cluster_state_from_row(row) == params["state"]
             ), f"Cluster state should be {params['state']}"
 
+    @pytest.mark.parametrize(
+        ("upgrade_name", "config", "hc_acl", "is_new_host", "disclaimer_text"),
+        [
+            ("simple_upgrade", None, False, False, False),
+            ("upgrade_with_hc_acl", None, True, False, False),
+            ("upgrade_with_config", {"somestring2": "test"}, False, False, False),
+            ("upgrade_with_config_and_hc_acl", {"somestring2": "test"}, True, False, False),
+            ("upgrade_with_hc_acl_and_disclaimer", None, True, False, DISCLAIMER_TEXT),
+            ("upgrade_with_config_and_disclaimer", {"somestring2": "test"}, False, False, DISCLAIMER_TEXT),
+            ("upgrade_with_config_and_hc_acl_and_disclaimer", {"somestring2": "test"}, True, False, DISCLAIMER_TEXT),
+            # next steps are skipped until https://tracker.yandex.ru/ADCM-3001
+            # ("upgrade_with_hc_acl", None, True, True, False),
+            # ("upgrade_with_config_and_hc_acl", {"somestring2": "test"}, True, True, False),
+            # ("upgrade_with_hc_acl_and_disclaimer", None, True, True, DISCLAIMER_TEXT),
+            # ("upgrade_with_config_and_hc_acl_and_disclaimer", {"somestring2": "test"}, True, True, DISCLAIMER_TEXT),
+        ],
+    )
+    def test_run_upgrade_v2_on_cluster_list_page(
+        self,
+        upload_and_create_provider,
+        sdk_client_fs,
+        app_fs,
+        upgrade_name,
+        config,
+        hc_acl,
+        is_new_host,
+        disclaimer_text,
+    ):
+        """Test run upgrade new version from the /cluster page"""
+        params = {
+            "state": "upgraded",
+        }
+        with allure.step("Upload main cluster bundle"):
+            bundle = cluster_bundle(sdk_client_fs, BUNDLE_COMMUNITY)
+            cluster = bundle.cluster_create(name=CLUSTER_NAME)
+            if not is_new_host:
+                host = upload_and_create_provider.host_create(HOST_NAME)
+                cluster.host_add(host)
+        with allure.step("Upload cluster bundle to upgrade"):
+            cluster_bundle(sdk_client_fs, BUNDLE_UPGRADE_V2)
+        cluster_page = ClusterListPage(app_fs.driver, app_fs.adcm.url).open()
+        row_with_upgrade = cluster_page.get_row_by_cluster_name("Test cluster")
+        cluster_page.run_upgrade_in_cluster_row(
+            row=row_with_upgrade,
+            upgrade_name=upgrade_name,
+            config=config,
+            hc_acl=hc_acl,
+            is_new_host=is_new_host,
+            disclaimer_text=disclaimer_text,
+        )
+        with allure.step("Check that cluster has been upgraded"):
+            cluster_page = ClusterListPage(app_fs.driver, app_fs.adcm.url).open()
+            cluster_page.header.wait_success_job_amount_from_header(1)
+            row = cluster_page.get_row_by_cluster_name("Test cluster")
+            assert (
+                cluster_page.get_cluster_state_from_row(row) == params["state"]
+            ), f"Cluster state should be {params['state']}"
+
 
 class TestClusterMainPage:
     """Tests for the /cluster/{}/main page"""
diff --git a/tests/ui_tests/test_cluster_list_page_data/upgradable_cluster_v2/config.yaml b/tests/ui_tests/test_cluster_list_page_data/upgradable_cluster_v2/config.yaml
new file mode 100644
index 0000000000..23382e4a9d
--- /dev/null
+++ b/tests/ui_tests/test_cluster_list_page_data/upgradable_cluster_v2/config.yaml
@@ -0,0 +1,199 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- type: cluster
+  name: test_cluster
+  edition: community
+  version: 1.6
+
+  upgrade:
+    - &upgrade
+      name: simple_upgrade
+      versions:
+        min: 0.1
+        max: 1.5
+      scripts:
+        - &action
+          name: first_action
+          script: ./succeed.yaml
+          script_type: ansible
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+      states:
+        available: any
+        on_success: upgraded
+
+    - <<: *upgrade
+      name: upgrade_with_hc_acl
+      versions:
+        min: 0.1
+        max: 1.5
+      scripts:
+        - <<: *action
+          name: first_action
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+      hc_acl:
+        - service: test_service
+          component: test_component
+          action: add
+
+    - <<: *upgrade
+      name: upgrade_with_config
+      versions:
+        min: 0.1
+        max: 1.5
+      scripts:
+        - <<: *action
+          name: first_action
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+
+      config:
+        - name: somestring2
+          type: string
+          required: false
+        - name: someint2
+          type: integer
+          default: 122
+
+    - <<: *upgrade
+      name: upgrade_with_config_and_hc_acl
+      versions:
+        min: 0.1
+        max: 1.5
+      scripts:
+        - <<: *action
+          name: first_action
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+      hc_acl:
+        - service: test_service
+          component: test_component
+          action: add
+      config:
+        - name: somestring2
+          type: string
+          required: false
+        - name: someint2
+          type: integer
+          default: 122
+
+    - <<: *upgrade
+      name: upgrade_with_hc_acl_and_disclaimer
+      versions:
+        min: 0.1
+        max: 1.5
+      ui_options:
+        disclaimer: "Are you really want to click me?"
+      scripts:
+        - <<: *action
+          name: first_action
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+      hc_acl:
+        - service: test_service
+          component: test_component
+          action: add
+
+    - <<: *upgrade
+      name: upgrade_with_config_and_disclaimer
+      versions:
+        min: 0.1
+        max: 1.5
+      ui_options:
+        disclaimer: "Are you really want to click me?"
+      scripts:
+        - <<: *action
+          name: first_action
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+
+      config:
+        - name: somestring2
+          type: string
+          required: false
+        - name: someint2
+          type: integer
+          default: 122
+
+    - <<: *upgrade
+      name: upgrade_with_config_and_hc_acl_and_disclaimer
+      versions:
+        min: 0.1
+        max: 1.5
+      ui_options:
+        disclaimer: "Are you really want to click me?"
+      scripts:
+        - <<: *action
+          name: first_action
+        - name: switch action
+          script: bundle_switch
+          script_type: internal
+        - <<: *action
+          name: first action after switch
+      hc_acl:
+        - service: test_service
+          component: test_component
+          action: add
+      config:
+        - name: somestring2
+          type: string
+          required: false
+        - name: someint2
+          type: integer
+          default: 122
+
+  config: &config
+    - name: somestring
+      type: string
+      required: false
+    - name: someint
+      type: integer
+      default: 12
+    - name: somethingelse
+      type: map
+      required: false
+    - name: moar
+      type: list
+      default:
+        - high
+        - noon
+
+
+- type: service
+  version: 2.3
+  name: test_service
+
+  config: *config
+
+  components:
+    test_component:
+      config: *config
diff --git a/tests/ui_tests/test_cluster_list_page_data/upgradable_cluster_v2/succeed.yaml b/tests/ui_tests/test_cluster_list_page_data/upgradable_cluster_v2/succeed.yaml
new file mode 100644
index 0000000000..3f4b78d31d
--- /dev/null
+++ b/tests/ui_tests/test_cluster_list_page_data/upgradable_cluster_v2/succeed.yaml
@@ -0,0 +1,22 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- name: Succeed action
+  hosts: localhost
+  connection: local
+  gather_facts: no
+
+  tasks:
+    - name: dummy action
+      debug:
+        msg: "Some message"
+