diff --git a/README.md b/README.md index 1f387356..f431040c 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,129 @@ # Summary OpenSDS dashboard uses the front-end development framework Angular5 (https://angular.io/) -and relies on PrimeNG UI Components (https://www.primefaces.org/primeng/). Regardless of -deployment or two development, prepare the corresponding environment. +and relies on PrimeNG UI Components (https://www.primefaces.org/primeng/). # Prerequisite -### 1. Ubuntu +## 1. Ubuntu * Version information ```shell root@proxy:~# cat /etc/issue Ubuntu 16.04.2 LTS \n \l ``` - -### 2. Nginx installation -```shell -sudo apt-get install -y nginx -``` - -### 3. NodeJS installation, NPM will be installed with nodejs. +## 2. NodeJS installation, NPM will be installed with nodejs. ```shell curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs ``` -### 4. Angular CLI installation +## 3. Angular CLI installation Specify the version[1.7.4] of angular5 suitable for installation. ```shell sudo npm install -g @angular/cli@1.7.4 ``` +## 4. Nginx installation (Optional) +To be used for testing the build using `ng build`. For development a local npm server is enough (explained below) +```shell +sudo apt-get install -y nginx +``` # Build & Start -### 1. Git clone dashboard code. +## 1. Git clone dashboard code. ```shell git clone https://github.com/opensds/opensds-dashboard.git ``` +Switch to appropriate branch or release tag. (`master`, `development` or the release tag) + +## 2. Build opensds dashboard. +### 2.1 **For local development** a local npm server can be started. This will allow to run a local server and make changes to the code and use the live reload feature to see code changes on the fly. + +#### Configure local proxy +Open the `proxy.conf.json ` file located in the document root and update the IP addresses with the appropriate endpoints where the OpenSDS components are installed (usually machine IP or VM IP) +```shell +cd opensds-dashboard +nano proxy.conf.json +``` +The contents of the proxy.conf.json files are : +``` +{ + "/v1beta": { + "target": "http://139.159.150.240:50040", + "secure": false + }, + "/v1": { + "target": "http://139.159.150.240:8089", + "secure": false + }, + "/v3": { + "target": "http://139.159.150.240/identity", + "secure": false + } +} +``` + +To enable debug mode and to check log messages replace the content with the following. The `/orch` block is needed to test Orchestration services if orchestration is enabled in the setup. +``` +{ + "/v1beta": { + "target": "http://192.168.56.123:50040", + "secure": false, + "logLevel" : "debug" + }, + "/orch" : { + "target": "http://192.168.56.123:5000", + "secure": false, + "logLevel" : "debug", + "changeOrigin" : true, + "pathRewrite" : { + "^/orch/" : "/v1beta/" + } + }, + "/v1": { + "target": "http://192.168.56.123:8089", + "secure": false, + "logLevel" : "debug" + }, + "/v3": { + "target": "http://192.168.56.123/identity", + "secure": false, + "logLevel" : "debug" + } +} +``` + +#### Start the live development server +```shell +npm install +npm start +``` +This will start a NG Live development server and can be accessed at http://localhost:4200/ + +Make any changes in the code and the server will live reload. If this fails then run the below commands. +```shell +sudo sysctl fs.inotify.max_user_watches=524288 +systemd-sysusers # (1) +``` + +### 2.2 **Alternatively the dashboard code can be built and deployed.** -### 2. Build opensds dashboard. -After the build work finished, the files in the `dist` folder should be copied to the folder ` /var/www/html/`. +Use the commands below to build the dashboard code. ```shell cd opensds-dashboard sudo npm install sudo ng build --prod ``` +After the build is successful, the files in the `dist` folder should be copied to the folder ` /var/www/html/`. ```shell cp -R opensds-dashboard/dist/* /var/www/html/ ``` -### 3. Set nginx default config. +## 3. Set nginx default config. (optional) ```shell vi /etc/nginx/sites-available/default ``` -Configure proxy, points to the resource server, multi-cloud server and the authentication server respectively. +Configure proxy, points to the resource server, multi-cloud server and the authentication server respectively. (replace 1.1.1.0 with the appropriate endpoints) Parameter 'client_max_body_size' configuration supports uploaded file size. Such as: * Keystone server `http://1.1.1.0/identity` @@ -73,12 +144,12 @@ location /v1/ { } ``` -### 4. Restart nginx +## 4. Restart nginx (optional) ```shell service nginx restart ``` -### 5. Access dashboard in browser. +## 5. Access dashboard in browser. (optional) ```shell http://localhost/ ``` diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 202d7dcf..9c87adc1 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { HostsResolver } from './business/block/modify-host/host-resolver.service'; +import { AzResolver } from './business/block/modify-host/az-resolver.service'; const routes: Routes = [ {path: '', redirectTo: '/home', pathMatch: 'full'}, @@ -7,6 +9,14 @@ const routes: Routes = [ {path: 'block', loadChildren: './business/block/block.module#BlockModule'}, {path: 'createVolume', loadChildren: './business/block/create-volume/create-volume.module#CreateVolumeModule'}, {path: 'volumeDetails/:volumeId', loadChildren: './business/block/volume-detail/volume-detail.module#VolumeDetailModule'}, + {path: 'createHost', loadChildren: './business/block/create-host/create-host.module#CreateHostModule'}, + {path: 'modifyHost/:hostId', loadChildren: './business/block/modify-host/modify-host.module#ModifyHostModule', + resolve: { + host: HostsResolver, + az: AzResolver + } + }, + {path: 'hostDetails/:hostId', loadChildren: './business/block/host-detail/host-detail.module#HostDetailModule'}, {path: 'cloud', loadChildren: './business/cloud/cloud.module#CloudModule'}, {path: 'profile', loadChildren: './business/profile/profile.module#ProfileModule'}, {path: 'createProfile', loadChildren: './business/profile/createProfile/createProfile.module#CreateProfileModule'}, @@ -26,6 +36,10 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + exports: [RouterModule], + providers: [ + HostsResolver, + AzResolver + ] }) export class AppRoutingModule {} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8bc994f6..8a0a9fa3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -78,7 +78,7 @@ export class AppComponent implements OnInit, AfterViewInit { }, { "title": "Resource", - "description": "Volumes / Buckets / File Share", + "description": "Volumes / Buckets / File Share / Hosts", "routerLink": "/block" }, { @@ -111,7 +111,7 @@ export class AppComponent implements OnInit, AfterViewInit { }, { "title": "Resource", - "description": "Volumes / Buckets / File Share", + "description": "Volumes / Buckets / File Share / Hosts", "routerLink": "/block" }, { diff --git a/src/app/business/block/attach.service.ts b/src/app/business/block/attach.service.ts new file mode 100644 index 00000000..f5566ecd --- /dev/null +++ b/src/app/business/block/attach.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { I18NService, HttpService, ParamStorService } from '../../shared/api'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AttachService { + constructor( + private http: HttpService, + private paramStor: ParamStorService + ) { } + + url = 'v1beta/{project_id}/block/attachments'; + + + //Fetch all Attachments + getAttachments(): Observable { + return this.http.get(this.url); + } + + //Fetch Attachment by ID + getAttachmentById(id): Observable { + let attachIdUrl = this.url + '/' + id + return this.http.get(attachIdUrl); + } + //Create Attachment + createAttachment(param) { + return this.http.post(this.url, param); + } + + //Update Attachment + modifyAttachments(id,param) { + let modifyUrl = this.url + '/' + id + return this.http.put(modifyUrl, param); + } + + //Delete Attachment + deleteAttachment(id): Observable { + let deleteUrl = this.url + '/' + id + return this.http.delete(deleteUrl); + } + +} \ No newline at end of file diff --git a/src/app/business/block/block.component.ts b/src/app/business/block/block.component.ts index 7e25ccdb..9aefeb9a 100644 --- a/src/app/business/block/block.component.ts +++ b/src/app/business/block/block.component.ts @@ -40,6 +40,7 @@ export class BlockComponent implements OnInit{ showDropDown:boolean = false; fromVolume:boolean = false; fromFileShare:boolean = false; + fromHosts: boolean = false; countItems = []; constructor( public I18N: I18NService, @@ -54,6 +55,7 @@ export class BlockComponent implements OnInit{ this.fromBuckets = params.fromRoute === "fromBuckets"; this.fromVolume = params.fromRoute === "fromVolume"; this.fromFileShare = params.fromRoute === "fromFileShare"; + this.fromHosts = params.fromRoute === "fromHosts"; } ); } diff --git a/src/app/business/block/block.html b/src/app/business/block/block.html index 7fe9f1b3..5138ab99 100644 --- a/src/app/business/block/block.html +++ b/src/app/business/block/block.html @@ -23,5 +23,11 @@ + +
+

{{I18N.keyID['sds_block_hosts_descrip']}}

