aboutsummaryrefslogtreecommitdiffstats
path: root/usecaseui-portal/src/app/services
diff options
context:
space:
mode:
Diffstat (limited to 'usecaseui-portal/src/app/services')
-rw-r--r--usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.css59
-rw-r--r--usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.html61
-rw-r--r--usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.less50
-rw-r--r--usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.spec.ts25
-rw-r--r--usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.ts61
-rw-r--r--usecaseui-portal/src/app/services/services-list/services-list.component.css132
-rw-r--r--usecaseui-portal/src/app/services/services-list/services-list.component.html185
-rw-r--r--usecaseui-portal/src/app/services/services-list/services-list.component.less127
-rw-r--r--usecaseui-portal/src/app/services/services-list/services-list.component.spec.ts25
-rw-r--r--usecaseui-portal/src/app/services/services-list/services-list.component.ts511
-rw-r--r--usecaseui-portal/src/app/services/services.component.html18
-rw-r--r--usecaseui-portal/src/app/services/services.component.less0
-rw-r--r--usecaseui-portal/src/app/services/services.component.spec.ts25
-rw-r--r--usecaseui-portal/src/app/services/services.component.ts15
14 files changed, 1294 insertions, 0 deletions
diff --git a/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.css b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.css
new file mode 100644
index 00000000..a1cc6455
--- /dev/null
+++ b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.css
@@ -0,0 +1,59 @@
+/*
+ Copyright (C) 2018 CMCC, Inc. and others. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.title {
+ font: 700 18px/18px "思源黑体";
+ color: #4c5e70;
+ margin-bottom: 18px;
+}
+hr {
+ border: none;
+ height: 2px;
+ background-color: #dce1e7;
+ margin-bottom: 20px;
+}
+.list {
+ background-color: #fff;
+ border-radius: 5px;
+ padding: 10px;
+}
+.list nz-table tbody td span.onboarding {
+ font-size: 12px;
+ color: #147dc2;
+}
+.list nz-table tbody td span.onboarded {
+ font-size: 14px;
+ color: #147dc2;
+}
+.list nz-table tbody td span.updating {
+ font-size: 12px;
+ color: blue;
+}
+.list nz-table tbody td span.deleting {
+ font-size: 12px;
+ color: red;
+}
+.list nz-table tbody td span.invalid {
+ font-size: 14px;
+ color: purple;
+}
+.list nz-table tbody td i.anticon {
+ cursor: pointer;
+ font-size: 18px;
+ padding: 2px;
+}
+.list nz-table tbody td i.anticon:hover {
+ color: #147dc2;
+}
diff --git a/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.html b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.html
new file mode 100644
index 00000000..d5286a3a
--- /dev/null
+++ b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.html
@@ -0,0 +1,61 @@
+<!--
+ Copyright (C) 2018 CMCC, Inc. and others. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<h3 class="title"> Onboard VNF </h3>
+<hr>
+<div class="list">
+ <nz-table #nzTable [nzData]="tableData"
+ nzShowSizeChanger
+ [nzFrontPagination]="false"
+ [nzShowQuickJumper]="true"
+ [nzPageSizeOptions]="[5,10,15,20]"
+ [nzTotal]= 'total'
+ [(nzPageSize)]="pageSize"
+ [(nzPageIndex)]='pageIndex'
+ [nzLoading]="loading"
+ nzSize="middle"
+ (nzPageIndexChange)="searchData()"
+ (nzPageSizeChange)="searchData(true)">
+ <thead (nzSortChange)="sort($event)" nzSingleSort>
+ <tr>
+ <th nzWidth="5%">NO</th>
+ <th nzWidth="20%" nzShowSort nzSortKey="name"> Name </th>
+ <th nzWidth="20%">Type</th>
+ <th nzWidth="15%">Version</th>
+ <th nzWidth="20%">Status</th>
+ <th nzWidth="15%">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ <!-- <ng-template ngFor let-data [ngForOf]="nzTable.data" let-i="index"> -->
+ <tr *ngFor="let item of nzTable.data; let i = index; ">
+ <td>{{i+1}}</td>
+ <td>{{item.name}}</td>
+ <td>{{item.type}}</td>
+ <td>{{item.version}}</td>
+ <td>
+ <span [ngClass]="{'onboarding':item.status=='Onboarding','onboarded':item.status=='Onboarded',
+ 'updating':item.status=='Updating','deleting':item.status=='Deleting','invalid':item.status=='Invalid'}">{{item.status}}</span>
+ <nz-progress *ngIf="item.status!='Onboarded' && item.status!='Invalid'" [nzPercent]="item.progress"></nz-progress>
+ </td>
+ <td>
+ <i class="anticon anticon-cloud-upload-o" (click)="updataService()"></i>
+ <i class="anticon anticon-delete" (click)="deleteService()"></i>
+ </td>
+ </tr>
+ <!-- </ng-template> -->
+ </tbody>
+ </nz-table>
+</div> \ No newline at end of file
diff --git a/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.less b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.less
new file mode 100644
index 00000000..ab118737
--- /dev/null
+++ b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.less
@@ -0,0 +1,50 @@
+.title {
+ font: 700 18px/18px "思源黑体";
+ color: #4c5e70;
+ margin-bottom: 18px;
+}
+hr {
+ border: none;
+ height: 2px;
+ background-color: #dce1e7;
+ margin-bottom: 20px;
+}
+.list {
+ background-color: #fff;
+ border-radius: 5px;
+ padding: 10px;
+ nz-table {
+ tbody {
+ td {
+ span.onboarding {
+ font-size: 12px;
+ color: #147dc2;
+ }
+ span.onboarded {
+ font-size: 14px;
+ color: #147dc2;
+ }
+ span.updating {
+ font-size: 12px;
+ color: blue;
+ }
+ span.deleting {
+ font-size: 12px;
+ color: red;
+ }
+ span.invalid {
+ font-size: 14px;
+ color: purple;
+ }
+ i.anticon {
+ cursor: pointer;
+ font-size: 18px;
+ padding: 2px;
+ &:hover{
+ color: #147dc2;
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.spec.ts b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.spec.ts
new file mode 100644
index 00000000..0e49f656
--- /dev/null
+++ b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OnboardVnfVmComponent } from './onboard-vnf-vm.component';
+
+describe('OnboardVnfVmComponent', () => {
+ let component: OnboardVnfVmComponent;
+ let fixture: ComponentFixture<OnboardVnfVmComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ OnboardVnfVmComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OnboardVnfVmComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.ts b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.ts
new file mode 100644
index 00000000..c42b3ce3
--- /dev/null
+++ b/usecaseui-portal/src/app/services/onboard-vnf-vm/onboard-vnf-vm.component.ts
@@ -0,0 +1,61 @@
+import { Component, OnInit, HostBinding } from '@angular/core';
+import { MyhttpService } from '../../myhttp.service';
+import { slideToRight } from '../../animates';
+
+@Component({
+ selector: 'app-onboard-vnf-vm',
+ templateUrl: './onboard-vnf-vm.component.html',
+ styleUrls: ['./onboard-vnf-vm.component.less'],
+ animations: [ slideToRight ]
+})
+export class OnboardVnfVmComponent implements OnInit {
+ @HostBinding('@routerAnimate') routerAnimateState;
+ constructor(private myhttp: MyhttpService) { }
+
+ ngOnInit() {
+ this.getTableData();
+ }
+
+
+ //表格数据
+ tableData = [];
+ pageIndex = 1;
+ pageSize = 10;
+ total = 100;
+ loading = false;
+ sortName = null;
+ sortValue = null;
+ getTableData(){
+ // 查询参数: 当前页码,每页条数,排序方式
+ let paramsObj = {
+ pageIndex:this.pageIndex,
+ pageSize:this.pageSize,
+ nameSort:this.sortValue
+ }
+ this.myhttp.getOnboardTableData(paramsObj)
+ .subscribe((data)=>{
+ console.log(data);
+ this.total = data.body.total;
+ this.tableData = data.body.tableList;
+ },(err)=>{
+ console.log(err);
+ })
+ }
+ sort(sort: { key: string, value: string }): void {
+ console.log(sort);
+ this.sortName = sort.key;
+ this.sortValue = sort.value;
+ this.getTableData();
+ }
+ searchData(reset:boolean = false){
+ console.log(reset)
+ this.getTableData();
+ }
+ updataService(){
+ console.log("updataService!");
+ }
+ deleteService(){
+ console.log("deleteService!");
+ }
+
+}
diff --git a/usecaseui-portal/src/app/services/services-list/services-list.component.css b/usecaseui-portal/src/app/services/services-list/services-list.component.css
new file mode 100644
index 00000000..eaf8fddb
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services-list/services-list.component.css
@@ -0,0 +1,132 @@
+/*
+ Copyright (C) 2018 CMCC, Inc. and others. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+.title {
+ font: 700 18px/18px "思源黑体";
+ color: #4c5e70;
+ margin-bottom: 18px;
+}
+hr {
+ border: none;
+ height: 2px;
+ background-color: #dce1e7;
+ margin-bottom: 20px;
+}
+.action {
+ margin-bottom: 20px;
+}
+.action span {
+ display: inline-block;
+ font: 700 14px "Arial";
+ color: #4c5e70;
+}
+.action nz-dropdown {
+ vertical-align: middle;
+}
+.action nz-dropdown :hover {
+ border-color: #147dc2;
+}
+.action nz-dropdown button {
+ width: 165px;
+ height: 30px;
+ background-color: #eceff4;
+ text-align: left;
+ border-color: #9fa9ab;
+}
+.action nz-dropdown button span {
+ font-weight: 400;
+ display: inline-block;
+ width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-top: 2px;
+}
+.action nz-dropdown button i {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+}
+.action .create {
+ float: right;
+ height: 30px;
+ padding: 0 10px;
+}
+.action .create span {
+ color: #fff;
+ font-weight: 400;
+}
+.list {
+ background-color: #fff;
+ border-radius: 5px;
+ padding: 10px;
+}
+.list nz-table tbody td span.active {
+ font-size: 14px;
+ color: #147dc2;
+}
+.list nz-table tbody td span.closed {
+ font-size: 14px;
+ color: red;
+}
+.list nz-table tbody td span.onboarding {
+ font-size: 12px;
+ color: #147dc2;
+}
+.list nz-table tbody td span.updating {
+ font-size: 12px;
+ color: blue;
+}
+.list nz-table tbody td span.deleting {
+ font-size: 12px;
+ color: red;
+}
+.list nz-table tbody td span.creating {
+ font-size: 12px;
+ color: green;
+}
+.list nz-table tbody td i.anticon {
+ cursor: pointer;
+ font-size: 18px;
+ padding: 2px;
+}
+.list nz-table tbody td i.anticon:hover {
+ color: #147dc2;
+}
+.list nz-table tbody tr.childtr td {
+ font-size: 12px;
+ color: #147dc2;
+}
+.detailComponent {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100vh;
+ background-color: #f3f3f3;
+ overflow-y: auto;
+ padding: 20px 32px;
+ z-index: 3;
+}
+.createComponent {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100vh;
+ background-color: #f3f3f3;
+ overflow-y: auto;
+ padding: 20px 32px;
+ z-index: 3;
+}
diff --git a/usecaseui-portal/src/app/services/services-list/services-list.component.html b/usecaseui-portal/src/app/services/services-list/services-list.component.html
new file mode 100644
index 00000000..e0866524
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services-list/services-list.component.html
@@ -0,0 +1,185 @@
+<!--
+ Copyright (C) 2018 CMCC, Inc. and others. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<h3 class="title"> Services List </h3>
+<hr>
+<div class="action">
+ <span>Customer: </span>
+ <nz-dropdown [nzTrigger]="'click'" [nzPlacement]="'bottomLeft'">
+ <button nz-button nz-dropdown><span>{{customerSelected.name}}</span> <i class="anticon anticon-down"></i></button>
+ <ul nz-menu style="max-height: 200px; overflow: auto;">
+ <li nz-menu-item (click)="choseCustomer(item)" *ngFor="let item of customerList">
+ <a title="{{item.name}}" style="max-width: 165px; overflow: hidden; text-overflow: ellipsis;">{{item.name}}</a>
+ </li>
+ </ul>
+ </nz-dropdown>
+
+ &nbsp;&nbsp;
+ <span>Service Type: </span>
+ <nz-dropdown [nzTrigger]="'click'" [nzPlacement]="'bottomLeft'">
+ <button nz-button nz-dropdown><span>{{serviceTypeSelected.name}}</span> <i class="anticon anticon-down"></i></button>
+ <ul nz-menu style="max-height: 200px; overflow: auto;">
+ <li nz-menu-item (click)="choseServiceType(item)" *ngFor="let item of serviceTypeList">
+ <a title="{{item.name}}" style="max-width: 165px; overflow: hidden; text-overflow: ellipsis;">{{item.name}}</a>
+ </li>
+ </ul>
+ </nz-dropdown>
+
+ <button class="create" nz-button [nzType]="'primary'" (click)="showModal()"><i class="anticon anticon-plus-circle-o"></i><span> Create </span></button>
+ <nz-modal [(nzVisible)]="isVisible" nzTitle="Create" (nzOnCancel)="handleCancel()" (nzOnOk)="handleOk()">
+ <p>Content one</p>
+ <p>Content two</p>
+ <p>Content three</p>
+ </nz-modal>
+ <button class="create" nz-button [nzType]="'primary'" (click)="showModal2()"><i class="anticon anticon-plus-circle-o"></i><span> Create </span></button>
+ <nz-modal [(nzVisible)]="isVisible2" nzTitle="Create" (nzOnCancel)="handleCancel()" (nzOnOk)="handleOk2()">
+ <span style="display:inline-block;width:70px;">Service: </span>
+ <nz-select style="width: 165px;" [(ngModel)]="templateTypeSelected" nzAllowClear (ngModelChange)="choseTemplateType()">
+ <!-- <nz-option *ngFor="let item of templateType" [nzValue]="item" [nzLabel]="item"></nz-option> -->
+ <nz-option nzValue="SOTN" nzLabel="SOTN"></nz-option>
+ <nz-option nzValue="CCVPN" nzLabel="CCVPN"></nz-option>
+ </nz-select>
+
+ <hr>
+ <span>SOTN VPN: </span>
+ <nz-select style="width: 165px;" [(ngModel)]="template1" nzAllowClear >
+ <nz-option *ngFor="let item of templates" [nzValue]="item" [nzLabel]="item.name"></nz-option>
+ </nz-select>
+
+ <span> SITE: </span>
+ <nz-select style="width: 165px;" [(ngModel)]="template2" nzAllowClear >
+ <nz-option *ngFor="let item of templates" [nzValue]="item" [nzLabel]="item.name"></nz-option>
+ </nz-select>
+
+ <div *ngIf="templateTypeSelected == 'CCVPN'">
+ <br>
+ <span style="display:inline-block;width:70px;">SD-WAN: </span>
+ <nz-select style="width: 165px;" [(ngModel)]="template3" nzAllowClear >
+ <nz-option *ngFor="let item of templates" [nzValue]="item" [nzLabel]="item.name"></nz-option>
+ </nz-select>
+ </div>
+ </nz-modal>
+</div>
+<div class="list">
+ <nz-table *ngIf="1"
+ #nzTable [nzData]="tableData"
+ nzShowSizeChanger
+ [nzFrontPagination]="false"
+ [nzShowQuickJumper]="true"
+ [nzPageSizeOptions]="[5,10,15,20]"
+ [nzTotal]= 'total'
+ [(nzPageSize)]="pageSize"
+ [(nzPageIndex)]='pageIndex'
+ [nzLoading]="loading"
+ [nzSize]="'middle'"
+ [nzScroll]="{ y: '58vh' }"
+ (nzPageIndexChange)="searchData()"
+ (nzPageSizeChange)="searchData(true)">
+ <thead (nzSortChange)="sort($event)" nzSingleSort>
+ <tr>
+ <th nzWidth="5%">NO</th>
+ <th nzWidth="5%"></th>
+ <th nzWidth="20%" nzShowSort nzSortKey="name">Service Instance Id</th>
+ <th nzWidth="20%">Name</th>
+ <th nzWidth="15%">Type</th>
+ <th nzWidth="20%">Status</th>
+ <th nzWidth="15%">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ <ng-template ngFor let-data [ngForOf]="nzTable.data" let-i="index">
+ <tr>
+ <td>{{i+1}}</td>
+ <td [nzShowExpand]="data.children[0]" [(nzExpand)]="data.expand"></td>
+ <td>{{data.serviceId}}</td>
+ <td>{{data.name}}</td>
+ <td>{{data.type}}</td>
+ <td>
+ <span [ngClass]="{'active':data.status=='Active','closed':data.status=='Closed','onboarding':data.status=='Onboarding',
+ 'updating':data.status=='Updating','deleting':data.status=='Deleting','creating':data.status=='Creating'}">{{data.status}}</span>
+ <nz-progress *ngIf="data.status!='Active' && data.status!='Closed'" [nzPercent]="data.progress"></nz-progress>
+ </td>
+ <td>
+ <i class="anticon anticon-setting" (click)="scaleService()"></i>
+ <i class="anticon anticon-cloud-upload-o" (click)="updataService()"></i>
+ <i class="anticon anticon-delete" (click)="deleteService()"></i>
+ </td>
+ </tr>
+ <tr class="childtr" [nzExpand]="data.expand" *ngFor="let item of data.children">
+ <td></td>
+ <td></td>
+ <td>{{item.serviceId}}</td>
+ <td>{{item.name}}</td>
+ <td colspan="3">{{item.type}}</td>
+ </tr>
+ </ng-template>
+ </tbody>
+ </nz-table>
+ <nz-table *ngIf="1"
+ #nzTable2 [nzData]="tableData2"
+ nzShowSizeChanger
+ [nzFrontPagination]="true"
+ [nzShowQuickJumper]="true"
+ [nzPageSizeOptions]="[5,10,15,20]"
+ [(nzPageSize)]="pageSize"
+ [(nzPageIndex)]='pageIndex'
+ nzSize="middle"
+ [nzScroll]="{ y: '58vh' }">
+ <thead nzSingleSort>
+ <tr>
+ <th nzWidth="5%">NO.</th>
+ <th nzWidth="20%"> Instance ID </th>
+ <th nzWidth="20%">Instance Name</th>
+ <!-- <th nzWidth="10%">Type</th> -->
+ <th nzWidth="25%">Description</th>
+ <th nzWidth="15%">Status</th>
+ <th nzWidth="10%">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ <!-- <ng-template ngFor let-data [ngForOf]="nzTable2.data" let-i="index"> -->
+ <tr *ngFor="let item of nzTable2.data; let i = index; ">
+ <td>{{pageSize*(pageIndex-1) + i+1}}</td>
+ <td>{{item.sotnvpnSer['service-instance-id']}}</td>
+ <td>{{item.sotnvpnSer['service-instance-name']}}</td>
+ <!-- <td>{{item.type}}</td> -->
+ <td>{{item.sotnvpnSer.description}}</td>
+ <td>
+ <span *ngIf="item.sotnvpnSer.status!='creating' && item.sotnvpnSer.status!='deleting'">{{item.sotnvpnSer.status}}</span>
+ <span *ngIf="item.sotnvpnSer.status=='creating' || item.sotnvpnSer.status=='deleting'" [ngClass]="{'deleting':item.sotnvpnSer.status=='deleting','creating':item.sotnvpnSer.status=='creating'}">{{item.sotnvpnSer.status}}</span>
+ <nz-progress *ngIf="item.sotnvpnSer.status=='creating' || item.sotnvpnSer.status=='deleting'" [nzPercent]="item.sotnvpnSer.rate"></nz-progress>
+ </td>
+ <td>
+ <span title="detail" class="action" [ngClass]="{'cannotclick':item.sotnvpnSer.status=='deleting'||item.sotnvpnSer.status=='creating'}"
+ (click)="showDetail(item)"> <i class="anticon anticon-ellipsis"></i> </span>
+ <span title="delete" class="action" [ngClass]="{'cannotclick':item.sotnvpnSer.status=='deleting'||item.sotnvpnSer.status=='creating'}"
+ (click)="deleteInstace(item)"> <i class="anticon anticon-delete"></i> </span>
+ </td>
+ </tr>
+ <!-- </ng-template> -->
+ </tbody>
+ </nz-table>
+</div>
+
+<div class="detailComponent" *ngIf="detailshow">
+ <app-ccvpn-detail [namesTranslate]="namesTranslate" [detailParams]="detailData" (closeDetail)="detailshow = false;"></app-ccvpn-detail>
+</div>
+<div class="createComponent" *ngIf="createshow">
+ <app-ccvpn-creation
+ [createParams]="createData"
+ [namesTranslate]="namesTranslate"
+ (closeCreate)="closeCreate($event)">
+ </app-ccvpn-creation>
+</div>
diff --git a/usecaseui-portal/src/app/services/services-list/services-list.component.less b/usecaseui-portal/src/app/services/services-list/services-list.component.less
new file mode 100644
index 00000000..7e8ff80e
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services-list/services-list.component.less
@@ -0,0 +1,127 @@
+.title {
+ font: 700 18px/18px "思源黑体";
+ color: #4c5e70;
+ margin-bottom: 18px;
+}
+hr {
+ border: none;
+ height: 2px;
+ background-color: #dce1e7;
+ margin-bottom: 20px;
+}
+.action {
+ margin-bottom: 20px;
+ span {
+ display: inline-block;
+ font: 700 14px "Arial";
+ color: #4c5e70;
+ }
+ nz-dropdown {
+ vertical-align: middle;
+ :hover{
+ border-color: #147dc2;
+ }
+ button {
+ width: 165px;
+ height: 30px;
+ background-color: #eceff4;
+ text-align: left;
+ border-color: #9fa9ab;
+ span {
+ font-weight: 400;
+ display: inline-block;
+ width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-top: 2px;
+ }
+ i {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ }
+ }
+ //下拉框中的样式在style.less中,下拉框是在body中额外临时生成的
+ }
+ .create {
+ float: right;
+ height: 30px;
+ padding: 0 10px;
+ span {
+ color: #fff;
+ font-weight: 400;
+ }
+ }
+}
+.list {
+ background-color: #fff;
+ border-radius: 5px;
+ padding: 10px;
+ nz-table {
+ tbody {
+ td {
+ span.active {
+ font-size: 14px;
+ color: #147dc2;
+ }
+ span.closed {
+ font-size: 14px;
+ color: red;
+ }
+ span.onboarding{
+ font-size: 12px;
+ color: #147dc2;
+ }
+ span.updating{
+ font-size: 12px;
+ color: blue;
+ }
+ span.deleting {
+ font-size: 12px;
+ color: red;
+ }
+ span.creating {
+ font-size: 12px;
+ color: green;
+ }
+ i.anticon {
+ cursor: pointer;
+ font-size: 18px;
+ padding: 2px;
+ &:hover{
+ color: #147dc2;
+ }
+ }
+ }
+ tr.childtr {
+ td {
+ font-size: 12px;
+ color: #147dc2;
+ }
+ }
+ }
+ }
+}
+
+.detailComponent {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100vh;
+ background-color: #f3f3f3;
+ overflow-y: auto;
+ padding: 20px 32px;
+ z-index: 3;
+}
+.createComponent {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100vh;
+ background-color: #f3f3f3;
+ overflow-y: auto;
+ padding: 20px 32px;
+ z-index: 3;
+}
diff --git a/usecaseui-portal/src/app/services/services-list/services-list.component.spec.ts b/usecaseui-portal/src/app/services/services-list/services-list.component.spec.ts
new file mode 100644
index 00000000..61440dc3
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services-list/services-list.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ServicesListComponent } from './services-list.component';
+
+describe('ServicesListComponent', () => {
+ let component: ServicesListComponent;
+ let fixture: ComponentFixture<ServicesListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ServicesListComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServicesListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/usecaseui-portal/src/app/services/services-list/services-list.component.ts b/usecaseui-portal/src/app/services/services-list/services-list.component.ts
new file mode 100644
index 00000000..d893070d
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services-list/services-list.component.ts
@@ -0,0 +1,511 @@
+import { Component, OnInit, HostBinding } from '@angular/core';
+import { MyhttpService } from '../../myhttp.service';
+import { slideToRight } from '../../animates';
+import { NzModalService } from 'ng-zorro-antd';
+
+@Component({
+ selector: 'app-services-list',
+ templateUrl: './services-list.component.html',
+ styleUrls: ['./services-list.component.less'],
+ animations: [ slideToRight ]
+})
+export class ServicesListComponent implements OnInit {
+ @HostBinding('@routerAnimate') routerAnimateState;
+ constructor(private myhttp: MyhttpService, private modalService: NzModalService) { }
+
+ ngOnInit() {
+ this.getallCustomers();
+ this.getTemplateSubTypes();
+ this.inputNamests();
+ }
+ // 筛选框(下拉框)customer servicetype
+ customerList = [];
+ customerSelected = {name:null,id:null};
+ serviceTypeList = [];
+ serviceTypeSelected = {name:null,id:null};
+
+ // 获取所有customer
+ getallCustomers(){
+ this.myhttp.getAllCustomers()
+ .subscribe((data)=>{
+ this.customerList = data.map((item)=>{return {name:item["subscriber-name"],id:item["global-customer-id"]}});
+ this.customerSelected = this.customerList[0];
+ this.choseCustomer(this.customerSelected);
+ // console.log(this.customers)
+ })
+ }
+
+ choseCustomer(item){
+ this.customerSelected = item;
+ this.myhttp.getServiceTypes(this.customerSelected)
+ .subscribe((data)=>{
+ this.serviceTypeList = data.map((item)=>{return {name:item["service-type"]}});
+ this.serviceTypeSelected = this.serviceTypeList[0];
+ this.choseServiceType(this.serviceTypeSelected);
+ // console.log(this.listServiceTypes);
+ })
+ }
+ choseServiceType(item){
+ this.serviceTypeSelected = item;
+ this.getTableData();
+ }
+
+ // 模态框(对话框) create
+ isVisible = false;
+ showModal(): void {
+ this.isVisible = true;
+ }
+ handleOk(): void {
+ console.log('Button ok clicked!');
+ this.isVisible = false;
+ }
+ handleCancel(): void {
+ console.log('Button cancel clicked!');
+ this.isVisible = false;
+ this.isVisible2 = false;
+ }
+
+ // 创建模态框2(对话框) create -------------------------------
+ isVisible2 = false;
+ showModal2(): void {
+ this.isVisible2 = true;
+ this.templates1 = []; //多次创建会push累积名字,从新置空
+ this.templates2 = [];
+ this.templates3 = [];
+ this.getAlltemplates();
+ }
+ // 服务
+ templateTypeSelected = "SOTN";
+ choseTemplateType(){
+ // this.filterTemplates();//分类
+ }
+ // 模板
+ templates = []; templates1;templates2;templates3;
+ template1={name:null};
+ template2={name:null};
+ template3={name:null};
+ // 模板分类数据,创建、获取实例分类共用
+ templateSubTypes = {}; //子类,sotnvpn、site、sdwan
+ getTemplateSubTypes(){
+ this.myhttp.getServicesCategory()
+ .subscribe((data)=>{
+ this.templateSubTypes = data;
+ },(err)=>{
+ console.log("getTemplateTypes err")
+ })
+ }
+
+ getAlltemplates(){ //获取所有模板类型
+ this.myhttp.getAllServiceTemplates()
+ .subscribe((data)=>{
+ console.log(data)
+ this.templates = data;
+ this.template1 = data[0];
+ this.template2 = data[1];
+ this.template3 = data[2];
+ // this.filterTemplates();//分类
+ },(err)=>{
+
+ })
+ }
+ // filterTemplates(){ //模板类型分类,本地配置文件
+ // this.templates1 = [];
+ // this.templates2 = [];
+ // this.templates3 = [];
+ // this.templates.forEach((item)=>{
+ // this.templateSubTypes[this.templateTypeSelected].sotnvpn.find((d)=>{
+ // return d["model-invariant-id"] == item.uuid && d["model-version-id"] == item.invariantUUID
+ // })?this.templates1.push(item):null;
+ // this.templateSubTypes[this.templateTypeSelected].site.find((d)=>{
+ // return d["model-invariant-id"] == item.uuid && d["model-version-id"] == item.invariantUUID
+ // })?this.templates2.push(item):null;
+ // if(this.templateTypeSelected=="CCVPN"){
+ // this.templateSubTypes[this.templateTypeSelected].sdwan.find((d)=>{
+ // return d["model-invariant-id"] == item.uuid && d["model-version-id"] == item.invariantUUID
+ // })?this.templates3.push(item):null;
+ // }
+ // })
+ // this.template1 = this.templates1[0];
+ // this.template2 = this.templates2[0];
+ // if(this.templates3[0]){
+ // this.template3 = this.templates3[0];
+ // }
+ // }
+
+
+ // 确定、取消
+ createshow = false;
+ createData:Object={};
+ handleOk2(): void {
+ console.log('Button ok clicked!');
+ this.isVisible2 = false;
+ let data1 = {commonParams:{customer:this.customerSelected, serviceType:this.serviceTypeSelected, templateType:"SOTN"},templates:{template1:this.template1,template2:this.template2}};
+ let data2 = {commonParams:{customer:this.customerSelected, serviceType:this.serviceTypeSelected, templateType:"CCVPN"},templates:{template1:this.template1,template2:this.template2,template3:this.template3}};
+
+ this.createData = this.templateTypeSelected == "SOTN" ? data1 : data2;
+ this.createshow = true;
+ }
+ // handleCancel(): void {
+ // console.log('Button cancel clicked!');
+ // this.isVisible2 = false;
+ // }
+
+
+ //表格数据
+ tableData = [];
+ pageIndex = 1;
+ pageSize = 10;
+ total = 100;
+ loading = false;
+ sortName = null;
+ sortValue = null;
+ getTableData(){
+ // 查询参数: customer serviceType 当前页码,每页条数,排序方式
+ let paramsObj = {
+ customer:this.customerSelected,
+ serviceType:this.serviceTypeSelected,
+ pageIndex:this.pageIndex,
+ pageSize:this.pageSize,
+ serviceIdSort:this.sortValue
+ }
+ this.myhttp.getServicesTableData(paramsObj)
+ .subscribe((data)=>{
+ console.log(data);
+ this.total = data.body.total;
+ this.tableData = data.body.tableList;
+ },(err)=>{
+ console.log(err);
+ })
+ }
+ sort(sort: { key: string, value: string }): void {
+ console.log(sort);
+ this.sortName = sort.key;
+ this.sortValue = sort.value;
+ this.getTableData();
+ }
+ searchData(reset:boolean = false){
+ console.log(reset)
+ this.getTableData();
+ }
+
+ scaleService(){
+ console.log("scaleService!");
+ }
+ updataService(){
+ console.log("updataService!");
+ }
+ deleteService(){
+ console.log("deleteService!");
+ }
+
+ //表格数据
+ tableData2 = [];
+ getTableData2(){
+ let params = {
+ customerId:this.customerSelected.id,
+ serviceType:this.serviceTypeSelected
+ }
+ this.myhttp.getInstanceTableData(params)
+ .subscribe((data)=>{
+ this.pageIndex = 1;
+ this.tableData2 = [];
+ console.log(data)
+ // data.results.forEach((item)=>{
+ // item["sotnvpnSer"] = item["service-subscription"]["service-instances"]["service-instance"].find((d)=>{
+ // return this.templateSubTypes["SOTN"].sotnvpn.find((m)=>{
+ // return d["model-invariant-id"]==m["model-invariant-id"] && d["model-version-id"]==m["model-version-id"]
+ // })?item["Type"]="SOTN":null || this.templateSubTypes["CCVPN"].sotnvpn.find((m)=>{
+ // return d["model-invariant-id"]==m["model-invariant-id"] && d["model-version-id"]==m["model-version-id"]
+ // })?item["Type"]="CCVPN":null
+ // })
+
+ // if(item["sotnvpnSer"]){
+ // this.tableData2.push(item);
+ // }
+ // })
+
+ //---------数据结构有问题,模拟只有一组数据情况---------//
+ data["sotnvpnSer"] = data["service-instance"].find((d)=>{
+ return this.templateSubTypes["SOTN"].sotnvpn.find((m)=>{
+ return d["model-invariant-id"]==m["model-invariant-id"] && d["model-version-id"]==m["model-version-id"]
+ })?d["Type"]="SOTN":null || this.templateSubTypes["CCVPN"].sotnvpn.find((m)=>{
+ return d["model-invariant-id"]==m["model-invariant-id"] && d["model-version-id"]==m["model-version-id"]
+ })?d["Type"]="CCVPN":null
+ })
+ let inputParams = JSON.parse(data["sotnvpnSer"]["input-parameters"]).service.parameters.requestInputs;
+ let descriptionName = Object.keys(inputParams).find((item)=>{ return item.endsWith("_description")});
+ data["sotnvpnSer"]["description"] = inputParams[descriptionName];
+ data["sotnvpnSer"]["status"] = "Active";
+ this.tableData2.push(data);
+
+ console.log(this.tableData2)
+ },(err)=>{
+ console.log(err);
+ })
+ }
+
+ // 显示详情
+ detailshow = false;
+ detailData:Object;
+ showDetail(service){
+ service["siteSer"]=[];
+ service["sdwanSer"]=[];
+ service["customer"]=this.customerSelected;
+ service["serviceType"] = this.serviceTypeSelected;
+ // service["service-subscription"]["service-instances"]["service-instance"].forEach((item)=>{
+ // this.templateSubTypes[service.Type].site.find((d)=>{
+ // return d["model-invariant-id"] == item["model-invariant-id"] && d["model-version-id"] == item["model-version-id"]
+ // })?service["siteSer"].push(item):null;
+ // if(service.Type=="CCVPN"){
+ // this.templateSubTypes[service.Type].sdwan.find((d)=>{
+ // return d["model-invariant-id"] == item["model-invariant-id"] && d["model-version-id"] == item["model-version-id"]
+ // })?service["sdwanSer"].push(item):null;
+ // }
+ // })
+ service["service-instance"].forEach((item)=>{
+ this.templateSubTypes[service.sotnvpnSer.Type].site.find((d)=>{
+ return d["model-invariant-id"] == item["model-invariant-id"] && d["model-version-id"] == item["model-version-id"]
+ })?service["siteSer"].push(item):null;
+ if(service.sotnvpnSer.Type=="CCVPN"){
+ this.templateSubTypes[service.sotnvpnSer.Type].sdwan.find((d)=>{
+ return d["model-invariant-id"] == item["model-invariant-id"] && d["model-version-id"] == item["model-version-id"]
+ })?service["sdwanSer"].push(item):null;
+ }
+ })
+ this.detailshow = true;
+ this.detailData = service;
+ console.log(service);
+ }
+ // 删除 确认模态框
+ deleteInstace(service){
+ // 创建确认框
+ this.modalService.confirm({
+ nzTitle : 'Are you sure delete this instance?',
+ nzContent : `Instance ID: <b class="deleteModelContent"> ${service.sotnvpnSer["service-instance-id"]}</b>`,
+ nzOkText : 'Yes',
+ nzOkType : 'danger',
+ nzOnOk : () => {
+ console.log(service);
+ let allprogress = {}; //所有进度值,以operationId为键
+ let querypros = []; //所有查询
+ service.sotnvpnSer.rate = 0;
+ service.sotnvpnSer.status = "deleting";
+ // let deletePros = service["service-subscription"]["service-instances"]["service-instance"].map((item)=>{
+ // let id = item["service-instance-id"];
+ // return new Promise((res,rej)=>{
+ // this.myhttp.deleteInstance(id)
+ // .subscribe((data)=>{
+ // let obj = {serviceId:id,operationId:data.operationId}
+ // let updata = (prodata)=>{
+ // allprogress[prodata.operationId] = prodata.progress;
+ // let average = ((arr)=>{return eval(arr.join("+"))/arr.length})(Object.values(allprogress));
+ // service.sotnvpnSer["rate"]=average;
+ // }
+ // querypros.push(this.queryProgress(obj,updata));
+ // res();
+ // })
+ // })
+ // })
+ let deletePros = service["service-instance"].map((item)=>{
+ let params = {
+ globalSubscriberId:this.customerSelected.id,
+ serviceType:this.serviceTypeSelected,
+ serviceInstanceId:item["service-instance-id"]
+ }
+ return new Promise((res,rej)=>{
+ this.myhttp.deleteInstance(params)
+ .subscribe((data)=>{
+ let obj = {serviceId:params.serviceInstanceId,operationId:data.operationId}
+ let updata = (prodata)=>{
+ allprogress[prodata.operationId] = prodata.progress;
+ let average = ((arr)=>{return eval(arr.join("+"))/arr.length})(Object.values(allprogress));
+ service.sotnvpnSer["rate"]=average;
+ }
+ querypros.push(this.queryProgress(obj,updata));
+ res();
+ })
+ })
+ })
+ console.log(deletePros)
+ Promise.all(deletePros).then(()=>{
+ Promise.all(querypros).then((data)=>{
+ console.log(data);
+ service.sotnvpnSer.rate = 100;
+ service.sotnvpnSer.status = "deleted";
+ setTimeout(()=>{
+ this.getTableData();
+ },1000)
+ })
+ })
+
+ },
+ nzCancelText: 'No',
+ nzOnCancel : () => console.log('Cancel')
+ });
+ }
+
+
+ closeCreate(obj){
+ if(!obj){
+ this.createshow = false; //关闭创建窗口
+ return false;
+ }
+ this.createshow = false; //关闭创建窗口
+ console.log(obj);
+ let newData; //主表格中新创建的服务数据
+ let stageNum = 0; //不同阶段进度,用于后续服务进度相加;
+ // --------------------------------------------------------------------------
+ // obj.groupbody.map((group)=>{ //所有创建
+ // return this.createService(group)
+ // })
+ // obj.sitebody.map((group)=>{ //所有创建
+ // console.log(group)
+ // return this.createService(group)
+ // })
+ // -----------------------------------------------------------------------------
+ this.createService(obj.vpnbody).then((data)=>{
+ console.log(data)
+ newData = { //主表格中新创建的服务数据
+ 'service-instance-id':data["serviceId"],
+ 'service-instance-name':obj.vpnbody.service.name,
+ description:obj.vpnbody.service.description,
+ status:"creating",
+ rate:0,
+ }
+ this.tableData2 = [{sotnvpnSer:newData},...this.tableData2];
+ let updata = (prodata)=>{
+ newData.rate = Math.floor(prodata.progress/3);
+ }
+ let queryParams = {serviceId:data["serviceId"],operationId:data["operationId"]};
+ return this.queryProgress(queryParams,updata);
+ }).then((data)=>{
+ console.log(data);
+ stageNum = newData.rate; //阶段进度值更新;
+ let allprogress = {}; //所有进度值,以operationId为键
+ let querypros = []; //所有查询
+ let createPros = obj.groupbody.map((group)=>{ //所有创建
+ return this.createService(group).then((data)=>{
+ console.log(data);
+ let updata = (prodata)=>{
+ allprogress[prodata.operationId] = prodata.progress;
+ let average = ((arr)=>{return eval(arr.join("+"))/arr.length})(Object.values(allprogress))
+ newData.rate = Math.floor(average/3) + stageNum;
+ }
+ let queryParams = {serviceId:data["serviceId"],operationId:data["operationId"]};
+ querypros.push(this.queryProgress(queryParams,updata))
+ })
+ })
+
+ return new Promise((res)=>{
+ Promise.all(createPros).then(()=>{ //所有创建好之后querypros中查询进度才全都添加完毕
+ Promise.all(querypros).then((data)=>{
+ console.log(data);
+ res("site--begin");
+ })
+ })
+ })
+ }).then((data)=>{
+ console.log(data);
+ stageNum = newData.rate; //阶段进度值更新;
+ let allprogress = {};
+ let querypros = []; //所有查询
+ let createPros = obj.sitebody.map((group)=>{ //所有创建
+ return this.createService(group).then((data)=>{
+ console.log(data);
+ let updata = (prodata)=>{
+ allprogress[prodata.operationId] = prodata.progress;
+ let average =((arr)=>{return eval(arr.join("+"))/arr.length})(Object.values(allprogress))
+ newData.rate = Math.floor(average/3) + stageNum;
+ }
+ let queryParams = {serviceId:data["serviceId"],operationId:data["operationId"]};
+ querypros.push(this.queryProgress(queryParams,updata))
+ })
+ })
+ console.log(createPros);
+ Promise.all(createPros).then(()=>{ //所有创建好之后querypros中查询进度才全都添加完毕
+ Promise.all(querypros).then((data)=>{
+ console.log(data);
+ newData.rate = 100;
+ newData.status = "completed";
+ setTimeout(()=>{
+ this.getTableData();
+ },1000)
+ })
+ })
+ })
+
+ }
+
+ createService(params){
+ let mypromise = new Promise((res,rej)=>{
+ this.myhttp.createInstance(params)
+ .subscribe((data)=>{
+
+ res(data.service);
+ })
+ })
+ return mypromise;
+ }
+
+ queryProgress(obj,callback){
+ let mypromise = new Promise((res,rej)=>{
+ // let data = {
+ // operationStatus:{
+ // "operationId": "XXXXXX",
+ // "operation": "create|delete|update|scale",
+ // "result": "finished|error|processing",
+ // "reason": "",
+ // "userId": "",
+ // "operationContent": "Be creating pop.",
+ // "progress": 0,
+ // "operateAt": "",
+ // "finishedAt": ""
+ // }
+ // }
+ let requery = ()=>{
+ this.myhttp.getProgress(obj)
+ .subscribe((data)=>{
+ if(data.operationStatus.progress==undefined){
+ console.log(data);
+ setTimeout(()=>{
+ requery();
+ },5000)
+ return false;
+ }
+ if(data.operationStatus.progress < 100){
+ callback(data.operationStatus);
+ setTimeout(()=>{
+ requery();
+ },5000)
+ }else {
+ res(data.operationStatus);
+ }
+ })
+ // setTimeout(()=>{
+ // console.log(data.operationStatus.progress)
+ // data.operationStatus.progress++;
+ // if(data.operationStatus.progress<100){
+ // callback(data.operationStatus);
+ // requery()
+ // }else{
+ // callback(data.operationStatus);
+ // res(data.operationStatus)
+ // }
+ // },100)
+ }
+ requery();
+ })
+ return mypromise;
+ }
+
+
+ // 名字转换参数匹配 --> 传给子组件用
+ namesTranslate:Object;
+ inputNamests(){
+ this.myhttp.inputNamesTransform()
+ .subscribe((data)=>{
+ this.namesTranslate = data;
+ })
+ }
+
+}
diff --git a/usecaseui-portal/src/app/services/services.component.html b/usecaseui-portal/src/app/services/services.component.html
new file mode 100644
index 00000000..c4fddfc0
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services.component.html
@@ -0,0 +1,18 @@
+<!--
+ Copyright (C) 2018 CMCC, Inc. and others. All rights reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<p>
+ services works!
+</p>
diff --git a/usecaseui-portal/src/app/services/services.component.less b/usecaseui-portal/src/app/services/services.component.less
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services.component.less
diff --git a/usecaseui-portal/src/app/services/services.component.spec.ts b/usecaseui-portal/src/app/services/services.component.spec.ts
new file mode 100644
index 00000000..2e76b9f9
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ServicesComponent } from './services.component';
+
+describe('ServicesComponent', () => {
+ let component: ServicesComponent;
+ let fixture: ComponentFixture<ServicesComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ServicesComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServicesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/usecaseui-portal/src/app/services/services.component.ts b/usecaseui-portal/src/app/services/services.component.ts
new file mode 100644
index 00000000..eec235b4
--- /dev/null
+++ b/usecaseui-portal/src/app/services/services.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-services',
+ templateUrl: './services.component.html',
+ styleUrls: ['./services.component.less']
+})
+export class ServicesComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}