diff --git a/components/automate-gateway/gateway/services.go b/components/automate-gateway/gateway/services.go index 84b237504c8..d24634000ba 100644 --- a/components/automate-gateway/gateway/services.go +++ b/components/automate-gateway/gateway/services.go @@ -976,7 +976,7 @@ func (s *Server) UploadZipFile(w http.ResponseWriter, r *http.Request) { var cType, fileName, serverId string var content bytes.Buffer file, metaData, err := r.FormFile("file") - serverId = r.FormValue("serverId") + serverId = r.FormValue("server_id") if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/components/automate-ui/src/app/entities/servers/server.actions.ts b/components/automate-ui/src/app/entities/servers/server.actions.ts index 29e41a178d2..f0ddbc3dafb 100644 --- a/components/automate-ui/src/app/entities/servers/server.actions.ts +++ b/components/automate-ui/src/app/entities/servers/server.actions.ts @@ -1,7 +1,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Action } from '@ngrx/store'; -import { Server , User, WebUIKey } from './server.model'; +import { MigrationStatus, Server , User, WebUIKey } from './server.model'; import { ValidateWebUIKeyResponse } from './server.requests'; export enum ServerActionTypes { @@ -29,7 +29,10 @@ export enum ServerActionTypes { VALIDATE_WEB_UI_KEY = 'SERVER::VALIDATE_WEB_UI_KEY', VALIDATE_WEB_UI_KEY_SUCCESS = 'SERVER::VALIDATE_WEB_UI_KEY::SUCCESS', VALIDATE_WEB_UI_KEY_SUCCESS_NOT= 'SERVER::VALIDATE_WEB_UI_KEY::SUCCESS_NOT', - VALIDATE_WEB_UI_KEY_FAILURE = 'SERVER::VALIDATE_WEB_UI_KEY::FAILURE' + VALIDATE_WEB_UI_KEY_FAILURE = 'SERVER::VALIDATE_WEB_UI_KEY::FAILURE', + GET_MIGRATION_STATUS = 'SERVER::GET_MIGRATION_STATUS', + GET_MIGRATION_STATUS_SUCCESS = 'SERVER::GET_MIGRATION_STATUS::SUCCESS', + GET_MIGRATION_STATUS_FAILURE = 'SERVER::GET_MIGRATION_STATUS::FAILURE' } @@ -199,6 +202,24 @@ export class ValidateWebUIKeyFailure implements Action { constructor(public payload: HttpErrorResponse) { } } +export class GetMigrationStatus implements Action { + readonly type = ServerActionTypes.GET_MIGRATION_STATUS; + + constructor(public payload: string) { } +} + +export class GetMigrationStatusSuccess implements Action { + readonly type = ServerActionTypes.GET_MIGRATION_STATUS_SUCCESS; + + constructor(public payload: MigrationStatus) { } +} + +export class GetMigrationStatusFailure implements Action { + readonly type = ServerActionTypes.GET_MIGRATION_STATUS_FAILURE; + + constructor(public payload: HttpErrorResponse) { } +} + export type ServerActions = | GetServers | GetServersSuccess @@ -224,4 +245,8 @@ export type ServerActions = | ValidateWebUIKey | ValidateWebUIKeySuccess | ValidateWebUIKeySuccessNot - | ValidateWebUIKeyFailure; + | ValidateWebUIKeyFailure + | GetMigrationStatus + | GetMigrationStatusSuccess + | GetMigrationStatusFailure; + diff --git a/components/automate-ui/src/app/entities/servers/server.effects.ts b/components/automate-ui/src/app/entities/servers/server.effects.ts index 920fbfa0719..8624f70fbcb 100644 --- a/components/automate-ui/src/app/entities/servers/server.effects.ts +++ b/components/automate-ui/src/app/entities/servers/server.effects.ts @@ -36,7 +36,10 @@ import { ValidateWebUIKey, ValidateWebUIKeySuccess, ValidateWebUIKeyFailure, - ValidateWebUIKeySuccessNot + ValidateWebUIKeySuccessNot, + GetMigrationStatus, + GetMigrationStatusSuccess, + GetMigrationStatusFailure } from './server.actions'; import { @@ -251,4 +254,22 @@ export class ServerEffects { message: `Could not validated Web UI Key ${msg || payload.error}.` }); }))); + + GetMigrationStatus$ = createEffect(() => this.actions$.pipe( + ofType(ServerActionTypes.GET_MIGRATION_STATUS), + mergeMap(({ payload }: GetMigrationStatus) => + this.requests.getMigrationStatus(payload).pipe( + map((resp) => new GetMigrationStatusSuccess(resp)), + catchError((error: HttpErrorResponse) => + observableOf(new GetMigrationStatusFailure(error))))))); + + GetMigrationStatusFailure$ = createEffect(() => this.actions$.pipe( + ofType(ServerActionTypes.GET_MIGRATION_STATUS_FAILURE), + map(({ payload }: GetMigrationStatusFailure) => { + const msg = payload.error.error; + return new CreateNotification({ + type: Type.error, + message: `Could not update Migration status: ${msg || payload.error}.` + }); + }))); } diff --git a/components/automate-ui/src/app/entities/servers/server.model.ts b/components/automate-ui/src/app/entities/servers/server.model.ts index 106b725a289..08ce960b5b7 100644 --- a/components/automate-ui/src/app/entities/servers/server.model.ts +++ b/components/automate-ui/src/app/entities/servers/server.model.ts @@ -5,6 +5,9 @@ export interface Server { ip_address: string; orgs_count?: number; webui_key?: string; + migration_id?: string; + migration_type?: string; + migration_status?: string; } export interface User { @@ -21,3 +24,9 @@ export interface WebUIKey { id: string; webui_key: string; } + +export interface MigrationStatus { + migration_id: string; + migration_type: string; + migration_status: string; +} diff --git a/components/automate-ui/src/app/entities/servers/server.reducer.ts b/components/automate-ui/src/app/entities/servers/server.reducer.ts index 2ddc07f713c..8a93f226d77 100644 --- a/components/automate-ui/src/app/entities/servers/server.reducer.ts +++ b/components/automate-ui/src/app/entities/servers/server.reducer.ts @@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { pipe, set, unset } from 'lodash/fp'; import { EntityStatus } from 'app/entities/entities'; import { ServerActionTypes, ServerActions } from './server.actions'; -import { Server, User } from './server.model'; +import { MigrationStatus, Server, User } from './server.model'; import { ValidateWebUIKeyResponse } from './server.requests'; export interface ServerEntityState extends EntityState { @@ -18,6 +18,8 @@ export interface ServerEntityState extends EntityState { updateWebUIKeyStatus: EntityStatus; getvalidateWebUIKeyStatus: ValidateWebUIKeyResponse; validateWebUIKeyStatus: EntityStatus; + getMigrationStatus: MigrationStatus; + migrationStatus: EntityStatus; } const GET_ALL_STATUS = 'getAllStatus'; @@ -29,6 +31,7 @@ const DELETE_STATUS = 'deleteStatus'; const GET_USERS_STATUS = 'getUsersStatus'; const UPDATE_WEB_UI_KEY_STATUS = 'updateWebUIKeyStatus'; const VALIDATE_WEB_UI_KEY_STATUS = 'validateWebUIKeyStatus'; +const GET_MIGRATION_STATUS = 'migrationStatus'; export const serverEntityAdapter: EntityAdapter = createEntityAdapter(); @@ -44,7 +47,9 @@ export const ServerEntityInitialState: ServerEntityState = getUsersStatus: EntityStatus.notLoaded, updateWebUIKeyStatus: EntityStatus.notLoaded, getvalidateWebUIKeyStatus: null, - validateWebUIKeyStatus: EntityStatus.notLoaded + validateWebUIKeyStatus: EntityStatus.notLoaded, + getMigrationStatus: null, + migrationStatus: EntityStatus.notLoaded }); export function serverEntityReducer( @@ -170,6 +175,19 @@ export function serverEntityReducer( case ServerActionTypes.VALIDATE_WEB_UI_KEY_FAILURE: return set(VALIDATE_WEB_UI_KEY_STATUS, EntityStatus.loadingFailure, state); + + case ServerActionTypes.GET_MIGRATION_STATUS: + return set(GET_MIGRATION_STATUS, EntityStatus.loading, state); + + case ServerActionTypes.GET_MIGRATION_STATUS_SUCCESS: + return pipe( + set(GET_MIGRATION_STATUS, EntityStatus.loadingSuccess), + set('getMigrationStatus', action.payload || {}) + )(state) as ServerEntityState; + + case ServerActionTypes.GET_MIGRATION_STATUS_FAILURE: + return set(GET_MIGRATION_STATUS, EntityStatus.loadingFailure, state); + } return state; diff --git a/components/automate-ui/src/app/entities/servers/server.requests.ts b/components/automate-ui/src/app/entities/servers/server.requests.ts index 8acaf30c442..c7c7259c1d6 100644 --- a/components/automate-ui/src/app/entities/servers/server.requests.ts +++ b/components/automate-ui/src/app/entities/servers/server.requests.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import { mapKeys, snakeCase } from 'lodash/fp'; import { environment as env } from 'environments/environment'; -import { Server, User, WebUIKey } from './server.model'; +import { MigrationStatus, Server, User, WebUIKey } from './server.model'; import { CreateServerPayload, ServerSuccessPayload } from './server.actions'; export interface ServersResponse { @@ -64,4 +64,8 @@ export class ServerRequests { return this.http.post (`${env.infra_proxy_url}/servers/validate`, payload); } + + public getMigrationStatus(migration_id: string): Observable { + return this.http.get(`${env.infra_proxy_url}/servers/migrations/status/${migration_id}`); + } } diff --git a/components/automate-ui/src/app/entities/servers/server.selectors.ts b/components/automate-ui/src/app/entities/servers/server.selectors.ts index 88b20b1b232..3bef9c7553b 100644 --- a/components/automate-ui/src/app/entities/servers/server.selectors.ts +++ b/components/automate-ui/src/app/entities/servers/server.selectors.ts @@ -71,3 +71,13 @@ export const validateWebUIKeyStatus = createSelector( serverState, (state) => state.validateWebUIKeyStatus ); + +export const getMigrationStatus = createSelector( + serverState, + (state) => state.getMigrationStatus +); + +export const migrationStatus = createSelector( + serverState, + (state) => state.migrationStatus +); diff --git a/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.html b/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.html index 63566d19671..3bb6e2f27b0 100644 --- a/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.html +++ b/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.html @@ -10,16 +10,20 @@
+

CHEF SERVER INFORMATION

- {{ server?.name }}
  • FQDN - {{ server?.fqdn }} + {{ server?.fqdn }}
  • IP Address - {{ server?.ip_address }} + {{ server?.ip_address === '' ? '--' : server?.ip_address }} +
  • +
  • + +
@@ -27,20 +31,66 @@
  • Web UI Key - Validating... - Valid - warning - Invalid - - Update + + Validating... + Valid + warning + Invalid + + Update +
  • - Org & User last synced - Thu, Feb 04, 12:00:00 UTC + last sync date + Thu, Feb 04, 12:00:00 UTC +
  • +
  • + Last Migration Status + + + info + Completed + + + + + + warning + Migration failed, migration step not completed! + + info + Failed + + + + Sync In Progress + + + + + + {{stepsCompleted}} Steps Completed + Click to Preview + + + Loading.... + --
diff --git a/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.scss b/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.scss index 0dcaac0ba2f..a5ccee60efe 100644 --- a/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.scss +++ b/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.scss @@ -18,15 +18,35 @@ chef-page-header { padding: 0 1em; &.first { - width: 33%; + width: 39%; + + .heading { + display: inline-block; + font-weight: bold; + width: 30%; + } + + .server-entity-value { + width: 70%; + } } &.middle { - width: 40%; + width: 46%; + + .heading { + display: inline-block; + font-weight: bold; + width: 30%; + } + + .server-entity-value { + width: 70%; + } } &.last { - width: 25%; + width: 14%; } .meta-head { @@ -44,19 +64,13 @@ chef-page-header { li { display: flex; margin-bottom: 15px; - height: 50px; + height: 20px; span { font-size: 14px; line-height: 21px; letter-spacing: 0.2px; - &.heading { - display: inline-block; - font-weight: bold; - width: 50%; - } - &.webUIKeyStatus { text-transform: uppercase; } @@ -72,16 +86,16 @@ chef-page-header { } img { - margin: 2.5px 5.5px 0px 0px; + margin: 2.5px 1.5px -2px 0px; width: 14px; - height: 13.5px; + height: 12.5px; } } } .sync-button { float: right; - margin-bottom: 0; + margin-bottom: 62px; .img-inline { width: 17px; @@ -231,3 +245,64 @@ img { } } +.circular-chart { + display: inline-block; + width: 30px; + justify-content: space-around ; +} + +.circle-bg { + fill: none; + stroke: #eee; + stroke-width: 3.8; +} + +.circle { + fill: none; + stroke-width: 2.8; + stroke-linecap: round; + animation: progress 1s ease-out forwards; +} + +@keyframes progress { + 0% { + stroke-dasharray: 0 100; + } +} + +.circular-chart.blue .circle { + stroke: #3864F1; +} + +.svg-block { + display: flex; + height: 30px; + align-items: center; + gap: 10px; +} + +.status-icon-completed { + position: absolute; + margin-top: 0px; +} + +.status-icon-failed { + position: absolute; + margin-top: 0px; +} + +.preview-link { + cursor: pointer; +} + +.tooltip { + --tooltip-color: #F9DBEA; + left: 1395.05px; + top: 280.766px; + font-size: 10px; + color: #ba1e6a; +} + +.chef-server-info-text { + margin-left: 14px; +} diff --git a/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.ts b/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.ts index 3f02179bf48..4c16a437161 100644 --- a/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.ts +++ b/components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MatOptionSelectionChange } from '@angular/material/core/option'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { Subject, combineLatest } from 'rxjs'; +import { Subject, combineLatest, interval } from 'rxjs'; import { filter, pluck, takeUntil } from 'rxjs/operators'; import { identity, isNil } from 'lodash/fp'; import { HttpStatus } from 'app/types/types'; @@ -20,11 +20,14 @@ import { getUsersStatus, updateWebUIKey, validateWebUIKeyStatus, - getValidateWebUIKeyStatus + getValidateWebUIKeyStatus, + migrationStatus, + getMigrationStatus } from 'app/entities/servers/server.selectors'; -import { Server, WebUIKey } from 'app/entities/servers/server.model'; +import { MigrationStatus, Server, WebUIKey } from 'app/entities/servers/server.model'; import { + GetMigrationStatus, GetServer, UpdateServer, UpdateWebUIKey, @@ -91,6 +94,32 @@ export class ChefServerDetailsComponent implements OnInit, OnDestroy { public uploadZipForm: FormGroup; public isUploaded = false; public migrationID: string; + public migrationStatus: MigrationStatus; + public migrationStatusPercentage: number; + public stepsCompleted: string; + public totalMigrationSteps = 13; + public migrationStepValue: number; + public migrationfailed = false; + public migrationCompleted = false; + public migrationInProgress = false; + public migrationLoading = true; + public migrationStarted = false; + public migrationIsInPreview = false; + public migrationSteps: Record = { + 1: 'Migration started', + 2: 'Upload of zip file', + 3: 'Unzip of zip file', + 4: 'Parsing of orgs file', + 5: 'Parsing of users file', + 6: 'Parsing of user association file', + 7: 'Parsing of user permissions file', + 8: 'Creating Preview', + 9: 'Migration of organization', + 10: 'Migration of users', + 11: 'Association of users to orgs', + 12: 'Migrating user permissions', + 13: 'Migration Completed' + }; @ViewChild('upload', { static: false }) upload: SyncOrgUsersSliderComponent; @@ -199,6 +228,11 @@ export class ChefServerDetailsComponent implements OnInit, OnDestroy { this.orgsListLoading = false; this.closeCreateModal(); this.isServerLoaded = true; + this.migrationLoading = false; + if (this.server.migration_id !== '') { + this.migrationStarted = true; + this.getMigrationStatus(this.server.migration_id); + } }); combineLatest([ @@ -282,11 +316,17 @@ export class ChefServerDetailsComponent implements OnInit, OnDestroy { } }); - setTimeout(() => { + setTimeout(() => { if (this.isServerLoaded) { this.validateWebUIKey(this.server); } }, 1000); + + interval(50000).subscribe(() => { + if (this.migrationStarted) { + this.getMigrationStatus(this.server.migration_id); + } + }); } ngOnDestroy(): void { @@ -363,6 +403,52 @@ export class ChefServerDetailsComponent implements OnInit, OnDestroy { }); } + // get migration status + private getMigrationStatus(migration_id: string): void { + this.store.dispatch(new GetMigrationStatus(migration_id)); + combineLatest([ + this.store.select(migrationStatus), + this.store.select(getMigrationStatus) + ]).pipe(takeUntil(this.isDestroyed)) + .subscribe(([migrationSt, getMigrationState]) => { + if (migrationSt === EntityStatus.loadingSuccess && !isNil(getMigrationState)) { + this.migrationStatus = getMigrationState; + const migration_type = this.migrationStatus.migration_type; + const migration_status = this.migrationStatus.migration_status; + if (migration_status === 'Completed' ) { + this.migrationStepValue = this.getKeyByValue(this.migrationSteps, migration_type); + this.migrationStatusPercentage = + Number((this.migrationStepValue / this.totalMigrationSteps) * 100); + this.migrationInProgress = true; + this.migrationLoading = false; + if (this.migrationStatusPercentage.toFixed(0) === '100') { + this.migrationCompleted = true; + this.migrationInProgress = false; + } + this.stepsCompleted = this.migrationStepValue.toFixed(0) + '/' + '13'; + if (migration_type === 'Creating Preview') { + this.migrationIsInPreview = true; + } + } else { + this.migrationfailed = true; + } + } + }); + } + + public getKeyByValue(object: Record, value: string) { + return Number(Object.keys(object).find(key => + object[key] === value)); + } + + public currentMigrationProcess() { + return `${this.migrationStatusPercentage.toFixed(0)}, 100`; + } + + public currentMigrationPercent() { + return this.migrationStatusPercentage.toFixed(0); + } + saveServer(): void { this.saveSuccessful = false; this.saveInProgress = true; @@ -400,5 +486,7 @@ export class ChefServerDetailsComponent implements OnInit, OnDestroy { formData: formData }; this.store.dispatch(new UploadZip( uploadZipPayload )); + this.migrationStarted = true; + this.getMigrationStatus(this.server.migration_id); } } diff --git a/components/automate-ui/src/assets/img/completed.png b/components/automate-ui/src/assets/img/completed.png new file mode 100644 index 00000000000..8b2528ac5c9 Binary files /dev/null and b/components/automate-ui/src/assets/img/completed.png differ diff --git a/components/automate-ui/src/assets/img/failed.png b/components/automate-ui/src/assets/img/failed.png new file mode 100644 index 00000000000..df0f17dc3d5 Binary files /dev/null and b/components/automate-ui/src/assets/img/failed.png differ