+
+ +
diff --git a/src/app/business/block/block.module.ts b/src/app/business/block/block.module.ts index b452460b..7566f0d9 100644 --- a/src/app/business/block/block.module.ts +++ b/src/app/business/block/block.module.ts @@ -6,6 +6,7 @@ import { VolumeListModule } from './volumeList.module'; import { VolumeGroupModule } from './volumeGroup.module'; import { BucketsModule } from './buckets.module'; import { FileShareModule } from './fileShare.module'; +import { HostsModule } from './hosts.module'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @@ -26,6 +27,7 @@ let routers = [{ ButtonModule, BucketsModule, FileShareModule, + HostsModule, ReactiveFormsModule, FormsModule, CommonModule diff --git a/src/app/business/block/bucket-detail/bucket-detail.component.html b/src/app/business/block/bucket-detail/bucket-detail.component.html index 72ed856a..e1fdb000 100644 --- a/src/app/business/block/bucket-detail/bucket-detail.component.html +++ b/src/app/business/block/bucket-detail/bucket-detail.component.html @@ -25,7 +25,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/src/app/business/block/bucket-detail/bucket-detail.component.ts b/src/app/business/block/bucket-detail/bucket-detail.component.ts index e6c1e56f..a088a6a5 100644 --- a/src/app/business/block/bucket-detail/bucket-detail.component.ts +++ b/src/app/business/block/bucket-detail/bucket-detail.component.ts @@ -104,7 +104,7 @@ export class BucketDetailComponent implements OnInit { } //Click on folder folderLink(file){ - let folderKey = file.ObjectKey; + let folderKey = file.Contents.Key; if(this.folderId == ""){ this.folderId = folderKey; }else{ @@ -144,7 +144,7 @@ export class BucketDetailComponent implements OnInit { let str = res._body; let x2js = new X2JS(); let jsonObj = x2js.xml_str2json(str); - let alldir = jsonObj.ListObjectResponse.ListObjects ? jsonObj.ListObjectResponse.ListObjects :[] ; + let alldir = jsonObj.ListBucketResult ? jsonObj.ListBucketResult :[] ; if(Object.prototype.toString.call(alldir) === "[object Array]"){ this.allDir = alldir; }else if(Object.prototype.toString.call(alldir) === "[object Object]"){ @@ -160,17 +160,17 @@ export class BucketDetailComponent implements OnInit { } this.allDir = this.allDir.filter(arr=>{ let folderContain = false; - if(arr.ObjectKey.substring(0,this.folderId.length) == this.folderId && arr.ObjectKey.length > this.folderId.length){ + if(arr.Contents.Key.substring(0,this.folderId.length) == this.folderId && arr.Contents.Key.length > this.folderId.length){ // The number of occurrences of ":" in the folder let folderNum = (this.folderId.split(this.colon)).length-1; - let ObjectKeyNum = (arr.ObjectKey.split(this.colon)).length-1; + let ObjectKeyNum = (arr.Contents.Key.split(this.colon)).length-1; if(folderNum == ObjectKeyNum){ //Identify the file in the folder folderContain = true; }else if(ObjectKeyNum == folderNum + 1){ //Identify folders within folders - let lastNum = arr.ObjectKey.lastIndexOf(this.colon); - if(lastNum == arr.ObjectKey.length -1){ + let lastNum = arr.Contents.Key.lastIndexOf(this.colon); + if(lastNum == arr.Contents.Key.length -1){ folderContain = true; } } @@ -184,47 +184,48 @@ export class BucketDetailComponent implements OnInit { //Distinguish between folders and files at the first level this.allDir = this.allDir.filter(item=>{ let folderIndex = false; - if(item.ObjectKey.indexOf(this.colon) !=-1){ + if(item.Contents.Key.indexOf(this.colon) !=-1){ let index; - index = item.ObjectKey.indexOf(this.colon,index); + index = item.Contents.Key.indexOf(this.colon,index); //Distinguish between folders and files in folders - if(index == item.ObjectKey.length-1){ + if(index == item.Contents.Key.length-1){ folderIndex = true; } } - return item.ObjectKey.indexOf(this.colon) ==-1 || folderIndex; + return item.Contents.Key.indexOf(this.colon) ==-1 || folderIndex; }) } let folderArray = []; this.allFolderNameForCheck = []; this.allDir.forEach(item=>{ - item.size = Utils.getDisplayCapacity(item.Size,2,'KB'); - item.lastModified = Utils.formatDate(item.LastModified *1000); - item.Tier = "Tier_" + item.Tier + " (" + item.StorageClass + ")"; - if(item.ObjectKey.indexOf(this.colon) !=-1){ - item.objectName = item.ObjectKey.slice(0,item.ObjectKey.lastIndexOf(this.colon)); + item.size = Utils.getDisplayCapacity(item.Contents.Size,2,'KB'); + item.lastModified = Utils.formatDate(item.Contents.LastModified *1000); + item.Location = item.Contents.Location; + item.Tier = "Tier_" + item.Contents.Tier + " (" + item.Contents.StorageClass + ")"; + if(item.Contents.Key.indexOf(this.colon) !=-1){ + item.objectName = item.Contents.Key.slice(0,item.Contents.Key.lastIndexOf(this.colon)); this.allFolderNameForCheck.push(item.objectName); item.newFolder = true; item.disabled = false; item.size = "--"; backupAllDir.forEach(arr=>{ if(this.folderId !=""){ - let hasFolder = arr.ObjectKey.indexOf(this.folderId) !=-1 && arr.ObjectKey != this.folderId; + let hasFolder = arr.Contents.Key.indexOf(this.folderId) !=-1 && arr.Contents.Key != this.folderId; if( hasFolder){ - let newArrKey = arr.ObjectKey.slice(this.folderId.length); - if(newArrKey.slice(0,item.ObjectKey.length) == item.ObjectKey && newArrKey != item.ObjectKey){ + let newArrKey = arr.Contents.Key.slice(this.folderId.length); + if(newArrKey.slice(0,item.Contents.Key.length) == item.Contents.Key && newArrKey != item.Contents.Key){ item.disabled = true } } }else{ - let hasFile = arr.ObjectKey.indexOf(item.ObjectKey) !=-1 && arr.ObjectKey != item.ObjectKey; - if(hasFile && arr.ObjectKey.slice(0,item.ObjectKey.length) == item.ObjectKey){ + let hasFile = arr.Contents.Key.indexOf(item.Contents.Key) !=-1 && arr.Contents.Key != item.Contents.Key; + if(hasFile && arr.Contents.Key.slice(0,item.Contents.Key.length) == item.Contents.Key){ item.disabled = true } } }) }else{ - item.objectName = item.ObjectKey; + item.objectName = item.Contents.Key; item.newFolder = false; item.disabled = false; } @@ -237,20 +238,20 @@ export class BucketDetailComponent implements OnInit { resolveObject(){ let set = new Set(); this.allDir.forEach((item,index)=>{ - let includeIndex = item.ObjectKey.indexOf(this.colon); - if(includeIndex != -1 && includeIndex < item.ObjectKey.length-1){ + let includeIndex = item.Contents.Key.indexOf(this.colon); + if(includeIndex != -1 && includeIndex < item.Contents.Key.length-1){ while(includeIndex > -1){ - set.add(item.ObjectKey.substr(0,includeIndex+1)); - includeIndex = item.ObjectKey.indexOf(this.colon, includeIndex+1); + set.add(item.Contents.Key.substr(0,includeIndex+1)); + includeIndex = item.Contents.Key.indexOf(this.colon, includeIndex+1); } } }) this.allDir.forEach(it=>{ - set.delete(it.ObjectKey); + set.delete(it.Contents.Key); }) set.forEach(item=>{ let defaultObject = lodash.cloneDeep(this.allDir[0]); - defaultObject.ObjectKey = item; + defaultObject.Contents.Key = item; this.allDir.push(defaultObject); }) } @@ -362,9 +363,9 @@ export class BucketDetailComponent implements OnInit { downloadFile(file) { let fileObjectKey; if(this.folderId !=""){ - fileObjectKey = this.folderId + file.ObjectKey; + fileObjectKey = this.folderId + file.Contents.Key; }else{ - fileObjectKey = file.ObjectKey; + fileObjectKey = file.Contents.Key; } let downloadUrl = `${this.BucketService.url}/${this.bucketId}/${fileObjectKey}`; window['getAkSkList'](()=>{ @@ -383,14 +384,14 @@ export class BucketDetailComponent implements OnInit { this.httpClient.get(downloadUrl, options).subscribe((res)=>{ let blob = new Blob([res]); if (typeof window.navigator.msSaveBlob !== 'undefined') { - window.navigator.msSaveBlob(blob, file.ObjectKey); + window.navigator.msSaveBlob(blob, file.Contents.Key); } else { let URL = window.URL let objectUrl = URL.createObjectURL(blob) - if (file.ObjectKey) { + if (file.Contents.Key) { let a = document.createElement('a') a.href = objectUrl - a.download = file.ObjectKey + a.download = file.Contents.Key document.body.appendChild(a) a.click() a.remove() @@ -449,10 +450,10 @@ export class BucketDetailComponent implements OnInit { } deleteFile(file){ let fileObjectKey; - if(file.ObjectKey.indexOf(this.colon) !=-1){ - fileObjectKey = file.ObjectKey.slice(0,file.ObjectKey.length-1); + if(file.Contents.Key.indexOf(this.colon) !=-1){ + fileObjectKey = file.Contents.Key.slice(0,file.Contents.Key.length-1); }else{ - fileObjectKey = file.ObjectKey; + fileObjectKey = file.Contents.Key; } let msg = "
Are you sure you want to delete the File ?

[ "+ fileObjectKey +" ]

"; let header ="Delete"; @@ -471,7 +472,7 @@ export class BucketDetailComponent implements OnInit { try { switch(func){ case "delete": - let objectKey = file.ObjectKey; + let objectKey = file.Contents.Key; //If you want to delete files from a folder, you must include the name of the folder if(this.folderId !=""){ objectKey = this.folderId + objectKey; @@ -491,7 +492,7 @@ export class BucketDetailComponent implements OnInit { break; case "deleteMilti": file.forEach(element => { - let objectKey = element.ObjectKey; + let objectKey = element.Contents.Key; //If you want to delete files from a folder, you must include the name of the folder if(this.folderId !=""){ objectKey = this.folderId + objectKey; diff --git a/src/app/business/block/bucket-detail/lifeCycle/lifeCycle.component.ts b/src/app/business/block/bucket-detail/lifeCycle/lifeCycle.component.ts index 721dc8f0..71e71a17 100644 --- a/src/app/business/block/bucket-detail/lifeCycle/lifeCycle.component.ts +++ b/src/app/business/block/bucket-detail/lifeCycle/lifeCycle.component.ts @@ -539,32 +539,36 @@ export class LifeCycleComponent implements OnInit { getLifeCycleDataArray(value, object?, dialog?) { let xmlStr = ``; let x2js = new X2JS(); - let arr = lodash.cloneDeep(object.LifecycleConfiguration); - //In the modified/enable/disable state, delete the original lifeCycle and create new lifeCycle - let modifyCycle, modifyCleanDays; - if (dialog != "create") { - if (_.isArray(arr.Rule)) { - arr.Rule = arr.Rule.filter(item => { - return item.ID != value.name; - }) - } else { - arr = ""; + let defaultLifeCycle, modifyCycle, modifyCleanDays; + if (object) { + let arr = lodash.cloneDeep(object.LifecycleConfiguration); + //In the modified/enable/disable state, delete the original lifeCycle and create new lifeCycle + if (dialog != "create") { + if (_.isArray(arr.Rule)) { + arr.Rule = arr.Rule.filter(item => { + return item.ID != value.name; + }) + } else { + arr = ""; + } + if (dialog != "update") { + modifyCycle = this.modifyArr.filter(item => { + return item.ID == value.name; + }); + modifyCleanDays = modifyCycle[0].AbortIncompleteMultipartUpload.DaysAfterInitiation; + } } - if (dialog != "update") { - modifyCycle = this.modifyArr.filter(item => { - return item.ID == value.name; - }); - modifyCleanDays = modifyCycle[0].AbortIncompleteMultipartUpload.DaysAfterInitiation; + if(arr == "" || arr.Rule[0] || arr.Rule.constructor === Object){ + defaultLifeCycle = x2js.json2xml_str(arr); } } - let defaultLifeCycle = x2js.json2xml_str(arr); - if(value.Status){ - value.Status = value.Status == "enable" ? "Enabled" : "Disabled"; + if (value.Status) { + value.Status = value.Status.toLowerCase() == "enabled" ? "Enabled" : "Disabled"; } let Rules = { Rule: { ID: value.name, - Filter: { Prefix: value.Prefix ? value.Prefix : value.prefix }, + Filter: { Prefix: value.Prefix ? value.Prefix : (value.newPrefix ? value.newPrefix : value.prefix)}, Status: (((this.transChecked && this.transOptions.length != 0) || this.expirChecked || this.expirCleanUp) && value.enabled) ? "Enabled" : value.Status ? value.Status : "Disabled" } @@ -653,7 +657,11 @@ export class LifeCycleComponent implements OnInit { jsonObj = jsonObj + Incomplete; } let newLifeCycle = jsonObj + ruleXml; - xmlStr = xmlStr + defaultLifeCycle + newLifeCycle + ``; + if (defaultLifeCycle) { + xmlStr = xmlStr + defaultLifeCycle + newLifeCycle + ``; + } else { + xmlStr = xmlStr + newLifeCycle + ``; + } return xmlStr; } checkStorageClass(control: FormControl): { [s: string]: boolean } { @@ -680,8 +688,8 @@ export class LifeCycleComponent implements OnInit { acceptLabel: this.i18n.keyID['ok'], isWarning: true, accept: () => { - selectedArr.forEach(item=>{ - item.Status = "enable" + selectedArr.forEach(item => { + item.Status = "enabled" item.name = item.ObjectKey; this.onSubmit(item,"enable"); }) @@ -706,8 +714,8 @@ export class LifeCycleComponent implements OnInit { acceptLabel: this.i18n.keyID['ok'], isWarning: true, accept: () => { - selectedArr.forEach(item=>{ - item.Status = "disable" + selectedArr.forEach(item => { + item.Status = "disabled" item.name = item.ObjectKey; this.onSubmit(item,"disable"); }) @@ -906,7 +914,11 @@ export class LifeCycleComponent implements OnInit { isWarning: true, accept: () => { arr.forEach((item, index) => { - this.deleteLifeCycle(item) + if(arr.length > 1){ + this.deleteLifeCycle(item,"multiple"); + }else{ + this.deleteLifeCycle(item); + } }) }, reject: () => { } @@ -915,7 +927,8 @@ export class LifeCycleComponent implements OnInit { } - deleteLifeCycle(value) { + deleteLifeCycle(value, multiple?) { + //Multiple means batch deletion window['getAkSkList'](() => { let requestMethod = "DELETE"; let url = this.BucketService.url + '/' + this.bucketId + "/?lifecycle" + "&ruleID=" + value.ObjectKey; @@ -924,10 +937,22 @@ export class LifeCycleComponent implements OnInit { this.getSignature(options); let requestUrl = this.bucketId + "/?lifecycle" + "&ruleID=" + value.ObjectKey; this.BucketService.deleteLifeCycle(requestUrl, options).subscribe((res) => { - this.modifyArr = this.modifyArr.filter(item => { - item.ObjectKey != value.ObjectKey; + let lifeCycleArr = _.filter(this.lifeCycleAlls, item=>{ + item.name = item.ObjectKey; + return item.ObjectKey != value.ObjectKey; + }); + this.modifyArr = _.filter(this.modifyArr, item => { + return item.ID != value.ObjectKey; }) - this.getLifeCycleList(); + this.submitObj.LifecycleConfiguration.Rule = _.filter(this.submitObj.LifecycleConfiguration.Rule,it=>{ + return it.ID != value.ObjectKey; + }) + if(lifeCycleArr.length >0 && !multiple){ + let data = this.getLifeCycleDataArray(lifeCycleArr[0], this.submitObj, lifeCycleArr[0].Status); + this.createLifeCycleSubmit(data); + }else{ + this.getLifeCycleList(); + } }) }) }) diff --git a/src/app/business/block/buckets.component.ts b/src/app/business/block/buckets.component.ts index abcfa58a..5105c7df 100644 --- a/src/app/business/block/buckets.component.ts +++ b/src/app/business/block/buckets.component.ts @@ -5,7 +5,7 @@ import { AppService } from 'app/app.service'; import { trigger, state, style, transition, animate} from '@angular/animations'; import { I18nPluralPipe } from '@angular/common'; import { FormControl, FormGroup, FormBuilder, Validators, ValidatorFn, AbstractControl } from '@angular/forms'; -import { MenuItem ,ConfirmationService} from '../../components/common/api'; +import { MenuItem ,ConfirmationService, Message} from '../../components/common/api'; import { BucketService} from './buckets.service'; import { debug } from 'util'; import { MigrationService } from './../dataflow/migration.service'; @@ -72,6 +72,15 @@ export class BucketsComponent implements OnInit{ allBucketNameForCheck=[]; showCreateBucket = false; akSkRouterLink = "/akSkManagement"; + enableVersion: boolean; + enableEncryption = false; + sseTypes = []; + selectedSse; + isSSE: boolean = false; + isSSEKMS: boolean = false; + isSSEC: boolean = false; + msgs: Message[]; + constructor( public I18N: I18NService, private router: Router, @@ -92,7 +101,10 @@ export class BucketsComponent implements OnInit{ this.createBucketForm = this.fb.group({ "backend":["",{validators:[Validators.required], updateOn:'change'}], "backend_type":["",{validators:[Validators.required], updateOn:'change'}], - "name":["",{validators:[Validators.required,Utils.isExisted(this.allBucketNameForCheck)], updateOn:'change'}] + "name":["",{validators:[Validators.required,Utils.isExisted(this.allBucketNameForCheck)], updateOn:'change'}], + "version": [false, { validators: [Validators.required], updateOn: 'change' }], + "encryption": [false, { validators: [Validators.required], updateOn: 'change' }], + "sse":["",{}], }); this.migrationForm = this.fb.group({ "name": ['',{validators:[Validators.required], updateOn:'change'}], @@ -131,6 +143,12 @@ export class BucketsComponent implements OnInit{ }]; this.allBackends = []; this.getBuckets(); + this.sseTypes = [ + { + label: "SSE", + value: 'sse' + } + ] } showcalendar(){ this.selectTime = !this.selectTime; @@ -190,6 +208,8 @@ export class BucketsComponent implements OnInit{ label:item.name, value:item.name }); + item.encryptionEnabled = item.SSEConfiguration.SSE.enabled.toLower() == "true" ? true : false; + item.versionEnabled = item.VersioningConfiguration.Status.toLower() == "enabled" ? true : false; }); this.initBucket2backendAnd2Type(); }); @@ -327,7 +347,12 @@ export class BucketsComponent implements OnInit{ }); }); } - + versionControl(){ + this.enableVersion = this.createBucketForm.get('version').value; + } + encryptionControl(){ + this.enableEncryption = this.createBucketForm.get('encryption').value; + } creatBucket(){ if(!this.createBucketForm.valid){ for(let i in this.createBucketForm.controls){ @@ -338,7 +363,7 @@ export class BucketsComponent implements OnInit{ let param = { name:this.createBucketForm.value.name, backend_type:this.createBucketForm.value.backend_type, - backend:this.createBucketForm.value.backend, + backend:this.createBucketForm.value.backend }; let xmlStr = ` ${this.createBucketForm.value.backend} @@ -352,14 +377,148 @@ export class BucketsComponent implements OnInit{ options.headers.set('Content-Type','application/xml'); this.BucketService.createBucket(this.createBucketForm.value.name,xmlStr,options).subscribe(()=>{ this.createBucketDisplay = false; - this.getBuckets(); + /* Add the PUT Encryption Call here before fetching the updated list of Buckets */ + if(this.enableEncryption){ + this.bucketEncryption(); + } + if(this.enableVersion){ + this.enableBucketVersioning(this.createBucketForm.value.name); + } + if(!this.enableEncryption && !this.enableVersion){ + this.getBuckets(); + } + + /* Call the getBuckets call in the success of the encryption call */ + }); }) }) } + + bucketEncryption(){ + switch (this.selectedSse) { + case 'sse': + this.isSSE = true; + break; + case 'sse-kms': + this.isSSEKMS = true; + break; + case 'sse-c': + this.isSSEC = true; + break; + + default: + break; + } + + let encryptStr = ` + + ${this.isSSE} + + + ${this.isSSEKMS} + string + + `; + window['getAkSkList'](()=>{ + let requestMethod = "PUT"; + let url = this.BucketService.url+"/"+this.createBucketForm.value.name; + window['canonicalString'](requestMethod, url,()=>{ + let options: any = {}; + this.getSignature(options); + options.headers.set('Content-Type','application/xml'); + this.BucketService.setEncryption(this.createBucketForm.value.name,encryptStr,options).subscribe((res)=>{ + if(this.enableVersion){ + this.enableBucketVersioning(this.createBucketForm.value.name); + this.enableVersion = false; + } + this.getBuckets(); + }, (error) => { + console.log("Set encryption failed", error); + }); + }); + }) + } + showEnableVersioning(bucketName){ + let msg = "
Are you sure you want to Enable Versioning on the Bucket ?

[ "+ bucketName +" ]

"; + let header ="Enable Versioning"; + let acceptLabel = "Enable"; + let warming = false; + this.confirmDialog([msg,header,acceptLabel,warming,"enable"], bucketName); + } + enableBucketVersioning(bucketName){ + let versionStr = ` + Enabled + ` + window['getAkSkList'](()=>{ + let requestMethod = "PUT"; + let url = this.BucketService.url+"/"+bucketName; + window['canonicalString'](requestMethod, url,()=>{ + let options: any = {}; + this.getSignature(options); + options.headers.set('Content-Type','application/xml'); + this.BucketService.setVersioning(bucketName, versionStr, options).subscribe(()=>{ + + if(this.enableEncryption){ + this.bucketEncryption(); + this.enableEncryption=false; + } + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Versioning enabled successfully.'}); + this.getBuckets(); + }, (error) =>{ + console.log("Set versioning failed", error); + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: "Enable versioning failed
" + error}); + }); + }); + }); + } + showSuspendVersioning(bucketName){ + let msg = "
Are you sure you want to Suspend Versioning on the Bucket ?

[ "+ bucketName +" ]

"; + let header ="Suspend Versioning"; + let acceptLabel = "Suspend"; + let warming = true; + this.confirmDialog([msg,header,acceptLabel,warming,"suspend"], bucketName); + } + suspendVersioning(bucketName){ + console.log("Suspend Versioning", bucketName); + let versionStr = ` + Suspended + ` + window['getAkSkList'](()=>{ + let requestMethod = "PUT"; + let url = this.BucketService.url+"/"+bucketName; + window['canonicalString'](requestMethod, url,()=>{ + let options: any = {}; + this.getSignature(options); + options.headers.set('Content-Type','application/xml'); + this.BucketService.suspendVersioning(bucketName, versionStr, options).subscribe(()=>{ + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Versioning suspended successfully.'}); + this.getBuckets(); + }, (error) =>{ + console.log("Suspend versioning failed", error); + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: "Suspend versioning failed
" + error}); + }); + }); + }); + + } showCreateForm(){ this.createBucketDisplay = true; - this.createBucketForm.reset(); + this.enableEncryption = false; + this.enableVersion = false; + this.createBucketForm.reset( + { + "backend":"", + "backend_type":"", + "name":"", + "version": false, + "encryption": false + } + ); this.createBucketForm.controls['name'].setValidators([Validators.required,Utils.isExisted(this.allBucketNameForCheck)]); this.getTypes(); } @@ -374,7 +533,7 @@ export class BucketsComponent implements OnInit{ let str = res._body; let x2js = new X2JS(); let jsonObj = x2js.xml_str2json(str); - let alldir = jsonObj.ListObjectResponse.ListObjects ? jsonObj.ListObjectResponse.ListObjects :[] ; + let alldir = jsonObj.ListBucketResult ? jsonObj.ListBucketResult :[] ; if(alldir.length === 0){ this.http.get(`v1/{project_id}/plans?bucketname=${bucket.name}`).subscribe((res)=>{ let plans = res.json().plans ? res.json().plans : []; @@ -418,26 +577,41 @@ export class BucketsComponent implements OnInit{ isWarning: warming, accept: ()=>{ try { - let name = bucket.name; - if(plans){ - plans.forEach(element => { - this.http.delete(`v1/{project_id}/plans/${element.id}`).subscribe(); - }); + switch (func) { + case "delete": console.log("Delete Confirm"); + let name = bucket.name; + if(plans){ + plans.forEach(element => { + this.http.delete(`v1/{project_id}/plans/${element.id}`).subscribe(); + }); + } + window['getAkSkList'](()=>{ + let requestMethod = "DELETE"; + let url = this.BucketService.url + '/' + name; + window['canonicalString'](requestMethod, url,()=>{ + let options: any = {}; + this.getSignature(options); + this.BucketService.deleteBucket(name,options).subscribe((res) => { + this.getBuckets(); + }, + error=>{ + this.getBuckets(); + }); + }) + }) + break; + + case "suspend": console.log("Suspend Confirm") + this.suspendVersioning(bucket); + break; + case "enable": console.log("Enable Confirm"); + this.suspendVersioning(bucket); + break; + + default: + break; } - window['getAkSkList'](()=>{ - let requestMethod = "DELETE"; - let url = this.BucketService.url + '/' + name; - window['canonicalString'](requestMethod, url,()=>{ - let options: any = {}; - this.getSignature(options); - this.BucketService.deleteBucket(name,options).subscribe((res) => { - this.getBuckets(); - }, - error=>{ - this.getBuckets(); - }); - }) - }) + } catch (e) { diff --git a/src/app/business/block/buckets.html b/src/app/business/block/buckets.html index 0a219a8f..97fec20e 100644 --- a/src/app/business/block/buckets.html +++ b/src/app/business/block/buckets.html @@ -1,4 +1,4 @@ - +
@@ -27,15 +27,33 @@ + + +

+ Yes +

+

+ No +

+
+
+ + +

YesAES256

+

No

+
+
+
Suspend Versioning + Enable Versioning Migrate Delete - +
@@ -48,6 +66,16 @@ + + + + + + + + + + diff --git a/src/app/business/block/buckets.module.ts b/src/app/business/block/buckets.module.ts index 68cf83c7..0ef9c8ce 100644 --- a/src/app/business/block/buckets.module.ts +++ b/src/app/business/block/buckets.module.ts @@ -3,7 +3,7 @@ import { BucketsComponent } from './buckets.component'; import { RouterModule } from '@angular/router'; import { TabViewModule, ButtonModule } from '../../components/common/api'; import {DataTableModule, DropMenuModule, DialogModule, FormModule, InputTextModule, InputTextareaModule, - DropdownModule ,ConfirmationService,ConfirmDialogModule,CalendarModule,CheckboxModule} from '../../components/common/api'; + DropdownModule ,ConfirmationService,ConfirmDialogModule,CalendarModule,CheckboxModule, InputSwitchModule, GrowlModule} from '../../components/common/api'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { HttpService } from './../../shared/service/Http.service'; @@ -31,7 +31,9 @@ import { MigrationService } from './../dataflow/migration.service'; ConfirmDialogModule, RouterModule, CalendarModule, - CheckboxModule + CheckboxModule, + InputSwitchModule, + GrowlModule ], exports: [ BucketsComponent ], providers: [ diff --git a/src/app/business/block/buckets.service.ts b/src/app/business/block/buckets.service.ts index ea008add..990cdf04 100644 --- a/src/app/business/block/buckets.service.ts +++ b/src/app/business/block/buckets.service.ts @@ -49,6 +49,19 @@ export class BucketService { let url = this.url + '/' + id; return this.http.get(url,options); } + + //Set Bucket Encryption + setEncryption(name,param?,options?) { + return this.http.put(this.url+"/"+name+"/?DefaultEncryption",param,options); + } + + //Set Bucket Versioning + setVersioning(name,param?,options?) { + return this.http.put(this.url+"/"+name+"/?versioning",param,options); + } + suspendVersioning(name,param?,options?) { + return this.http.put(this.url+"/"+name+"/?versioning",param,options); + } getBckends(): Observable { return this.http.get('v1beta/{project_id}/backend'); diff --git a/src/app/business/block/create-host/create-host.component.html b/src/app/business/block/create-host/create-host.component.html new file mode 100644 index 00000000..43069038 --- /dev/null +++ b/src/app/business/block/create-host/create-host.component.html @@ -0,0 +1,63 @@ + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+ + +
+ + +
+
+ \ No newline at end of file diff --git a/src/app/business/block/create-host/create-host.component.ts b/src/app/business/block/create-host/create-host.component.ts new file mode 100644 index 00000000..fa65d676 --- /dev/null +++ b/src/app/business/block/create-host/create-host.component.ts @@ -0,0 +1,191 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { trigger, state, style, transition, animate } from '@angular/animations'; +import { Validators, FormControl, FormGroup, FormBuilder, FormArray } from '@angular/forms'; + +import { Message, SelectItem } from './../../../components/common/api'; +import { AvailabilityZonesService } from './../../resource/resource.service'; +import { HostsService} from './../hosts.service'; +import { I18NService,Utils } from '../../../../app/shared/api'; +import { Form } from '../../../components/form/form'; + +@Component({ + selector: 'app-create-host', + templateUrl: './create-host.component.html', + styleUrls: [ + + ], + animations: [ + trigger('overlayState', [ + state('hidden', style({ + opacity: 0 + })), + state('visible', style({ + opacity: 1 + })), + transition('visible => hidden', animate('400ms ease-in')), + transition('hidden => visible', animate('400ms ease-out')) + ]), + + trigger('notificationTopbar', [ + state('hidden', style({ + height: '0', + opacity: 0 + })), + state('visible', style({ + height: '*', + opacity: 1 + })), + transition('visible => hidden', animate('400ms ease-in')), + transition('hidden => visible', animate('400ms ease-out')) + ]) + ] +}) +export class CreateHostComponent implements OnInit { + + label = { + availabilityZones: this.i18n.keyID["sds_host_availability_zones"], + name: this.i18n.keyID["sds_block_volume_name"], + osType: this.i18n.keyID["sds_host_os_type"], + ip: this.i18n.keyID["sds_host_ip"], + accessMode: this.i18n.keyID["sds_host_access_mode"], + initiators: this.i18n.keyID["sds_host_initiators"] + }; + msgs: Message[]; + items; + availabilityZonesOptions = []; + osTypeOptions; + accessModeOptions; + protocolOptions; + createHostform; + errorMessage = { + "availabilityZones": { required: "Atleast one Zone is required."}, + "accessMode": { required: "Access Mode is required."}, + "osType": { required: "OS Type is required."}, + "ip": { + required: "IP Address is required", + pattern: "Enter valid IPv4 address" + }, + "hostName": { + required: "Host Name is required.", + maxlength: "Maximum 28 characters", + minlength: "Minimum 2 characters" + } + }; + validRule = { + 'validIp': '([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})' /* Validates IPv4 address */ + }; + + constructor( + private router: Router, + private fb: FormBuilder, + private HostsService: HostsService, + private availabilityZonesService:AvailabilityZonesService, + public i18n:I18NService + ) {} + + ngOnInit() { + this.osTypeOptions = [ + { + label: "Linux", + value: 'linux' + }, + { + label: "Windows", + value: 'windows' + }, + { + label: "ESXi", + value: 'esxi' + } + ]; + this.accessModeOptions = [ + { + label: "Agentless", + value: "agentless" + } + ]; + this.protocolOptions = [ + { + label: "iSCSI", + value: "iscsi" + }, + { + label: "FC", + value: "fibre_channel" + }, + { + label: "NVMeOF", + value: "nvmeof" + } + ] + this.getAZ(); + this.items = [ + { label: this.i18n.keyID["sds_Hosts_title"], url: '/block' }, + { label: this.i18n.keyID["sds_create_host"], url: '/createHost' } + ]; + this.createHostform = this.fb.group({ + 'availabilityZones': new FormControl('', Validators.required), + 'hostName': new FormControl('', {validators: [Validators.required, Validators.maxLength(28), Validators.minLength(2)]}), + 'accessMode': new FormControl('', Validators.required), + 'osType': new FormControl('', Validators.required), + 'ip': new FormControl('', {validators:[Validators.required, Validators.pattern(this.validRule.validIp)]}), + 'initiators' : this.fb.array([this.createInitiators()]) + }); + + } + createInitiators(){ + return this.fb.group({ + portName: new FormControl('', Validators.required), + protocol: new FormControl('', Validators.required) + }) + } + addNext() { + (this.createHostform.controls['initiators'] as FormArray).push(this.createInitiators()) + } + removeLink(i: number) { + if(this.createHostform.controls['initiators'].length > 1){ + this.createHostform.controls['initiators'].removeAt(i); + } + } + onSubmit(){ + if(this.createHostform.valid){ + let param = { + "accessMode" : this.createHostform.value.accessMode, + "hostName" : this.createHostform.value.hostName, + "osType" : this.createHostform.value.osType, + "ip" : this.createHostform.value.ip, + "availabilityZones" : this.createHostform.value.availabilityZones, + "initiators" : this.createHostform.value.initiators + } + this.createHost(param); + } + + } + createHost(param){ + this.HostsService.createHost(param).subscribe((res) => { + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Host Created Successfully.'}); + this.router.navigate(['/block',"fromHosts"]); + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); + }); + } + + getAZ(){ + this.availabilityZonesService.getAZ().subscribe((azRes) => { + let AZs=azRes.json(); + let azArr = []; + if(AZs && AZs.length !== 0){ + AZs.forEach(item =>{ + let obj = {label: item, value: item}; + azArr.push(obj); + }) + } + this.availabilityZonesOptions = azArr; + }) + } + + +} diff --git a/src/app/business/block/create-host/create-host.module.ts b/src/app/business/block/create-host/create-host.module.ts new file mode 100644 index 00000000..32a7c09c --- /dev/null +++ b/src/app/business/block/create-host/create-host.module.ts @@ -0,0 +1,48 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; + +import { CreateHostComponent } from './create-host.component'; + +import { HttpService } from './../../../shared/api'; +import { VolumeService,ReplicationService } from './../volume.service'; +import { ProfileService } from './../../profile/profile.service'; +import { AvailabilityZonesService } from './../../resource/resource.service'; +import { HostsService } from '../hosts.service'; +import { InputTextModule, CheckboxModule, ButtonModule, DropdownModule, MultiSelectModule, DialogModule, Message, GrowlModule, SpinnerModule, FormModule } from './../../../components/common/api'; + +let routers = [{ + path: '', + component: CreateHostComponent +}] + +@NgModule({ + imports: [ + RouterModule.forChild(routers), + ReactiveFormsModule, + FormsModule, + CommonModule, + InputTextModule, + CheckboxModule, + ButtonModule, + DropdownModule, + MultiSelectModule, + DialogModule, + GrowlModule, + SpinnerModule, + FormModule + ], + declarations: [ + CreateHostComponent + ], + providers: [ + HttpService, + VolumeService, + ProfileService, + ReplicationService, + AvailabilityZonesService, + HostsService + ] +}) +export class CreateHostModule { } diff --git a/src/app/business/block/file-share-detail/file-share-detail.component.ts b/src/app/business/block/file-share-detail/file-share-detail.component.ts index 5110181d..12f66209 100644 --- a/src/app/business/block/file-share-detail/file-share-detail.component.ts +++ b/src/app/business/block/file-share-detail/file-share-detail.component.ts @@ -251,7 +251,10 @@ export class FileShareDetailComponent implements OnInit{ showSnapshotPropertyDialog(dialog, selectedSnapshot?){ if(dialog == 'create'){ this.snapshotCreateShow = true; - this.createSnapshotFormGroup.reset(); + this.createSnapshotFormGroup.reset({ + name: "", + description: "" + }); this.getSnapshotNameCheck(this.createSnapshotFormGroup); }else if(dialog == 'modify'){ this.snapshotModifyShow = true; diff --git a/src/app/business/block/fileShare.component.ts b/src/app/business/block/fileShare.component.ts index a50cd6e0..2bf898af 100644 --- a/src/app/business/block/fileShare.component.ts +++ b/src/app/business/block/fileShare.component.ts @@ -163,7 +163,10 @@ export class FileShareComponent implements OnInit{ returnSelectedFileShare(selectedFileShare, dialog){ if(dialog == 'snapshot'){ this.createSnapshotShow = true; - this.createSnapshotForm.reset(); + this.createSnapshotForm.reset({ + name: "", + description: "" + }); this.checkSnapshotName = false; this.createSnapshotForm.get("name").valueChanges.subscribe((value: string)=>{ let defaultLength = "snapshot".length; @@ -181,7 +184,12 @@ export class FileShareComponent implements OnInit{ }else if(dialog == 'acl'){ this.aclCreateShow = true; this.aclsItems = [0]; - this.createAclsFormGroup.reset(); + this.createAclsFormGroup.reset({ + level : "", + user : "", + userInput0 : "", + description : "" + }); this.getAcls(selectedFileShare); this.showIpErrorMessage = [false]; } diff --git a/src/app/business/block/host-detail/host-detail.component.html b/src/app/business/block/host-detail/host-detail.component.html new file mode 100644 index 00000000..7b1b206f --- /dev/null +++ b/src/app/business/block/host-detail/host-detail.component.html @@ -0,0 +1,88 @@ +
+
+ + {{item.label}} + > + +
+
+

{{i18n.keyID['sds_block_volume_base_info']}}

+

{{ hostSource }}

+
+
+
+ {{label.hostName}}: +
+
+ {{host.hostName}} +
+
+ {{label.status}}: +
+
+ {{host.status? host.status : '--'}} +
+
+ {{label.createdAt}}: +
+
+ {{host.createdAt ? (host.createdAt | date:'long') : '--'}} +
+ +
+
+
+ {{label.ip}}: +
+
+ {{host.ip}} +
+
+ {{label.accessMode}}: +
+
+ {{host.accessMode}} +
+
+ {{label.updatedAt}}: +
+
+ {{host.updatedAt ? (host.updatedAt | date:'long') : '--'}} +
+ +
+
+
+ {{label.port}}: +
+
+ {{host.port ? host.port : '--'}} +
+
+ {{label.availabilityZones}}: +
+
+ {{host.availabilityZones}} +
+ +
+ {{label.osType}}: +
+
+ {{host.osType}} +
+
+
+
+ {{label.initiators}}: +
+
+
+ + + + +
+
+
+
diff --git a/src/app/business/block/host-detail/host-detail.component.ts b/src/app/business/block/host-detail/host-detail.component.ts new file mode 100644 index 00000000..87c98a63 --- /dev/null +++ b/src/app/business/block/host-detail/host-detail.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { Router,ActivatedRoute} from '@angular/router'; + +import { VolumeService } from '../volume.service'; +import { HostsService } from '../hosts.service'; +import { ProfileService } from '../../profile/profile.service'; +import { I18NService, Utils } from '../../../../app/shared/api'; + +let _ = require("underscore"); + +@Component({ + selector: 'app-host-detail', + templateUrl: './host-detail.component.html', + styleUrls: [ + + ] +}) +export class HostDetailComponent implements OnInit { + items; + label; + host; + hostDetails; + hostId; + showHostSource: boolean = false; + HostSource: string = ""; + fromHost: boolean = false; + + constructor( + private HostsService: HostsService, + private ActivatedRoute: ActivatedRoute, + private ProfileService: ProfileService, + public i18n:I18NService + ) { + + } + + ngOnInit() { + + this.ActivatedRoute.params.subscribe((params) => this.hostId = params.hostId); + console.log("Host: ", this.hostId); + this.HostsService.getHostById(this.hostId).subscribe((res) => { + + this.host = res.json(); + }, (err) => { + console.log("Something went wrong. Details could not be fetched.") + }); + this.items = [ + { label: this.i18n.keyID["sds_Hosts_title"], url: '/block' }, + { label: this.i18n.keyID["sds_Host_detail"], url: '/hostDetails' } + ]; + + this.label = { + hostName: "Name", + ip: "IP Address", + port: "Port", + createdAt: "Created At", + updatedAt: "Updated At", + tenantId: "Tenant", + status: "Status", + osType: "OS", + accessMode: "Access Mode", + availabilityZones: "Availability Zones", + initiators: "Initiators" + }; + + + } + +} diff --git a/src/app/business/block/host-detail/host-detail.module.ts b/src/app/business/block/host-detail/host-detail.module.ts new file mode 100644 index 00000000..69029968 --- /dev/null +++ b/src/app/business/block/host-detail/host-detail.module.ts @@ -0,0 +1,49 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { HostDetailComponent } from './host-detail.component'; + +import { TabViewModule,ButtonModule, DataTableModule,DropdownModule, DropMenuModule, DialogModule, FormModule, InputTextModule, InputTextareaModule, ConfirmDialogModule ,ConfirmationService,CheckboxModule} from '../../../components/common/api'; +import { HttpService } from '../../../shared/service/Http.service'; +import { VolumeService} from '../volume.service'; +import { ProfileService } from '../../profile/profile.service'; +import { HostsService } from '../hosts.service'; +import {GrowlModule} from '../../../components/growl/growl'; + +let routers = [{ + path: '', + component: HostDetailComponent +}] + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + FormsModule, + InputTextModule, + InputTextareaModule, + RouterModule.forChild(routers), + TabViewModule, + ButtonModule, + DataTableModule, + DialogModule, + FormModule, + ConfirmDialogModule, + DropdownModule, + DropMenuModule, + CheckboxModule, + GrowlModule + ], + declarations: [ + HostDetailComponent, + ], + providers: [ + HttpService, + VolumeService, + HostsService, + ConfirmationService, + ProfileService, + ] +}) +export class HostDetailModule { } diff --git a/src/app/business/block/hosts.component.html b/src/app/business/block/hosts.component.html new file mode 100644 index 00000000..18fcb990 --- /dev/null +++ b/src/app/business/block/hosts.component.html @@ -0,0 +1,122 @@ + +
+
+ +
+
+
+ + +
+ +
+
+ + + + + + + {{host.hostName}} + + + + + + {{host.port? host.port : '-'}} + + + + + + + {{host.availabilityZones[0]}} + ... + + + + + + + + +
+
+
+ {{label.hostName}}: +
+
+ {{host.hostName}} +
+
+ {{label.status}}: +
+
+ {{host.status? host.status : '--'}} +
+
+ {{label.createdAt}}: +
+
+ {{host.createdAt ? (host.createdAt | date:'long') : '--'}} +
+ +
+
+
+ {{label.ip}}: +
+
+ {{host.ip}} +
+
+ {{label.accessMode}}: +
+
+ {{host.accessMode}} +
+
+ {{label.updatedAt}}: +
+
+ {{host.updatedAt ? (host.updatedAt | date:'long') : '--'}} +
+ +
+
+
+ {{label.port}}: +
+
+ {{host.port ? host.port : '--'}} +
+
+ {{label.availabilityZones}}: +
+
+ {{host.availabilityZones}} +
+ +
+ {{label.osType}}: +
+
+ {{host.osType}} +
+
+
+
+ {{label.initiators}}: +
+
+
+ + + + +
+
+
+
+ + diff --git a/src/app/business/block/hosts.component.ts b/src/app/business/block/hosts.component.ts new file mode 100644 index 00000000..c86f890b --- /dev/null +++ b/src/app/business/block/hosts.component.ts @@ -0,0 +1,195 @@ +import { Component, OnInit, ViewContainerRef, ViewChild, Directive, ElementRef, HostBinding, HostListener } from '@angular/core'; +import { Router } from '@angular/router'; +import { I18NService, Utils } from 'app/shared/api'; +import { FormControl, FormGroup, FormBuilder, Validators, ValidatorFn, AbstractControl } from '@angular/forms'; +import { AppService } from 'app/app.service'; +import { I18nPluralPipe } from '@angular/common'; +import { trigger, state, style, transition, animate } from '@angular/animations'; +import { Message, MenuItem ,ConfirmationService} from '../../components/common/api'; + +import { VolumeService } from './volume.service'; +import { HostsService } from './hosts.service'; +import { ProfileService } from './../profile/profile.service'; +import { identifierModuleUrl } from '@angular/compiler'; + +let _ = require("underscore"); +@Component({ + selector: 'app-hosts', + templateUrl: 'hosts.component.html', + providers: [ConfirmationService], + styleUrls: [], + animations: [] +}) +export class HostsComponent implements OnInit { + + capacityOptions = [ + { + label: 'GB', + value: 'gb' + }, + { + label: 'TB', + value: 'tb' + } + + ]; + profileOptions = []; + snapProfileOptions = []; + azOption=[{label:"Secondary",value:"secondary"}]; + selectedHosts = []; + selectedHost; + volumes = []; + allVolumes; + menuItems: MenuItem[]; + menuDeleDisableItems: MenuItem[]; + isVolumeAttached: boolean = false; + msgs: Message[]; + allHosts = []; + label = { + hostName: "Name", + ip: "IP Address", + port: "Port", + createdAt: "Created At", + updatedAt: "Updated At", + tenantId: "Tenant", + status: "Status", + osType: "OS", + accessMode: "Access Mode", + availabilityZones: "Availability Zones", + initiators: "Initiators" + }; + + constructor( + public I18N: I18NService, + private router: Router, + private VolumeService: VolumeService, + private HostsService: HostsService, + private confirmationService: ConfirmationService, + private fb: FormBuilder + ) { + + } + + + + returnSelectedHost(host){ + this.selectedHost = host; + } + + ngOnInit() { + this.getAllHosts(); + this.menuItems = [ + { + "label": this.I18N.keyID['sds_block_volume_modify'], + command: () => { + if(this.selectedHost){ + this.modifyHost(this.selectedHost) + } + + }, + disabled:false + }, + { + "label": this.I18N.keyID['sds_block_volume_delete'], + command: () => { + if (this.selectedHost) { + this.batchDeleteHosts(this.selectedHost); + } + }, + disabled:false + } + ]; + this.menuDeleDisableItems = [ + { + "label": this.I18N.keyID['sds_block_volume_modify'], + command: () => { + if(this.selectedHost){ + this.modifyHost(this.selectedHost) + } + }, + disabled:false + }, + { + "label": this.I18N.keyID['sds_block_volume_delete'], + command: () => { + if (this.selectedHost) { + this.batchDeleteHosts(this.selectedHost); + } + }, + disabled:true + } + ]; + + } + + getAllHosts(){ + this.HostsService.getHosts().subscribe((res) => { + this.allHosts = res.json(); + }, (error) =>{ + console.log(error); + }) + + } + + getVolumes() { + this.VolumeService.getVolumes().subscribe((res) => { + let volumes = res.json(); + this.allVolumes = volumes; + }, (error) => { + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.message}); + }); + } + + deleteHost(id){ + console.log("Delete host", id); + this.HostsService.deleteHost(id).subscribe((res)=>{ + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Host Deleted Successfully.'}); + this.getAllHosts(); + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); + }); + } + + batchDeleteHosts(hosts){ + let arr=[], msg; + if(_.isArray(hosts)){ + hosts.forEach((item,index)=> { + arr.push(item.id); + }) + msg = "
Are you sure you want to delete the selected hosts?

[ "+ hosts.length +" Hosts ]

"; + }else{ + arr.push(hosts.id); + msg = "
Are you sure you want to delete the host?

[ "+ hosts.hostName +" ]

"; + } + + this.confirmationService.confirm({ + message: msg, + header: this.I18N.keyID['sds_block_host_delete'], + acceptLabel: this.I18N.keyID['sds_block_volume_delete'], + isWarning: true, + accept: ()=>{ + arr.forEach((item,index)=> { + this.deleteHost(item) + }) + + }, + reject:()=>{} + }) + + } + + modifyHost(host){ + // TODO: Check for Volume attachement and make appropriate modifications. + this.isVolumeAttached = false; + if(this.isVolumeAttached == false){ + this.router.navigate(['/modifyHost',host.id]); + } + } + + tablePaginate() { + this.selectedHosts = []; + } +} diff --git a/src/app/business/block/hosts.module.ts b/src/app/business/block/hosts.module.ts new file mode 100644 index 00000000..543bb4ee --- /dev/null +++ b/src/app/business/block/hosts.module.ts @@ -0,0 +1,26 @@ +import { NgModule, APP_INITIALIZER } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HostsComponent } from './hosts.component'; +import { ButtonModule, DataTableModule, InputTextModule, DropMenuModule, DialogModule,FormModule,MultiSelectModule, GrowlModule ,DropdownModule,InputTextareaModule} from '../../components/common/api'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { HttpService } from './../../shared/service/Http.service'; +import { VolumeService} from './volume.service'; +import { HostsService } from './hosts.service'; +import { AvailabilityZonesService } from './../resource/resource.service'; +import { ConfirmationService,ConfirmDialogModule} from '../../components/common/api'; +import { TooltipModule } from '../../components/tooltip/tooltip'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + declarations: [ HostsComponent ], + imports: [ CommonModule, ButtonModule, DataTableModule, InputTextModule, DropMenuModule, DialogModule,FormModule,MultiSelectModule, GrowlModule ,DropdownModule,ReactiveFormsModule,FormsModule,ConfirmDialogModule,InputTextareaModule, TooltipModule,RouterModule], + exports: [ HostsComponent ], + providers: [ + HttpService, + VolumeService, + HostsService, + ConfirmationService, + AvailabilityZonesService + ] +}) +export class HostsModule { } diff --git a/src/app/business/block/hosts.service.ts b/src/app/business/block/hosts.service.ts new file mode 100644 index 00000000..4c4eaaa2 --- /dev/null +++ b/src/app/business/block/hosts.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { I18NService, HttpService, ParamStorService } from '../../shared/api'; +import { Observable } from 'rxjs'; + +@Injectable() +export class HostsService { + constructor( + private http: HttpService, + private paramStor: ParamStorService + ) { } + + project_id = this.paramStor.CURRENT_TENANT().split("|")[1]; + hostsUrl = 'v1beta/{project_id}/host/hosts'; + //create host + createHost(param){ + let url = this.hostsUrl; + return this.http.post(url,param); + } + //get host + getHosts(): Observable { + return this.http.get(this.hostsUrl); + } + //delete host + deleteHost(hostId): Observable { + let url = this.hostsUrl + "/" + hostId + return this.http.delete(url); + } + //modify host + modifyHost(hostId,param): Observable { + let url = this.hostsUrl + "/" + hostId + return this.http.put(url,param); + } + //get host by id + getHostById(hostId): Observable { + let url = this.hostsUrl + "/" + hostId; + return this.http.get(url); + } + +} \ No newline at end of file diff --git a/src/app/business/block/modify-host/az-resolver.service.ts b/src/app/business/block/modify-host/az-resolver.service.ts new file mode 100644 index 00000000..d5c72ac1 --- /dev/null +++ b/src/app/business/block/modify-host/az-resolver.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { I18NService, HttpService, ParamStorService } from '../../../shared/api'; +import { Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable() +export class AzResolver implements Resolve { + allHosts; + constructor( + private http: HttpService, + private paramStor: ParamStorService + ) { } + + project_id = this.paramStor.CURRENT_TENANT().split("|")[1]; + azUrl = 'v1beta/{project_id}/availabilityZones'; + + resolve(): Observable { + let url = this.azUrl; + return this.http.get(url); + } +} \ No newline at end of file diff --git a/src/app/business/block/modify-host/host-resolver.service.ts b/src/app/business/block/modify-host/host-resolver.service.ts new file mode 100644 index 00000000..b4bde385 --- /dev/null +++ b/src/app/business/block/modify-host/host-resolver.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; +import { I18NService, HttpService, ParamStorService } from '../../../shared/api'; +import { Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable() +export class HostsResolver implements Resolve { + allHosts; + constructor( + private http: HttpService, + private paramStor: ParamStorService + ) { } + + project_id = this.paramStor.CURRENT_TENANT().split("|")[1]; + hostsUrl = 'v1beta/{project_id}/host/hosts'; + + resolve(route: ActivatedRouteSnapshot): Observable { + let id: any = route.params['hostId']; + let url = this.hostsUrl + "/" + id; + return this.http.get(url); + } +} \ No newline at end of file diff --git a/src/app/business/block/modify-host/modify-host.component.html b/src/app/business/block/modify-host/modify-host.component.html new file mode 100644 index 00000000..8d5f3a79 --- /dev/null +++ b/src/app/business/block/modify-host/modify-host.component.html @@ -0,0 +1,64 @@ + + +
+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+ + +
+ + +
+
+
\ No newline at end of file diff --git a/src/app/business/block/modify-host/modify-host.component.ts b/src/app/business/block/modify-host/modify-host.component.ts new file mode 100644 index 00000000..c9df8c92 --- /dev/null +++ b/src/app/business/block/modify-host/modify-host.component.ts @@ -0,0 +1,263 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { trigger, state, style, transition, animate } from '@angular/animations'; +import { Validators, FormControl, FormGroup, FormBuilder, FormArray } from '@angular/forms'; + +import { Message, SelectItem } from './../../../components/common/api'; +import { AvailabilityZonesService } from './../../resource/resource.service'; +import { HostsService} from './../hosts.service'; +import { I18NService,Utils } from '../../../../app/shared/api'; +import { Form } from '../../../components/form/form'; +import { Http, Headers, Response } from '@angular/http'; + +let _ = require("underscore"); + +@Component({ + selector: 'app-modify-host', + templateUrl: './modify-host.component.html', + styleUrls: [ + + ], + animations: [ + trigger('overlayState', [ + state('hidden', style({ + opacity: 0 + })), + state('visible', style({ + opacity: 1 + })), + transition('visible => hidden', animate('400ms ease-in')), + transition('hidden => visible', animate('400ms ease-out')) + ]), + + trigger('notificationTopbar', [ + state('hidden', style({ + height: '0', + opacity: 0 + })), + state('visible', style({ + height: '*', + opacity: 1 + })), + transition('visible => hidden', animate('400ms ease-in')), + transition('hidden => visible', animate('400ms ease-out')) + ]) + ] +}) +export class ModifyHostComponent implements OnInit { + + label = { + availabilityZones: this.i18n.keyID["sds_host_availability_zones"], + name: this.i18n.keyID["sds_block_volume_name"], + osType: this.i18n.keyID["sds_host_os_type"], + ip: this.i18n.keyID["sds_host_ip"], + accessMode: this.i18n.keyID["sds_host_access_mode"], + initiators: this.i18n.keyID["sds_host_initiators"] + }; + msgs: Message[]; + items; + hostId; + allHosts; + allAZs; + selectedHost; + host; + availabilityZonesOptions = []; + selectedAZs: string[] = []; + osTypeOptions; + selectedOs; + accessModeOptions; + selectedAccessMode; + protocolOptions; + selectedProtocol; + modifyHostform; + errorMessage = { + "availabilityZones": { required: "Atleast one Zone is required."}, + "accessMode": { required: "Access Mode is required."}, + "osType": { required: "OS Type is required."}, + "ip": { + required: "IP Address is required", + pattern: "Enter valid IP address" + }, + "hostName": { + required: "Host Name is required.", + maxlength: "Maximum 28 characters", + minlength: "Minimum 2 characters" + }, + "initiators": { + portName: { + required: "Port Name is required." + } + } + }; + validRule = { + 'validIp': '([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})' + }; + + constructor( + private ActivatedRoute: ActivatedRoute, + private router: Router, + private fb: FormBuilder, + private HostsService: HostsService, + private availabilityZonesService:AvailabilityZonesService, + public i18n:I18NService, + private http: Http + ) { + // Resolve the Host to modify + this.ActivatedRoute.data.subscribe((res:Response) => { + this.selectedHost = res['host'].json(); + }) + // Resolve the Availability Zones + this.ActivatedRoute.data.subscribe((azres:Response) => { + this.allAZs = azres['az'].json(); + let azArr = []; + if(this.allAZs && this.allAZs.length !== 0){ + this.allAZs.forEach(item =>{ + let obj = {label: item, value: item}; + azArr.push(obj); + }) + } + this.availabilityZonesOptions = azArr; + + }) + // Fetch the Selected Host ID + this.ActivatedRoute.params.subscribe((params) => { + this.hostId = params.hostId + }); + + } + + ngOnInit() { + this.osTypeOptions = [ + { + label: "Linux", + value: 'linux' + }, + { + label: "Windows", + value: 'windows', + }, + { + label: "ESXi", + value: 'esxi' + } + ]; + this.accessModeOptions = [ + { + label: "Agentless", + value: "agentless" + } + ]; + this.protocolOptions = [ + { + label: "iSCSI", + value: "iscsi" + }, + { + label: "SCSI", + value: "scsi" + }, + { + label: "FC", + value: "fibre_channel" + }, + { + label: "NVMe", + value: "nvme" + } + ] + + this.items = [ + { label: this.i18n.keyID["sds_Hosts_title"], url: '/block' }, + { label: this.i18n.keyID["sds_modify_host"], url: '/modifyHost' } + ]; + let self = this; + // Populate the OS type dropdown with selected value. + _.each(this.osTypeOptions, function(os){ + if(self.selectedHost['osType'] == os['value']){ + self.selectedOs = os['value']; + } + }); + // Populate the Access Mode dropdown with selected value. + _.each(this.accessModeOptions, function(ac){ + if(self.selectedHost['accessMode'] == ac['value']){ + self.selectedAccessMode = ac['value']; + } + }); + // Populate the Availability Zone dropdown with selected value. + _.each(this.selectedHost['availabilityZones'], function(az){ + self.availabilityZonesOptions.forEach(function(opt){ + if(az == opt['value']){ + self.selectedAZs.push(az); + } + }) + }); + this.modifyHostform = this.fb.group({ + 'availabilityZones': new FormControl('', Validators.required), + 'hostName': new FormControl(this.selectedHost['hostName'], {validators: [Validators.required, Validators.maxLength(28), Validators.minLength(2)]}), + 'accessMode': new FormControl('', Validators.required), + 'osType': new FormControl('', Validators.required), + 'ip': new FormControl(this.selectedHost['ip'], {validators:[Validators.required, Validators.pattern(this.validRule.validIp)]}), + 'initiators' : this.fb.array([]) + }); + + _.each(this.selectedHost['initiators'], function(item){ + self.protocolOptions.forEach(function(pr){ + if(item['protocol'] == pr['value']){ + item['selectedProtocol'] = item['protocol']; + } + + }); + let init = self.createInitiators(item); + (self.modifyHostform.controls['initiators'] as FormArray).push(init) + }); + } + + createInitiators(initiator?){ + if(initiator){ + console.log("Initiator:", initiator); + return this.fb.group({ + portName: new FormControl(initiator.portName, Validators.required), + protocol: new FormControl(initiator.selectedProtocol, Validators.required), + selectedProtocol: new FormControl(initiator.selectedProtocol) + }) + } else{ + return this.fb.group({ + portName: new FormControl('', Validators.required), + protocol: new FormControl('', Validators.required) + }) + } + } + addNext() { + (this.modifyHostform.controls['initiators'] as FormArray).push(this.createInitiators()) + } + removeLink(i: number) { + if(this.modifyHostform.controls['initiators'].length > 1){ + this.modifyHostform.controls['initiators'].removeAt(i); + } + } + onSubmit(){ + if(this.modifyHostform.valid){ + let param = { + "accessMode" : this.modifyHostform.value.accessMode, + "hostName" : this.modifyHostform.value.hostName, + "osType" : this.modifyHostform.value.osType, + "ip" : this.modifyHostform.value.ip, + "availabilityZones" : this.modifyHostform.value.availabilityZones, + "initiators" : this.modifyHostform.value.initiators + } + this.modifyHost(this.selectedHost.id, param); + } + + } + modifyHost(id, param){ + this.HostsService.modifyHost(id, param).subscribe((res) => { + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Host Modified Successfully.'}); + this.router.navigate(['/block',"fromHosts"]); + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.message}); + }); + } + +} diff --git a/src/app/business/block/modify-host/modify-host.module.ts b/src/app/business/block/modify-host/modify-host.module.ts new file mode 100644 index 00000000..8bd49ea8 --- /dev/null +++ b/src/app/business/block/modify-host/modify-host.module.ts @@ -0,0 +1,47 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; + +import { ModifyHostComponent } from './modify-host.component'; + +import { HttpService } from './../../../shared/api'; +import { VolumeService } from './../volume.service'; +import { ProfileService } from './../../profile/profile.service'; +import { AvailabilityZonesService } from './../../resource/resource.service'; +import { HostsService } from '../hosts.service'; +import { InputTextModule, CheckboxModule, ButtonModule, DropdownModule, MultiSelectModule, DialogModule, Message, GrowlModule, SpinnerModule, FormModule } from './../../../components/common/api'; + +let routers = [{ + path: '', + component: ModifyHostComponent +}] + +@NgModule({ + imports: [ + RouterModule.forChild(routers), + ReactiveFormsModule, + FormsModule, + CommonModule, + InputTextModule, + CheckboxModule, + ButtonModule, + DropdownModule, + MultiSelectModule, + DialogModule, + GrowlModule, + SpinnerModule, + FormModule + ], + declarations: [ + ModifyHostComponent + ], + providers: [ + HttpService, + VolumeService, + ProfileService, + AvailabilityZonesService, + HostsService + ] +}) +export class ModifyHostModule { } diff --git a/src/app/business/block/volume-detail/attachment-list/attachment-list.component.html b/src/app/business/block/volume-detail/attachment-list/attachment-list.component.html new file mode 100644 index 00000000..51e45ecb --- /dev/null +++ b/src/app/business/block/volume-detail/attachment-list/attachment-list.component.html @@ -0,0 +1,62 @@ + +
+
+
+ +
+
+
+ + +
+ +
+
+ +
+
{{item | json}}
+
+ + + {{host.hostName}} + + + + + + {{host.port? host.port : '-'}} + + + + + + + {{host.availabilityZones[0]}} + ... + + + + + {{this.I18N.keyID['sds_block_volume_detach_host']}} + + + +
+
+ + + +
+ + {{selectedVolume.name}} + + + {{selectedHost.hostName}} + +
+ + + + +
+ \ No newline at end of file diff --git a/src/app/business/block/volume-detail/attachment-list/attachment-list.component.ts b/src/app/business/block/volume-detail/attachment-list/attachment-list.component.ts new file mode 100644 index 00000000..59c5153e --- /dev/null +++ b/src/app/business/block/volume-detail/attachment-list/attachment-list.component.ts @@ -0,0 +1,131 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { FormControl, FormGroup, FormBuilder, Validators, ValidatorFn, AbstractControl } from '@angular/forms'; +import { VolumeService,SnapshotService } from '../../volume.service'; +import { ConfirmationService,ConfirmDialogModule} from '../../../../components/common/api'; +import { I18NService, MsgBoxService, Utils } from '../../../../../app/shared/api'; +import { Message} from '../../../../components/common/api'; +import { ProfileService } from '../../../profile/profile.service'; +import { HostsService } from '../../hosts.service'; +import { AttachService } from '../../attach.service'; + +let _ = require("underscore"); +@Component({ + selector: 'app-attachment-list', + templateUrl: './attachment-list.component.html', + providers: [ConfirmationService], + styleUrls: [ + + ] +}) +export class AttachmentListComponent implements OnInit { + + @Input() volumeId; + msgs: Message[]; + allHosts = []; + attachedHosts = []; + attachedHostsTableData = []; + allVolumes = []; + selectedHost; + detachDisplay: boolean = false; + detachHostFormGroup; + selectedVolume; + + constructor( + private VolumeService: VolumeService, + private HostsService: HostsService, + private attach: AttachService, + private fb: FormBuilder, + private confirmationService:ConfirmationService, + public I18N:I18NService, + private msg: MsgBoxService, + private ProfileService:ProfileService + ) { + let self = this; + this.detachHostFormGroup = this.fb.group({ + "attachmentId" : ['', Validators.required] + }) + this.getAllAttachedHosts(); + this.getVolumes(); + _.each(self.allVolumes, function(item){ + if(item['id'] == self.volumeId){ + self.selectedVolume = item; + } + }) + } + + ngOnInit() { + let self =this; + + } + + + + getAllAttachedHosts(){ + let self =this; + let tableData = []; + self.attachedHostsTableData = []; + this.attach.getAttachments().subscribe((res) => { + this.attachedHosts = res.json(); + if(this.attachedHosts.length > 0){ + let self = this; + this.HostsService.getHosts().subscribe((res) => { + this.allHosts = res.json(); + if(this.allHosts && this.allHosts.length){ + _.each(self.attachedHosts, function(atItem){ + _.each(self.allHosts, function(hostItem){ + if(atItem['hostId'] == hostItem['id'] && atItem['volumeId'] == self.volumeId){ + hostItem['attachmentId'] = atItem['id']; + tableData.push(hostItem); + } + }); + }); + } + this.attachedHostsTableData = tableData; + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.message}); + }) + + } + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); + }) + +} + + + showDetach(host){ + let self = this; + this.detachDisplay = true; + _.each(self.allVolumes, function(item){ + if(item['id'] == self.volumeId){ + self.selectedVolume = item; + } + }) + this.selectedHost = host; + } + + detachHost(){ + this.attach.deleteAttachment(this.selectedHost.attachmentId).subscribe((res) =>{ + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Host Detached Successfully.'}); + this.detachDisplay = false; + this.getAllAttachedHosts(); + }, (error) => { + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); + }) +} + getVolumes() { + this.VolumeService.getVolumes().subscribe((res) => { + let volumes = res.json(); + this.allVolumes = volumes; + }, (error) => { + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.message}); + }); +} + + +} diff --git a/src/app/business/block/volume-detail/snapshot-list/snapshot-list.component.ts b/src/app/business/block/volume-detail/snapshot-list/snapshot-list.component.ts index 0997235f..3dffdbe2 100644 --- a/src/app/business/block/volume-detail/snapshot-list/snapshot-list.component.ts +++ b/src/app/business/block/volume-detail/snapshot-list/snapshot-list.component.ts @@ -203,7 +203,11 @@ export class SnapshotListComponent implements OnInit { showSnapshotPropertyDialog(method,selectedSnapshot?){ this.snapshotPropertyDisplay = true; - this.snapshotFormGroup.reset(); + this.snapshotFormGroup.reset({ + name: "", + profile : "", + description: "" + }); if(method === 'create'){ this.isCreate = true; this.isModify = false; diff --git a/src/app/business/block/volume-detail/volume-detail.component.html b/src/app/business/block/volume-detail/volume-detail.component.html index 632093a1..8213ca62 100644 --- a/src/app/business/block/volume-detail/volume-detail.component.html +++ b/src/app/business/block/volume-detail/volume-detail.component.html @@ -59,6 +59,11 @@

{{i18n.keyID['sds_block_volume_base_info']}}

+ + + + +
diff --git a/src/app/business/block/volume-detail/volume-detail.module.ts b/src/app/business/block/volume-detail/volume-detail.module.ts index 2b1e7b26..dadc2d8a 100644 --- a/src/app/business/block/volume-detail/volume-detail.module.ts +++ b/src/app/business/block/volume-detail/volume-detail.module.ts @@ -3,13 +3,17 @@ import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { VolumeDetailComponent } from './volume-detail.component'; - -import { TabViewModule,ButtonModule, DataTableModule,DropdownModule, DropMenuModule, DialogModule, FormModule, InputTextModule, InputTextareaModule, ConfirmDialogModule ,ConfirmationService,CheckboxModule} from './../../../components/common/api'; +import { DeferModule } from '../../../components/defer/defer'; +import { TabViewModule,ButtonModule, DataTableModule,DropdownModule, DropMenuModule, DialogModule, FormModule, GrowlModule, InputTextModule, InputTextareaModule, ConfirmDialogModule ,ConfirmationService,CheckboxModule} from './../../../components/common/api'; +import { TooltipModule } from '../../../components/tooltip/tooltip'; import { HttpService } from './../../../shared/service/Http.service'; import { VolumeService,SnapshotService ,ReplicationService} from './../volume.service'; import { SnapshotListComponent } from './snapshot-list/snapshot-list.component'; +import { AttachmentListComponent } from './attachment-list/attachment-list.component'; import { ReplicationListComponent } from './replication-list/replication-list.component'; import { ProfileService } from './../../profile/profile.service'; +import { HostsService } from '../hosts.service'; +import { AttachService } from '../attach.service'; let routers = [{ path: '', @@ -29,13 +33,18 @@ let routers = [{ DataTableModule, DialogModule, FormModule, + GrowlModule, ConfirmDialogModule, DropdownModule, - CheckboxModule + CheckboxModule, + TooltipModule, + DropMenuModule, + DeferModule ], declarations: [ VolumeDetailComponent, SnapshotListComponent, + AttachmentListComponent, ReplicationListComponent ], providers: [ @@ -44,7 +53,9 @@ let routers = [{ SnapshotService, ConfirmationService, ProfileService, - ReplicationService + ReplicationService, + HostsService, + AttachService ] }) export class VolumeDetailModule { } diff --git a/src/app/business/block/volumeGroup.component.ts b/src/app/business/block/volumeGroup.component.ts index abde0e95..55fcd627 100644 --- a/src/app/business/block/volumeGroup.component.ts +++ b/src/app/business/block/volumeGroup.component.ts @@ -86,14 +86,22 @@ export class VolumeGroupComponent implements OnInit{ } //show create volumes group createVolumeGroup(){ - this.volumeGroupForm.reset(); + this.volumeGroupForm.reset({ + group_name : "", + description : "", + profile : "", + zone : "" + }); this.showVolumeGroupDialog = true; } ModifyVolumeGroupDisplay(volumeGroup){ - this.modifyGroupForm.reset(); + this.modifyGroupForm.reset({ + group_name : "", + description : "" + }); this.currentGroup = volumeGroup; this.modifyGroupForm.controls['group_name'].setValue(this.currentGroup.name); - this.modifyGroupForm.controls['description'].setValue(""); + this.modifyGroupForm.controls['description'].setValue(this.currentGroup.description); this.showModifyGroup = true; } submit(group){ diff --git a/src/app/business/block/volumeList.component.ts b/src/app/business/block/volumeList.component.ts index b7f94e08..feedfed5 100644 --- a/src/app/business/block/volumeList.component.ts +++ b/src/app/business/block/volumeList.component.ts @@ -5,10 +5,12 @@ import { FormControl, FormGroup, FormBuilder, Validators, ValidatorFn, AbstractC import { AppService } from 'app/app.service'; import { I18nPluralPipe } from '@angular/common'; import { trigger, state, style, transition, animate } from '@angular/animations'; -import { MenuItem ,ConfirmationService} from '../../components/common/api'; +import { MenuItem ,ConfirmationService, Message} from '../../components/common/api'; import { VolumeService, SnapshotService,ReplicationService} from './volume.service'; import { ProfileService } from './../profile/profile.service'; +import { HostsService } from './hosts.service'; +import { AttachService } from './attach.service'; import { identifierModuleUrl } from '@angular/compiler'; let _ = require("underscore"); @@ -24,6 +26,10 @@ export class VolumeListComponent implements OnInit { createReplicationDisplay = false; expandDisplay = false; modifyDisplay = false; + attachDisplay: boolean = false; + detachDisplay: boolean = false; + noHosts: boolean = false; + noAttachments: boolean = false; selectVolumeSize; newVolumeSize; newVolumeSizeFormat; @@ -40,6 +46,16 @@ export class VolumeListComponent implements OnInit { } ]; + attachModeOptions = [ + { + label: "RO", + value: "ro" + }, + { + label: "RW", + value: "rw" + } + ] profileOptions = []; snapProfileOptions = []; azOption=[{label:"Secondary",value:"secondary"}]; @@ -56,6 +72,8 @@ export class VolumeListComponent implements OnInit { snapshotFormGroup; modifyFormGroup; expandFormGroup; + attachHostFormGroup; + detachHostFormGroup; replicationGroup; errorMessage = { "name": { required: "Name is required." }, @@ -65,10 +83,17 @@ export class VolumeListComponent implements OnInit { "expandSize":{ required: "Expand Capacity is required.", pattern: "Expand Capacity can only be number" - } + }, + "hostId" : {required : "Host is required"}, + "attachmentId" : {required: "Attachment is required"} }; profiles; selectedVolume; + allHosts; + attachedHosts; + attachedHostOptions = []; + hostOptions = []; + msgs: Message[]; constructor( public I18N: I18NService, @@ -76,6 +101,8 @@ export class VolumeListComponent implements OnInit { private VolumeService: VolumeService, private SnapshotService: SnapshotService, private ProfileService: ProfileService, + private HostsService: HostsService, + private attach: AttachService, private ReplicationService: ReplicationService, private confirmationService: ConfirmationService, private fb: FormBuilder @@ -92,6 +119,13 @@ export class VolumeListComponent implements OnInit { "expandSize":[1,{validators:[Validators.required,Validators.pattern(/^\d+$/)], updateOn:'change'} ], "capacityOption":[this.capacityOptions[0] ] }); + this.attachHostFormGroup = this.fb.group({ + "hostId" : ['', Validators.required], + "attachMode" : [''] + }) + this.detachHostFormGroup = this.fb.group({ + "attachmentId" : ['', Validators.required] + }) this.expandFormGroup.get("expandSize").valueChanges.subscribe( (value:string)=>{ this.newVolumeSize = parseInt(this.selectedVolume.size) + parseInt(value)*this.unit; @@ -122,6 +156,22 @@ export class VolumeListComponent implements OnInit { }, disabled:false }, + { + "label": this.I18N.keyID['sds_block_volume_attach_host'], + command: () => { + this.showAttach(); + }, + disabled:false + }, + { + "label": this.I18N.keyID['sds_block_volume_detach_host'], + command: () => { + if (this.selectedVolume && this.selectedVolume.id) { + this.showDetach(this.selectedVolume); + } + }, + disabled:false + }, { "label": this.I18N.keyID['sds_block_volume_expand'], command: () => { @@ -150,6 +200,22 @@ export class VolumeListComponent implements OnInit { }, disabled:false }, + { + "label": this.I18N.keyID['sds_block_volume_attach_host'], + command: () => { + this.showAttach(); + }, + disabled:false + }, + { + "label": this.I18N.keyID['sds_block_volume_detach_host'], + command: () => { + if (this.selectedVolume && this.selectedVolume.id) { + this.showDetach(this.selectedVolume); + } + }, + disabled:false + }, { "label": this.I18N.keyID['sds_block_volume_expand'], command: () => { @@ -174,6 +240,97 @@ export class VolumeListComponent implements OnInit { this.getProfiles() } + showAttach(){ + let self = this; + this.attachDisplay = true; + this.hostOptions = []; + this.attachHostFormGroup.reset(); + this.getAllHosts(); + } + + attachHost(){ + let params = { + "volumeId" : this.selectedVolume.id, + "hostId" : this.attachHostFormGroup.value.hostId, + "attachMode" : this.attachHostFormGroup.value.attachMode + } + this.attach.createAttachment(params).subscribe((res) =>{ + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Host Attached Successfully.'}); + this.attachDisplay = false; + this.getProfiles(); + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); + this.attachDisplay = false; + }) + } + getAllHosts(){ + let self =this; + this.HostsService.getHosts().subscribe((res) => { + this.allHosts = res.json(); + if(this.allHosts.length == 0){ + this.noHosts = true; + } + _.each(this.allHosts, function(item){ + let option = { + label: item['hostName'] + ' ' + '(' + item['ip'] + ')', + value: item['id'] + } + self.hostOptions.push(option); + }) + }, (error) =>{ + console.log("No hosts.", error.json().message); + }) + } + showDetach(selectedVolume){ + this.getAllAttachedHosts(); + + } + + detachHost(){ + this.attach.deleteAttachment(this.detachHostFormGroup.value.attachmentId).subscribe((res) =>{ + this.msgs = []; + this.msgs.push({severity: 'success', summary: 'Success', detail: 'Host Detached Successfully.'}); + this.detachDisplay = false; + this.getProfiles(); + }, (error) => { + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); + }) + } + getAllAttachedHosts(){ + let self =this; + this.attachedHostOptions = []; + this.getAllHosts(); + this.attach.getAttachments().subscribe((res) => { + this.attachedHosts = res.json(); + if(this.attachedHosts.length > 0 && _.where(this.attachedHosts, {volumeId: self.selectedVolume.id}).length > 0){ + let self = this; + this.detachDisplay = true; + this.noAttachments = false; + this.detachHostFormGroup.reset(); + _.each(self.attachedHosts, function(atItem){ + _.each(self.allHosts, function(hostItem){ + if(self.selectedVolume.id == atItem['volumeId'] && atItem['hostId'] == hostItem['id']){ + let option = { + label: hostItem['hostName'] + ' ' + '(' + hostItem['ip'] + ')', + value: atItem['id'] + } + self.attachedHostOptions.push(option); + } + }) + }) + } else{ + this.noAttachments = true; + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: 'This volume does not have any attachments.'}); + } + }, (error) =>{ + console.log("Attachments not found", error); + }) + + } getVolumes() { this.selectedVolumes = []; this.VolumeService.getVolumes().subscribe((res) => { @@ -237,19 +394,14 @@ export class VolumeListComponent implements OnInit { }); } - batchDeleteVolume() { - this.selectedVolumes.forEach(volume => { - this.deleteVolume(volume.id); - }); - } - - deleteVolumeById(id) { - this.deleteVolume(id); - } + deleteVolume(id) { this.VolumeService.deleteVolume(id).subscribe((res) => { this.getVolumes(); + }, (error) =>{ + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error.json().message}); }); } @@ -276,7 +428,11 @@ export class VolumeListComponent implements OnInit { returnSelectedVolume(selectedVolume, dialog) { if (dialog === 'snapshot') { - this.snapshotFormGroup.reset(); + this.snapshotFormGroup.reset({ + name: "", + profile : "", + description: "" + }); this.createSnapshotDisplay = true; } else if (dialog === 'replication') { this.createReplicationDisplay = true; diff --git a/src/app/business/block/volumeList.html b/src/app/business/block/volumeList.html index 4b2985f5..f48d5ef3 100644 --- a/src/app/business/block/volumeList.html +++ b/src/app/business/block/volumeList.html @@ -1,3 +1,4 @@ +
@@ -13,16 +14,16 @@
- + {{volume.name}} - - - + + + - + {{I18N.keyID['sds_block_volume_createsna']}} {{I18N.keyID['sds_block_volume_createrep']}} @@ -91,8 +92,48 @@ + + + +
+ + {{selectedVolume.name}} + + + +
+ + There are no hosts. Would you like to add one? + Add Host +
+ +
+ + + +
+ + + + +
+ + +
+ + {{selectedVolume.name}} + + + + +
+ + + + +
- +
diff --git a/src/app/business/block/volumeList.module.ts b/src/app/business/block/volumeList.module.ts index da627be7..ad8adc85 100644 --- a/src/app/business/block/volumeList.module.ts +++ b/src/app/business/block/volumeList.module.ts @@ -2,11 +2,12 @@ import { NgModule, APP_INITIALIZER } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { VolumeListComponent } from './volumeList.component'; -import { ButtonModule, DataTableModule, DropMenuModule, DialogModule, FormModule, InputTextModule, InputTextareaModule, DropdownModule ,ConfirmationService,ConfirmDialogModule} from '../../components/common/api'; +import { ButtonModule, DataTableModule, DropMenuModule, DialogModule, FormModule, InputTextModule, InputTextareaModule, GrowlModule, DropdownModule ,ConfirmationService,ConfirmDialogModule} from '../../components/common/api'; import { HttpService } from './../../shared/service/Http.service'; import {VolumeService, SnapshotService, ReplicationService} from './volume.service'; import { ProfileService } from './../profile/profile.service'; +import { AttachService } from './attach.service'; import { RouterModule } from '@angular/router'; @NgModule({ @@ -18,6 +19,7 @@ import { RouterModule } from '@angular/router'; ButtonModule, InputTextModule, InputTextareaModule, + GrowlModule, DataTableModule, DropdownModule, DropMenuModule, @@ -32,6 +34,7 @@ import { RouterModule } from '@angular/router'; VolumeService, SnapshotService, ProfileService, + AttachService, ReplicationService, ConfirmationService ] diff --git a/src/app/business/home/home.component.ts b/src/app/business/home/home.component.ts index 7d7622e2..13441fba 100644 --- a/src/app/business/home/home.component.ts +++ b/src/app/business/home/home.component.ts @@ -224,10 +224,21 @@ export class HomeComponent implements OnInit { return initArray; },[]); this.Allbackends = result; + this.allBackends_count.localBKD = 0; this.allBackends_count.aws = this.Allbackends[this.cloud_type[0]] ? this.Allbackends[Consts.CLOUD_TYPE[0]].length :0; this.allBackends_count.huaweipri = this.Allbackends[this.cloud_type[1]] ? this.Allbackends[Consts.CLOUD_TYPE[1]].length :0; this.allBackends_count.huaweipub = this.Allbackends[this.cloud_type[2]] ? this.Allbackends[Consts.CLOUD_TYPE[2]].length :0; - this.allBackends_count.localBKD = this.Allbackends[this.cloud_type[3]] ? this.Allbackends[Consts.CLOUD_TYPE[3]].length :0 + this.Allbackends[this.cloud_type[4]] ? this.Allbackends[Consts.CLOUD_TYPE[4]].length :0; + if( this.Allbackends[this.cloud_type[3]]){ + this.allBackends_count.localBKD += this.Allbackends[this.cloud_type[3]].length; + } + if( this.Allbackends[this.cloud_type[4]]){ + this.allBackends_count.localBKD += this.Allbackends[this.cloud_type[4]].length; + } + if( this.Allbackends[this.cloud_type[7]]){ + this.allBackends_count.localBKD += this.Allbackends[this.cloud_type[7]].length; + } + + this.allBackends_count.ibmcos = this.Allbackends[this.cloud_type[5]] ? this.Allbackends[Consts.CLOUD_TYPE[5]].length :0; this.allBackends_count.gcp = this.Allbackends[this.cloud_type[6]] ? this.Allbackends[Consts.CLOUD_TYPE[6]].length :0; } @@ -280,7 +291,8 @@ export class HomeComponent implements OnInit { this.selectedType = null; let fs_arr = this.Allbackends['fusionstorage-object'] ? this.Allbackends['fusionstorage-object'] : []; let ceph_arr = this.Allbackends['ceph-s3'] ? this.Allbackends['ceph-s3'] : []; - this.typeDetail = fs_arr.concat(ceph_arr); + let yig_arr = this.Allbackends['yig'] ? this.Allbackends['yig'] : []; + this.typeDetail = fs_arr.concat(ceph_arr,yig_arr); } } diff --git a/src/app/business/profile/createProfile/createProfile.component.html b/src/app/business/profile/createProfile/createProfile.component.html index 13d817ba..3c655eca 100644 --- a/src/app/business/profile/createProfile/createProfile.component.html +++ b/src/app/business/profile/createProfile/createProfile.component.html @@ -42,19 +42,43 @@

{{I18N.keyID['sds_profile_create_title']}}

-
- -
- - {{I18N.keyID['sds_profile_IOPS_unit']}} -
-
- -
- - {{I18N.keyID['sds_profile_BWS_unit']}} -
-
+ +
+ +
+ + {{I18N.keyID['sds_profile_IOPS_unit']}} +
+
+ +
+ + {{I18N.keyID['sds_profile_BWS_unit']}} +
+
+ +
+ + {{I18N.keyID['sds_profile_latency_unit']}} +
+
+
+
+ +
+ + {{I18N.keyID['sds_profile_IOPS_unit']}} +
+
+ +
+ + {{I18N.keyID['sds_profile_BWS_unit']}} +
+
+
+ +
diff --git a/src/app/business/profile/createProfile/createProfile.component.ts b/src/app/business/profile/createProfile/createProfile.component.ts index 41edf32b..f27851ae 100644 --- a/src/app/business/profile/createProfile/createProfile.component.ts +++ b/src/app/business/profile/createProfile/createProfile.component.ts @@ -75,15 +75,15 @@ export class CreateProfileComponent implements OnInit { protocolOptions = [ { label: 'iSCSI', - value: 'iSCSI' + value: 'iscsi' }, { label: 'FC', - value: 'FC' + value: 'fibre_channel' }, { label: 'RBD', - value: 'RBD' + value: 'rbd' }, { label: 'nvmeof', @@ -326,7 +326,10 @@ export class CreateProfileComponent implements OnInit { key: this.I18N.keyID['sds_profile_extra_key'], value: this.I18N.keyID['sds_profile_extra_value'], maxIOPS: this.I18N.keyID['sds_profile_create_maxIOPS'], + minIOPS: this.I18N.keyID['sds_profile_create_minIOPS'], MBPS: this.I18N.keyID['sds_profile_create_maxBWS'], + minBWS: this.I18N.keyID['sds_profile_create_minBWS'], + latency: this.I18N.keyID['sds_profile_create_latency'], replicationLabel: { type:this.I18N.keyID['sds_profile_rep_type'] , RGO: this.I18N.keyID['sds_profile_rep_rgo'], @@ -349,7 +352,7 @@ export class CreateProfileComponent implements OnInit { this.profileform = this.fb.group({ 'name': new FormControl('', {validators:[Validators.required,Utils.isExisted(this.allProfileNameForCheck)]}), 'description':new FormControl('',Validators.maxLength(200)), - 'protocol': new FormControl('iSCSI'), + 'protocol': new FormControl('iscsi'), 'storageType': new FormControl('Thin'), 'policys': new FormControl(''), 'snapshotRetention': new FormControl('Time'), @@ -358,7 +361,10 @@ export class CreateProfileComponent implements OnInit { }); this.qosPolicy = this.fb.group({ "maxIOPS": new FormControl(6000, Validators.required), + "minIOPS": new FormControl(500), "maxBWS" : new FormControl(100, Validators.required), + "minBWS" : new FormControl(1), + "latency" : new FormControl(5), }); this.repPolicy = this.fb.group({ "repType": new FormControl("mirror", Validators.required), @@ -426,18 +432,18 @@ export class CreateProfileComponent implements OnInit { this.protocolOptions = [ { label: 'iSCSI', - value: 'iSCSI' + value: 'iscsi' }, { label: 'FC', - value: 'FC' + value: 'fibre_channel' }, { label: 'RBD', - value: 'RBD' + value: 'rbd' } ] - this.profileform.patchValue({protocol: 'iSCSI'}) + this.profileform.patchValue({protocol: 'iscsi'}) this.isReplicationFlag = true; } } @@ -476,7 +482,10 @@ export class CreateProfileComponent implements OnInit { this.param["provisioningProperties"]= { "ioConnectivity": { "maxIOPS": Number(this.qosPolicy.value.maxIOPS), + "minIOPS": Number(this.qosPolicy.value.minIOPS), "maxBWS": Number(this.qosPolicy.value.maxBWS), + "minBWS": Number(this.qosPolicy.value.minBWS), + "latency": Number(this.qosPolicy.value.latency), "accessProtocol": value.protocol == "FC" ? "fibre_channel" : value.protocol }, "dataStorage": { diff --git a/src/app/business/services/dynamic-form/dynamic-form.component.html b/src/app/business/services/dynamic-form/dynamic-form.component.html index 697d3f95..41d3547a 100644 --- a/src/app/business/services/dynamic-form/dynamic-form.component.html +++ b/src/app/business/services/dynamic-form/dynamic-form.component.html @@ -1,5 +1,5 @@ -
+

Instance Details

@@ -53,7 +53,12 @@

Parameters

- + +
+ + There are no hosts.Would you like to add a host? + Add Host +
diff --git a/src/app/business/services/dynamic-form/dynamic-form.component.ts b/src/app/business/services/dynamic-form/dynamic-form.component.ts index 8b1f5d2d..f23fa27e 100644 --- a/src/app/business/services/dynamic-form/dynamic-form.component.ts +++ b/src/app/business/services/dynamic-form/dynamic-form.component.ts @@ -5,6 +5,7 @@ import { I18NService, MsgBoxService, Utils, ParamStorService } from '../../../sh import { Validators, FormControl, FormGroup, FormBuilder } from '@angular/forms'; import { WorkflowService } from '../workflow.service'; import { ProfileService } from '../../profile/profile.service'; +import { HostsService } from '../../block/hosts.service'; import { trigger, state, style, transition, animate } from '@angular/animations'; import { CreateClusterComponent } from '../create-cluster/create-cluster.component' import * as _ from "underscore"; @@ -23,6 +24,9 @@ export class DynamicFormComponent implements OnInit { msgs: Message[]; profileOptions: any[] = []; + allHosts; + noHosts: boolean = false; + hostOptions: any[] = []; objectProps: any; displayFormObject: any[]; form: FormGroup; @@ -56,6 +60,7 @@ export class DynamicFormComponent implements OnInit { public i18n: I18NService, private wfservice: WorkflowService, private profileService: ProfileService, + private HostsService: HostsService, private paramStor: ParamStorService, private fb: FormBuilder) { this.default_parameters['auth_token'] = localStorage['auth-token']; @@ -66,6 +71,7 @@ export class DynamicFormComponent implements OnInit { ngOnInit() { let self = this; self.getProfiles(); + self.getAllHosts(); this.dataObject['instanceName'] = { "description": "Name of the Instance", "required": true, @@ -107,12 +113,12 @@ export class DynamicFormComponent implements OnInit { item['showThis'] = false; } /* Adding host info object for volume provisioning. */ - if(item['key'] == 'host_info'){ - item['label'] = "Host IP"; - item['showThis'] = true; - item['validation'] = {required: true}; - item['required'] = true; - item['type'] = 'string'; + if(item['key'] == 'host_id'){ + item['label'] = "Host"; + item['inputType'] = "select"; + item['options'] = self.hostOptions; + + formGroup[item['key']] = new FormControl(item['value'] || '', self.mapValidators(item['validation'])); } if(item['key'] == 'port' || item['key']=='analysis_args'){ @@ -145,18 +151,29 @@ export class DynamicFormComponent implements OnInit { formGroup[item['key']] = new FormControl(item['value'] || '', self.mapValidators(item['validation'])); } + switch (item['type']) { + case "string": item['inputType'] = "text"; + if((item['key'] == 'profile_id') || (item['key'] == 'host_id')){ + item['inputType'] = "select"; + } + + break; + case "boolean": item['inputType'] = "radio"; + item['options'] = [ + { label: "True", value: 'true'}, + { label: "False", value: 'false'} + ]; + + break; + case "integer": item['inputType'] = "number"; + + break; - if(item['type'] == "string" && item['key'] != 'profile_id'){ - item['inputType'] = "text"; - } else if(item['type'] == "boolean"){ - item['inputType'] = "radio"; - item['options'] = [ - { label: "True", value: 'true'}, - { label: "False", value: 'false'} - ]; - } else if(item['type'] == "integer"){ - item['inputType'] = "number"; + default: + break; } + + if(item['required']==true){ formGroup[item['key']] = new FormControl(item['value'] || '', self.mapValidators(item['validation'])); } @@ -170,7 +187,30 @@ export class DynamicFormComponent implements OnInit { } } - + getAllHosts(){ + let self = this; + this.HostsService.getHosts().subscribe((res) => { + this.allHosts = res.json(); + if(this.allHosts.length == 0){ + this.noHosts = true; + } + if(this.allHosts && this.allHosts.length){ + _.each(self.allHosts, function(item){ + let option = { + label: item['hostName'] + ' ' + '(' + item['ip'] + ')', + value: item['id'] + } + self.hostOptions.push(option); + }) + } + console.log("Host Options", self.hostOptions); + }, (error) =>{ + console.log("Something went wrong. Could not fetch hosts.", error); + this.msgs = []; + this.msgs.push({severity: 'error', summary: 'Error', detail: error._body}); + }) + + } mapValidators(validators) { const formValidators = []; @@ -206,12 +246,6 @@ export class DynamicFormComponent implements OnInit { onSubmit(value) { let formObject = value; - if(_.has(formObject, 'host_info')){ - let val = { - "ip" : formObject['host_info'] - } - formObject['host_info'] = val; - } this.requestBody.service_id = this.serviceId; this.requestBody.action = this.selectedService.action; this.requestBody.user_id = this.default_parameters['user_id']; diff --git a/src/app/business/services/services.module.ts b/src/app/business/services/services.module.ts index 710ce139..8d16a298 100644 --- a/src/app/business/services/services.module.ts +++ b/src/app/business/services/services.module.ts @@ -19,6 +19,7 @@ import { CreateInstanceComponent } from './create-instance/create-instance.compo import { CreateClusterComponent } from './create-cluster/create-cluster.component'; import { WorkflowService } from './workflow.service'; import { ProfileService } from '../profile/profile.service'; +import { HostsService } from '../block/hosts.service'; import { HttpClientModule } from '@angular/common/http'; import { DynamicFormComponent } from './dynamic-form/dynamic-form.component'; @@ -57,6 +58,6 @@ import { DynamicFormComponent } from './dynamic-form/dynamic-form.component'; HttpClientModule, SpinnerModule], exports: [ServicesRoutingModule], - providers: [WorkflowService, ProfileService, ConfirmationService] + providers: [WorkflowService, ProfileService, HostsService, ConfirmationService] }) export class ServicesModule { } \ No newline at end of file diff --git a/src/app/i18n/en/keyID.json b/src/app/i18n/en/keyID.json index de42c4fc..6a3765a7 100644 --- a/src/app/i18n/en/keyID.json +++ b/src/app/i18n/en/keyID.json @@ -6,6 +6,7 @@ "sds_block_volumes_descrip":"View and manage Volumes that have been manually created or applied for through service templates.", "sds_block_volumesGroup_descrip":"View and manage Volume Groups that have been manually created or applied for through service templates.", "sds_block_fileShare_descrip":"View and manage File Shares that have been manually created or applied for through service templates.", + "sds_block_hosts_descrip":"View and manage Hosts.", "sds_home_tenants":"Tenants", "sds_home_users":"Users", "sds_home_volumes":"Volumes", @@ -31,10 +32,15 @@ "sds_block_volume_createrep":"Create Replication", "sds_block_volume_more":"More", "sds_block_volume_modify":"Modify", + "sds_block_volume_attach_host":"Attach Host", + "sds_block_volume_detach_host":"Detach Host", "sds_block_volume_expand":"Expand", "sds_block_volume_delete":"Delete", "sds_block_volume_create":"Create", "sds_block_volume_search":"search", + "sds_block_volume_attach":"Attach", + "sds_block_volume_detach":"Detach", + "sds_block_volume_attachments":"Attachments", "sds_block_volume_createVol":"Create Volume", "sds_block_volume_createVol_desc":"Apply for block storage resources.", "sds_block_volume_quantity":"Quantity", @@ -69,8 +75,11 @@ "sds_profile_avai_pool":"Available Storage Pool", "sds_profile_extra_key":"Key", "sds_profile_extra_value":"Value", - "sds_profile_create_maxIOPS":"MaxIOPS", - "sds_profile_create_maxBWS":"MaxBWS", + "sds_profile_create_maxIOPS":"Maximum IOPS", + "sds_profile_create_minIOPS":"Minimum IOPS", + "sds_profile_create_maxBWS":"Maximum BWS", + "sds_profile_create_minBWS":"Minimum BWS", + "sds_profile_create_latency":"Latency", "sds_profile_rep_type":"Type", "sds_profile_rep_rgo":"RGO", "sds_profile_rep_mode":"Mode", @@ -86,6 +95,7 @@ "sds_profile_snap_dest":"Destination", "sds_profile_IOPS_unit":"IOPS/TB", "sds_profile_BWS_unit":"MBPS/TB", + "sds_profile_latency_unit":"ms", "sds_profile_unit_minutes":"Minutes", "sds_profile_enable":"Enable", "sds_profile_nuit_days":"Days", @@ -142,5 +152,17 @@ "sds_lifeCycle_disable": "Disable the lifecycle rule", "sds_fileShare_export": "Export Location", "sds_fileShare_update": "Updated At", - "sds_ippattern": "IP address must be (1~255). (0~255). (0~255). (0~255)/(1-32)" + "sds_ippattern": "IP address must be (1~255). (0~255). (0~255). (0~255)/(1-32)", + "sds_block_host_title":"Host", + "sds_block_attach_mode" : "Attach Mode", + "sds_Hosts_title": "Hosts", + "sds_Host_detail": "Host details", + "sds_create_host": "Create Host", + "sds_host_os_type": "OS Type", + "sds_host_ip": "IP Address", + "sds_host_access_mode": "Access Mode", + "sds_host_initiators": "Initiators", + "sds_host_availability_zones": "Availability Zones", + "sds_block_createHost_desc" : "Add a new host", + "sds_block_host_delete":"Delete Host" } diff --git a/src/app/i18n/zh/keyID.json b/src/app/i18n/zh/keyID.json index de42c4fc..6a3765a7 100644 --- a/src/app/i18n/zh/keyID.json +++ b/src/app/i18n/zh/keyID.json @@ -6,6 +6,7 @@ "sds_block_volumes_descrip":"View and manage Volumes that have been manually created or applied for through service templates.", "sds_block_volumesGroup_descrip":"View and manage Volume Groups that have been manually created or applied for through service templates.", "sds_block_fileShare_descrip":"View and manage File Shares that have been manually created or applied for through service templates.", + "sds_block_hosts_descrip":"View and manage Hosts.", "sds_home_tenants":"Tenants", "sds_home_users":"Users", "sds_home_volumes":"Volumes", @@ -31,10 +32,15 @@ "sds_block_volume_createrep":"Create Replication", "sds_block_volume_more":"More", "sds_block_volume_modify":"Modify", + "sds_block_volume_attach_host":"Attach Host", + "sds_block_volume_detach_host":"Detach Host", "sds_block_volume_expand":"Expand", "sds_block_volume_delete":"Delete", "sds_block_volume_create":"Create", "sds_block_volume_search":"search", + "sds_block_volume_attach":"Attach", + "sds_block_volume_detach":"Detach", + "sds_block_volume_attachments":"Attachments", "sds_block_volume_createVol":"Create Volume", "sds_block_volume_createVol_desc":"Apply for block storage resources.", "sds_block_volume_quantity":"Quantity", @@ -69,8 +75,11 @@ "sds_profile_avai_pool":"Available Storage Pool", "sds_profile_extra_key":"Key", "sds_profile_extra_value":"Value", - "sds_profile_create_maxIOPS":"MaxIOPS", - "sds_profile_create_maxBWS":"MaxBWS", + "sds_profile_create_maxIOPS":"Maximum IOPS", + "sds_profile_create_minIOPS":"Minimum IOPS", + "sds_profile_create_maxBWS":"Maximum BWS", + "sds_profile_create_minBWS":"Minimum BWS", + "sds_profile_create_latency":"Latency", "sds_profile_rep_type":"Type", "sds_profile_rep_rgo":"RGO", "sds_profile_rep_mode":"Mode", @@ -86,6 +95,7 @@ "sds_profile_snap_dest":"Destination", "sds_profile_IOPS_unit":"IOPS/TB", "sds_profile_BWS_unit":"MBPS/TB", + "sds_profile_latency_unit":"ms", "sds_profile_unit_minutes":"Minutes", "sds_profile_enable":"Enable", "sds_profile_nuit_days":"Days", @@ -142,5 +152,17 @@ "sds_lifeCycle_disable": "Disable the lifecycle rule", "sds_fileShare_export": "Export Location", "sds_fileShare_update": "Updated At", - "sds_ippattern": "IP address must be (1~255). (0~255). (0~255). (0~255)/(1-32)" + "sds_ippattern": "IP address must be (1~255). (0~255). (0~255). (0~255)/(1-32)", + "sds_block_host_title":"Host", + "sds_block_attach_mode" : "Attach Mode", + "sds_Hosts_title": "Hosts", + "sds_Host_detail": "Host details", + "sds_create_host": "Create Host", + "sds_host_os_type": "OS Type", + "sds_host_ip": "IP Address", + "sds_host_access_mode": "Access Mode", + "sds_host_initiators": "Initiators", + "sds_host_availability_zones": "Availability Zones", + "sds_block_createHost_desc" : "Add a new host", + "sds_block_host_delete":"Delete Host" } diff --git a/src/app/shared/utils/consts.ts b/src/app/shared/utils/consts.ts index 38e3325b..ff277a37 100644 --- a/src/app/shared/utils/consts.ts +++ b/src/app/shared/utils/consts.ts @@ -33,7 +33,7 @@ export const Consts = { BUCKET_TYPE:new Map(), BYTES_PER_CHUNK : 1024 * 1024 * 16, TIMEOUT: 30 * 60 * 1000, - CLOUD_TYPE:['aws-s3','azure-blob','hw-obs','fusionstorage-object','ceph-s3','ibm-cos','gcp'], + CLOUD_TYPE:['aws-s3','azure-blob','hw-obs','fusionstorage-object','ceph-s3','ibm-cos','gcp', 'yig'], TYPE_SVG:{ "aws-s3":'aws.svg', "hw-obs":"huawei.svg", @@ -50,6 +50,7 @@ export const Consts = { 'fusionstorage-object': "FusionStorage Object", 'ceph-s3': "Ceph S3", 'gcp-s3': "GCP Storage", - 'ibm-cos': "IBM COS" + 'ibm-cos': "IBM COS", + 'yig': "YIG" } } diff --git a/src/assets/business/css/site.scss b/src/assets/business/css/site.scss index 65052965..8be1c538 100644 --- a/src/assets/business/css/site.scss +++ b/src/assets/business/css/site.scss @@ -1863,7 +1863,8 @@ a { } .create-bucket-dialog{ .ui-dialog-content{ - height: 295px; + height: 550px; + padding-bottom: 0.6rem !important; } } @@ -2015,3 +2016,49 @@ a { .list-header{ height: 50px; } + +/* Bucket Listing Page */ +span.encryption-text, span.version-text{ + margin-left: 5px; + font-weight: 600; +} +span.encryption-type { + border: 1px solid #00b12e; + border-radius: 2px; + padding: 0 3px; + margin-left: 5px; + color: #00b12e; +} +/* Bucket Listing Page ends */ +/* Host Management Starts */ +/* Host List */ +.host-az{ + text-align: center; +} +.host-az-tip{ + font-size: 14px; + padding: 10px; +} +/* Create Host page */ +.initiator-array{ + width: 600px; + padding: 0 0 5px 0; +} +a.add-initiator{ + color: #00b12e; +} + +a.remove-initiator { + color: #ff0000; + padding: 8px 0 0 0; +} +/* Host Management Ends */ +/* Attach Host to Volume */ +.attachHostModal, .detachHostModal{ + .ui-dialog-content{ + height: 250px; + } +} +.input-context-help { + padding-top: 0.1rem; +} \ No newline at end of file