@@ -13,11 +13,5 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - -<!--<nav> - <app-navbar></app-navbar> -</nav> -<main> - <router-outlet></router-outlet> -</main>-->
\ No newline at end of file + +<sdc-loader global="true" name="general" testId="loader"></sdc-loader> @@ -7,9 +7,9 @@ * 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. @@ -17,18 +17,16 @@ * limitations under the License. * ============LICENSE_END========================================================= */ - -import { Component, Inject, ViewContainerRef } from '@angular/core'; +import { Component, ViewContainerRef} from '@angular/core'; import { AuthenticationService } from './services/authentication.service'; @Component({ selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] + templateUrl: './app.component.html' }) export class AppComponent { - constructor(auth:AuthenticationService, public viewContainerRef:ViewContainerRef){ + constructor(auth: AuthenticationService, public viewContainerRef: ViewContainerRef){ } @@ -3,7 +3,6 @@ * SDC * ================================================================================ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. - * Modifications Copyright (C) 2019 Nokia. 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. @@ -19,6 +18,7 @@ * ============LICENSE_END========================================================= */ +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule, APP_INITIALIZER } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -26,84 +26,110 @@ import { forwardRef } from '@angular/core'; import { AppComponent } from './app.component'; import { UpgradeAdapter } from '@angular/upgrade'; import { UpgradeModule } from '@angular/upgrade/static'; -import { SdcUiComponentsModule, SdcUiComponents } from "sdc-ui/lib/angular"; +import { SdcUiComponentsModule, SdcUiComponents } from 'onap-ui-angular'; import { PropertiesAssignmentModule } from './pages/properties-assignment/properties-assignment.module'; -import { PropertyCreatorModule } from './pages/properties-assignment/property-creator/property-creator.module'; import { - DataTypesServiceProvider, SharingServiceProvider, CookieServiceProvider, StateServiceFactory, - StateParamsServiceFactory, CacheServiceProvider, EventListenerServiceProvider, ScopeServiceFactory, + DataTypesServiceProvider, CookieServiceProvider, StateServiceFactory, + StateParamsServiceFactory, ScopeServiceFactory, NotificationServiceProvider, ComponentFactoryProvider -} from "./utils/ng1-upgraded-provider"; -import { ConfigService } from "./services/config.service"; -import { HttpModule } from '@angular/http'; -import { HttpService } from './services/http.service'; +} from './utils/ng1-upgraded-provider'; +import { ConfigService } from './services/config.service'; import { AuthenticationService } from './services/authentication.service'; -import { Cookie2Service } from "./services/cookie.service"; -import { ComponentServiceNg2 } from "./services/component-services/component.service"; -import { ComponentServiceFactoryNg2 } from "./services/component-services/component.service.factory"; -import { ServiceServiceNg2 } from "./services/component-services/service.service"; -import { ComponentInstanceServiceNg2 } from "./services/component-instance-services/component-instance.service"; -import { WorkflowServiceNg2 } from './services/workflow.service'; -import {ToscaTypesServiceNg2} from "./services/tosca-types.service"; -import { ModalService } from "./services/modal.service"; -import { UiElementsModule } from "./components/ui/ui-elements.module"; -import { ConnectionWizardModule } from "./pages/connection-wizard/connection-wizard.module"; -import { InterfaceOperationModule } from "./pages/interface-operation/interface-operation.module"; -import { OperationCreatorModule } from "./pages/interface-operation/operation-creator/operation-creator.module"; -import { LayoutModule } from "./components/layout/layout.module"; -import { UserService } from "./services/user.service"; -import { DynamicComponentService } from "./services/dynamic-component.service"; -import { SdcConfig } from "./config/sdc-config.config"; -import { SdcMenu } from "./config/sdc-menu.config"; -import { TranslateModule } from "./shared/translator/translate.module"; -import { TranslationServiceConfig } from "./config/translation.service.config"; -import { MultilineEllipsisModule } from "./shared/multiline-ellipsis/multiline-ellipsis.module"; -import { ServicePathCreatorModule } from './pages/service-path-creator/service-path-creator.module'; -import { ServicePathsListModule } from './pages/service-paths-list/service-paths-list.module'; -import { ServicePathModule } from 'app/ng2/components/logic/service-path/service-path.module'; -import { ServicePathSelectorModule } from 'app/ng2/components/logic/service-path-selector/service-path-selector.module'; -import { ServiceConsumptionModule } from 'app/ng2/components/logic/service-consumption/service-consumption.module'; -import { ServiceConsumptionCreatorModule } from './pages/service-consumption-editor/service-consumption-editor.module'; -import {ServiceDependenciesModule} from 'app/ng2/components/logic/service-dependencies/service-dependencies.module'; -import {ServiceDependenciesEditorModule} from './pages/service-dependencies-editor/service-dependencies-editor.module'; -import { CompositionPanelModule } from 'app/ng2/pages/composition/panel/panel.module'; -import { WindowRef } from "./services/window.service"; -import {ArchiveService} from "./services/archive.service"; +import { Cookie2Service } from './services/cookie.service'; +import { ComponentServiceNg2 } from './services/component-services/component.service'; +import { ComponentServiceFactoryNg2 } from './services/component-services/component.service.factory'; +import { ServiceServiceNg2 } from './services/component-services/service.service'; +import { ComponentInstanceServiceNg2 } from './services/component-instance-services/component-instance.service'; +import { ModalService } from './services/modal.service'; +import { UiElementsModule } from './components/ui/ui-elements.module'; +import { ConnectionWizardModule } from './pages/composition/graph/connection-wizard/connection-wizard.module'; +import { InterfaceOperationModule } from './pages/interface-operation/interface-operation.module'; +import { OperationCreatorModule } from './pages/interface-operation/operation-creator/operation-creator.module'; +import { LayoutModule } from './components/layout/layout.module'; +import { UserService } from './services/user.service'; +import { DynamicComponentService } from './services/dynamic-component.service'; +import { SdcConfig } from './config/sdc-config.config'; +import { SdcMenu } from './config/sdc-menu.config'; +import { TranslateModule } from './shared/translator/translate.module'; +import { TranslationServiceConfig } from './config/translation.service.config'; +import { MultilineEllipsisModule } from './shared/multiline-ellipsis/multiline-ellipsis.module'; +import { ServicePathCreatorModule } from './pages/composition/graph/service-path-creator/service-path-creator.module'; +import { ServicePathsListModule } from './pages/composition/graph/service-paths-list/service-paths-list.module'; +import { ServicePathSelectorModule } from 'app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module'; +import { CompositionPanelModule } from 'app/ng2/pages/composition/panel/composition-panel.module'; +import { CatalogModule } from './pages/catalog/catalog.module'; +import { HomeModule } from './pages/home/home.module'; +import { WindowRef } from './services/window.service'; +import { CatalogService } from './services/catalog.service'; import { ModalsHandlerProvider } from './utils/ng1-upgraded-provider'; -import {PluginFrameModule} from "./components/ui/plugin/plugin-frame.module"; -import {PluginsService} from "./services/plugins.service"; -import {EventBusService} from "./services/event-bus.service"; -import {GroupsService} from "./services/groups.service"; -import {PoliciesService} from "./services/policies.service"; -import {AutomatedUpgradeService} from "./pages/automated-upgrade/automated-upgrade.service"; -import {AutomatedUpgradeModule} from "./pages/automated-upgrade/automated-upgrade.module"; -import {RequirementsEditorModule} from "./pages/req-and-capabilities-editor/requirements-editor/requirements-editor.module" -import {CapabilitiesEditorModule} from "./pages/req-and-capabilities-editor/capabilities-editor/capabilities-editor.module" -import {GenericArtifactBrowserModule} from "./components/logic/generic-artifact-browser/generic-artifact-browser.module"; -import {GabService} from "./services/gab.service"; -import {DeclareListModule} from "./pages/properties-assignment/declare-list/declare-list.module" +import { PluginFrameModule } from './components/ui/plugin/plugin-frame.module'; +import { PluginsService } from './services/plugins.service'; +import { EventBusService } from './services/event-bus.service'; +import { GroupsService } from './services/groups.service'; +import { PoliciesService } from './services/policies.service'; +import { AutomatedUpgradeService } from './pages/automated-upgrade/automated-upgrade.service'; +import { AutomatedUpgradeModule } from './pages/automated-upgrade/automated-upgrade.module'; +import {WorkspaceModule} from './pages/workspace/workspace.module'; +import { ModalsModule } from './components/modals/modals.module'; +import { SharingService, CacheService, HomeService } from 'app/services-ng2'; +import { IUserProperties } from 'app/models'; +import { PluginsModule } from './pages/plugins/plugins-module'; +import {WorkspaceNg1BridgeService} from './pages/workspace/workspace-ng1-bridge-service'; +import {NgxsModule} from '@ngxs/store'; +import {NgxsLoggerPluginModule} from '@ngxs/logger-plugin'; +import {NgxsReduxDevtoolsPluginModule} from '@ngxs/devtools-plugin'; +import {EventListenerService} from '../services/event-listener-service'; +import { HttpClientModule } from '@angular/common/http'; +import { httpInterceptorProviders } from './http-interceptor'; +import { HttpHelperService } from './services/http-hepler.service'; +import { ModulesService } from "./services/modules.service"; +import { TranslateService } from 'app/ng2/shared/translator/translate.service'; +import { FileUtilsService } from './services/file-utils.service'; +import { ImportVSPService } from './components/modals/onboarding-modal/import-vsp.service'; +import { OnboardingService } from './services/onboarding.service'; +import { ServiceConsumptionCreatorModule } from './pages/service-consumption-editor/service-consumption-editor.module'; +import { ServiceDependenciesModule } from './components/logic/service-dependencies/service-dependencies.module'; +import { ServiceDependenciesEditorModule } from './pages/service-dependencies-editor/service-dependencies-editor.module'; +import { PropertyCreatorModule } from './pages/properties-assignment/property-creator/property-creator.module'; +import { DeclareListModule } from './pages/properties-assignment/declare-list/declare-list.module'; +import { WorkflowServiceNg2 } from './services/workflow.service'; +import { ToscaTypesServiceNg2 } from "./services/tosca-types.service"; + + +declare const __ENV__: string; export const upgradeAdapter = new UpgradeAdapter(forwardRef(() => AppModule)); -export function configServiceFactory(config: ConfigService) { +export function configServiceFactory(config: ConfigService, authService: AuthenticationService, eventListener: EventListenerService) { + return () => { - return Promise.all([ - config.loadValidationConfiguration(), - config.loadPluginsConfiguration() - ]); - } + return authService.authenticate().toPromise() + .then((userInfo: IUserProperties) => { + authService.setLoggedinUser(userInfo); + return Promise.all([ + config.loadSdcSetupData(), + config.loadValidationConfiguration(), + config.loadPluginsConfiguration(), + ]) + }).then(() => { + eventListener.notifyObservers('ON_FINISH_LOADING'); + }) + .catch(() => { + console.log('AUTH FAILED! from app module'); + }); + }; } - @NgModule({ declarations: [ AppComponent ], imports: [ + BrowserAnimationsModule, BrowserModule, UpgradeModule, FormsModule, - HttpModule, + HttpClientModule, LayoutModule, TranslateModule, MultilineEllipsisModule, @@ -111,80 +137,90 @@ export function configServiceFactory(config: ConfigService) { CompositionPanelModule, SdcUiComponentsModule, AutomatedUpgradeModule, - //We need to import them here since we use them in angular1 + + // We need to import them here since we use them in angular1 ConnectionWizardModule, PropertiesAssignmentModule, PropertyCreatorModule, DeclareListModule, PluginFrameModule, + PluginsModule, InterfaceOperationModule, OperationCreatorModule, ServicePathCreatorModule, ServicePathsListModule, - ServicePathModule, ServicePathSelectorModule, - ServiceConsumptionModule, ServiceConsumptionCreatorModule, ServiceDependenciesModule, ServiceDependenciesEditorModule, - RequirementsEditorModule, - CapabilitiesEditorModule, - GenericArtifactBrowserModule + WorkspaceModule, + ModalsModule, + CatalogModule, + HomeModule, + NgxsModule.forRoot([]), + NgxsLoggerPluginModule.forRoot({ logger: console, collapsed: false }), + NgxsReduxDevtoolsPluginModule.forRoot({ + disabled: __ENV__ === 'prod' + }) ], exports: [], entryComponents: [ - // *** sdc-ui components to be used as downgraded: - SdcUiComponents.SvgIconComponent ], providers: [ WindowRef, + httpInterceptorProviders, DataTypesServiceProvider, - SharingServiceProvider, + SharingService, + CacheService, + HomeService, ComponentFactoryProvider, CookieServiceProvider, StateServiceFactory, StateParamsServiceFactory, ScopeServiceFactory, - CacheServiceProvider, - EventListenerServiceProvider, NotificationServiceProvider, ModalsHandlerProvider, - AuthenticationService, + UserService, Cookie2Service, ConfigService, ComponentServiceNg2, ComponentServiceFactoryNg2, ModalService, + ImportVSPService, + OnboardingService, ServiceServiceNg2, AutomatedUpgradeService, WorkflowServiceNg2, ToscaTypesServiceNg2, - HttpService, - UserService, + WorkspaceNg1BridgeService, + HttpHelperService, + AuthenticationService, PoliciesService, GroupsService, + ModulesService, DynamicComponentService, SdcConfig, SdcMenu, ComponentInstanceServiceNg2, + EventListenerService, TranslationServiceConfig, + TranslateService, PluginsService, - GabService, - ArchiveService, + CatalogService, EventBusService, + FileUtilsService, { provide: APP_INITIALIZER, useFactory: configServiceFactory, - deps: [ConfigService], + deps: [ConfigService, AuthenticationService, EventListenerService], multi: true }, ], bootstrap: [AppComponent] }) - export class AppModule { - constructor(public upgrade: UpgradeModule, public eventBusService:EventBusService) { + constructor(public upgrade: UpgradeModule) { } } @@ -1,21 +1,19 @@ /*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= + <!-- + ~ Copyright (C) 2018 AT&T Intellectual Property. 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. + --> */ import { Component, Input } from '@angular/core'; @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`artifact form component should match current snapshot of artifact form component 1`] = ` +<artifact-form + artifactTypesOptions={[Function Array]} + cacheService={[Function Object]} + initArtifactTypes={[Function Function]} + onDescriptionChange={[Function Function]} + onLabelChange={[Function Function]} + onTypeChange={[Function Function]} + onUploadFile={[Function Function]} + onValidationChange={[Function Subject]} + verifyTypeAndFileWereFilled={[Function Function]} +> + <form + class="artifact-form" + name="artifactForm" + novalidate="" + > + <onap-file-upload /> + <div + class="artifact-form-container" + > + + <div + class="right-form-container" + > + <sdc-textarea + label="Description" + testid="description" + /> + <sdc-validation> + <sdc-required-validator /> + <sdc-regex-validator /> + </sdc-validation> + </div> + </div> + </form> +</artifact-form> +`; @@ -0,0 +1,55 @@ +<form class="artifact-form" novalidate name="artifactForm"> + + <onap-file-upload [disabled]="isViewOnly || showTypeFields && !artifact.artifactType" [convertToBase64]="true" [(value)]="artifact.artifactName" (fileUpload)="onUploadFile($event)" [placeHolder]="'Select File'" [label]="'Upload File'" [testId]="'fileUploadElement'" [required]="true"> + + </onap-file-upload> + <div class="artifact-form-container"> + <div class="left-form-container" *ngIf="showTypeFields"> + <sdc-input #artifactLabel + required="true" + [(value)]="artifact.artifactLabel" + [maxLength]="25" + [label]="'Artifact Label'" + [disabled]="isViewOnly || artifact && artifact.uniqueId" + [testId]="'artifactLabel'" + (keyup)="verifyTypeAndFileWereFilled()"> + </sdc-input> + <sdc-validation [validateElement]="artifactLabel" (validityChanged)="onLabelChange($event)"> + <sdc-required-validator [message]="'ADD_ARTIFACT_ERROR_LABEL_REQUIRED' | translate"></sdc-required-validator> + <sdc-regex-validator [message]="'VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED' | translate" + [pattern]="validationPatterns['label']"></sdc-regex-validator> + </sdc-validation> + + <sdc-dropdown #artifactType [disabled]="isViewOnly || artifact && artifact.uniqueId" label="Type" [required]="true" + [selectedOption]="selectedFileType" placeHolder="Please choose type" (changed)="onTypeChange($event)" + [options]="artifactTypesOptions" [testId]="'artifacttype'"></sdc-dropdown> + <sdc-validation [validateElement]="artifactType"> + <sdc-required-validator + [message]="'ADD_ARTIFACT_ERROR_TYPE_REQUIRED' | translate"></sdc-required-validator> + </sdc-validation> + </div> + + <div class="right-form-container"> + <sdc-textarea #artifactDescription + [(value)]="artifact.description" + [required]="true" + testId="description" + [maxLength]="256" + label="Description" + [disabled]="isViewOnly" + (keyup)="verifyTypeAndFileWereFilled()"> + </sdc-textarea> + <sdc-validation [validateElement]="artifactDescription" (validityChanged)="onDescriptionChange($event)"> + <sdc-required-validator + [message]="'ADD_ARTIFACT_ERROR_DESCRIPTION_REQUIRED' | translate:{'field': 'Message' }"></sdc-required-validator> + <sdc-regex-validator [message]="'VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED' | translate" + [pattern]="validationPatterns['comment']"></sdc-regex-validator> + </sdc-validation> + </div> + </div> +</form> + +<div *ngIf="artifact && artifact.esId"> + <div>UUID: {{artifact.artifactUUID}}</div> + <div>Version: {{artifact.artifactVersion}}</div> +</div>
\ No newline at end of file @@ -0,0 +1,23 @@ +.artifact-form { + display: flex; + justify-content: space-between; + flex-direction: column; + + .artifact-form-container { + margin-top: 10px; + display: flex; + flex-direction: row; + .left-form-container { + flex: 1; + padding-right: 10px; + } + + .right-form-container { + flex: 1; + + /deep/.sdc-textarea .sdc-textarea__textarea{ + min-height: 110px; + } + } + } +}
\ No newline at end of file @@ -0,0 +1,242 @@ +import {ArtifactFormComponent} from "./artifact-form.component"; +import {async, ComponentFixture} from "@angular/core/testing"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {CacheService} from "../../../services/cache.service"; +import {TranslateService} from "../../../shared/translator/translate.service"; +import {ArtifactModel} from "../../../../models/artifacts"; +import {IDropDownOption} from "onap-ui-angular/dist/form-elements/dropdown/dropdown-models"; +import {Observable, Subject} from "rxjs"; +import {getValue} from "@ngxs/store"; + + +describe('artifact form component', () => { + + let fixture: ComponentFixture<ArtifactFormComponent>; + let cacheServiceMock: Partial<CacheService>; + let onValidationChangeMock: Partial<Subject<boolean>>; + + let artifactModel = new ArtifactModel(); + + + beforeEach( + async(() => { + + onValidationChangeMock = { + next: jest.fn() + } + + cacheServiceMock = { + contains: jest.fn(), + remove: jest.fn(), + set: jest.fn(), + get: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ArtifactFormComponent], + imports: [TranslateModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: CacheService, useValue: cacheServiceMock}, + {provide: TranslateService, useValue: {}} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ArtifactFormComponent); + }); + }) + ); + + + it('should verify initArtifactTypes for DEPLOYMENT and ArtifactType = HEAT_ENV', () =>{ + + cacheServiceMock.get.mockImplementation(() =>{ + return { + artifacts: { + deployment:{ + resourceInstanceDeploymentArtifacts: [{name: "Dummy Value Returned from ui api"}], + } + } + } + }); + + fixture.componentInstance.artifactType = 'DEPLOYMENT'; + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.artifactType = 'HEAT_ENV'; + + fixture.componentInstance.initArtifactTypes(); + + expect(fixture.componentInstance.selectedFileType).toEqual(undefined); + + }); + + it('should verify initArtifactTypes for DEPLOYMENT and ComponentType = RESOURCE', () =>{ + + cacheServiceMock.get.mockImplementation(() =>{ + return { + artifacts: { + deployment:{ + resourceDeploymentArtifacts: [{name: "Dummy Value Returned from ui api"}], + } + } + } + }); + + fixture.componentInstance.artifactType = 'DEPLOYMENT'; + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.componentType = 'RESOURCE'; + + fixture.componentInstance.initArtifactTypes(); + + expect(fixture.componentInstance.selectedFileType).toEqual(undefined); + + }); + + it('should verify initArtifactTypes for DEPLOYMENT and NOT ComponentType = RESOURCE OR NOT ArtifactType = HEAT_ENV', () =>{ + + cacheServiceMock.get.mockImplementation(() =>{ + return { + artifacts: { + deployment:{ + serviceDeploymentArtifacts: [{name: "Dummy Value Returned from ui api"}], + } + } + } + }); + + fixture.componentInstance.artifactType = 'DEPLOYMENT'; + fixture.componentInstance.artifact = artifactModel; + // fixture.componentInstance.componentType = 'RESOURCE'; + + fixture.componentInstance.initArtifactTypes(); + + expect(fixture.componentInstance.selectedFileType).toEqual(undefined); + + }); + + it('should verify initArtifactTypes for INFORMATION', () =>{ + + cacheServiceMock.get.mockImplementation(() =>{ + return { + artifacts: { + other: [{name: "Val1"}, {name: "ExpectedValToBeSelected"}, {name: "Val3"}] + } + } + }); + + fixture.componentInstance.artifactType = 'INFORMATIONAL'; + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.artifactType = 'ExpectedValToBeSelected'; + + fixture.componentInstance.initArtifactTypes(); + + expect(fixture.componentInstance.selectedFileType).toEqual({"label": "ExpectedValToBeSelected", "value": "ExpectedValToBeSelected"}); + + }); + + + it('should match current snapshot of artifact form component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should verify onUploadFile -> file gets file name', () => { + let file = { + filename:'dummyFileName' + } + + fixture.componentInstance.verifyTypeAndFileWereFilled = jest.fn(); + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.onUploadFile(file); + + expect(fixture.componentInstance.artifact.artifactName).toBe('dummyFileName'); + + const spy1 = jest.spyOn(fixture.componentInstance,'verifyTypeAndFileWereFilled'); + expect(spy1).toHaveBeenCalled(); + }); + + it('should verify onUploadFile -> file is null', () => { + let file = null; + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.onUploadFile(file); + + expect(fixture.componentInstance.artifact.artifactName).toBe(null); + }); + + it('should verify onTypeChange -> verifyTypeAndFileWereFilled is being called', () => { + let selectedFileType:IDropDownOption; + selectedFileType = {"label": "dummyLabel", "value": "dummyValue"}; + fixture.componentInstance.verifyTypeAndFileWereFilled = jest.fn(); + + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.onTypeChange(selectedFileType); + + const spy1 = jest.spyOn(fixture.componentInstance,'verifyTypeAndFileWereFilled'); + expect(spy1).toHaveBeenCalled(); + }); + + it('should verify onDescriptionChange -> verifyTypeAndFileWereFilled is being called', () => { + fixture.componentInstance.verifyTypeAndFileWereFilled = jest.fn(); + fixture.componentInstance.onValidationChange.next = jest.fn(() => true); + + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.onDescriptionChange(); + + const spy1 = jest.spyOn(fixture.componentInstance,'verifyTypeAndFileWereFilled'); + expect(spy1).toHaveBeenCalled(); + }); + + it('should verify onLabelChange -> verifyTypeAndFileWereFilled is being called', () => { + fixture.componentInstance.verifyTypeAndFileWereFilled = jest.fn(); + fixture.componentInstance.onValidationChange.next = jest.fn(() => true); + + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.onLabelChange(true); + + const spy1 = jest.spyOn(fixture.componentInstance,'verifyTypeAndFileWereFilled'); + expect(spy1).toHaveBeenCalled(); + }); + + it('should verify verifyTypeAndFileWereFilled -> verify branch this.artifact.artifactType !== \'DEPLOYMENT\' ==>> onValidationChange.next(false)', () => { + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.artifactType = 'NOT_DEPLOYMENT'; + fixture.componentInstance.artifact.mandatory = true; + fixture.componentInstance.descriptionIsValid = false; + + let onValidationChangeResult; + + fixture.componentInstance.onValidationChange.subscribe((data) => { + onValidationChangeResult = data; + // console.log("Subscriber got data >>>>> "+ data); + }); + + fixture.componentInstance.verifyTypeAndFileWereFilled(); + + expect(onValidationChangeResult).toBe(false); + }); + + it('should verify verifyTypeAndFileWereFilled -> verify branch this.artifact.artifactType !== \'DEPLOYMENT\' ==>> onValidationChange.next(true)', () => { + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.artifactType = 'NOT_DEPLOYMENT'; + fixture.componentInstance.artifact.mandatory = true; + fixture.componentInstance.artifact.artifactName = 'Something'; + fixture.componentInstance.descriptionIsValid = true; + + let onValidationChangeResult; + + fixture.componentInstance.onValidationChange.subscribe((data) => { + onValidationChangeResult = data; + // console.log("Subscriber got data >>>>> "+ data); + }); + + fixture.componentInstance.verifyTypeAndFileWereFilled(); + + expect(onValidationChangeResult).toBe(true); + }); + + +});
\ No newline at end of file @@ -0,0 +1,132 @@ +/** + * Created by rc2122 on 5/31/2018. + */ +import { Component, Input } from '@angular/core'; +import * as _ from 'lodash'; +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; +import { Subject } from 'rxjs/Subject'; +import { ArtifactModel } from '../../../../models'; +import { ArtifactType, ComponentType } from '../../../../utils'; +import { Dictionary } from '../../../../utils/dictionary/dictionary'; +import { CacheService } from '../../../services/cache.service'; + +@Component({ + selector: 'artifact-form', + templateUrl: './artifact-form.component.html', + styleUrls: ['./artifact-form.component.less'] +}) +export class ArtifactFormComponent { + + @Input() artifact: ArtifactModel; + @Input() artifactType: ArtifactType; + @Input() componentType: string; + @Input() instanceId: string; + @Input() isViewOnly: boolean; + + public artifactTypesOptions: IDropDownOption[] = []; + public validationPatterns: Dictionary<string, RegExp>; + public selectedFileType: IDropDownOption; + public showTypeFields: boolean; + private onValidationChange: Subject<boolean> = new Subject(); + private descriptionIsValid: boolean; + private labelIsValid: boolean; + + constructor(private cacheService: CacheService) { + } + + ngOnInit(): void { + this.validationPatterns = this.cacheService.get('validation').validationPatterns; + this.initArtifactTypes(); + this.artifact.artifactGroupType = this.artifact.artifactGroupType || this.artifactType.toString(); + this.showTypeFields = (this.artifact.artifactGroupType === 'DEPLOYMENT' || !this.artifact.mandatory) && this.artifact.artifactGroupType !== 'SERVICE_API'; + } + + public onTypeChange = (selectedFileType: IDropDownOption) => { + this.artifact.artifactType = selectedFileType.value; + this.verifyTypeAndFileWereFilled(); + } + + public onUploadFile = (file) => { + if (file) { + this.artifact.artifactName = file.filename; + this.artifact.payloadData = file.base64; + console.log('FILE UPLOADED', file); + } else { + this.artifact.artifactName = null; + } + this.verifyTypeAndFileWereFilled(); + } + + private initArtifactTypes = (): void => { + const artifactTypes: any = this.cacheService.get('UIConfiguration'); + let validExtensions: string[]; + let artifactTypesList: string[]; + + switch (this.artifactType) { + case ArtifactType.DEPLOYMENT: + if (this.artifact.artifactType === ArtifactType.HEAT_ENV || this.instanceId) { + validExtensions = artifactTypes.artifacts.deployment.resourceInstanceDeploymentArtifacts; + } else if (this.componentType === ComponentType.RESOURCE) { + validExtensions = artifactTypes.artifacts.deployment.resourceDeploymentArtifacts; + } else { + validExtensions = artifactTypes.artifacts.deployment.serviceDeploymentArtifacts; + } + if (validExtensions) { + artifactTypesList = Object.keys(validExtensions); + } + break; + case ArtifactType.INFORMATION: + artifactTypesList = artifactTypes.artifacts.other.map((element: any) => { + return element.name; + }); + _.remove(artifactTypesList, (item: string) => { + return _.has(ArtifactType.THIRD_PARTY_RESERVED_TYPES, item) || + _.has(ArtifactType.TOSCA, item); + }); + break; + } + + _.forEach(artifactTypesList, (artifactType: string) => { + this.artifactTypesOptions.push({ label: artifactType, value: artifactType }); + }); + + this.selectedFileType = _.find(this.artifactTypesOptions, (artifactType) => { + return artifactType.value === this.artifact.artifactType; + }); + + } + + // Verify that the Type and the Name (file) are filled in the Modal + // For Description and Label - I used this.descriptionIsValid:boolean & this.labelIsValid:boolean as part of the sdc-validation Element + private verifyTypeAndFileWereFilled = () => { + if (this.artifact.artifactType === 'DEPLOYMENT' || !this.artifact.mandatory && this.artifact.artifactGroupType !== 'SERVICE_API') { + // In case of all fields are required: + // File, Description, Type and Label + if (this.artifact.artifactType && this.artifact.artifactName && this.descriptionIsValid && this.labelIsValid) { + this.onValidationChange.next(true); + } else { + this.onValidationChange.next(false); + } + } else { + // In case of like Information Artifact + // Only file and description are required + if (this.descriptionIsValid && this.artifact.artifactName) { + this.onValidationChange.next(true); + } else { + this.onValidationChange.next(false); + } + } + } + + // sdc-validation for Description + private onDescriptionChange = (isValid: boolean): void => { + this.descriptionIsValid = isValid; + this.onValidationChange.next(isValid) && this.verifyTypeAndFileWereFilled(); + } + + // sdc-validation for Label + private onLabelChange = (isValid: boolean): void => { + this.labelIsValid = isValid; + this.onValidationChange.next(isValid) && this.verifyTypeAndFileWereFilled(); + } +} @@ -0,0 +1,21 @@ +/** + * Created by rc2122 on 5/24/2018. + */ +import { NgModule } from "@angular/core"; +import { TranslateModule } from "app/ng2/shared/translator/translate.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { CommonModule } from '@angular/common'; +import {ArtifactFormComponent} from "./artifact-form.component"; + +@NgModule({ + declarations: [ArtifactFormComponent], + imports: [TranslateModule, + SdcUiComponentsModule, + CommonModule], + exports: [ArtifactFormComponent], + entryComponents: [ArtifactFormComponent] +}) + + +export class ArtifactFormModule { +}
\ No newline at end of file @@ -0,0 +1,175 @@ +import { Injectable } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ArtifactModel } from '../../../../models'; +import { ArtifactGroupType, ArtifactType } from '../../../../utils/constants'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { CreateOrUpdateArtifactAction, DeleteArtifactAction } from '../../../store/actions/artifacts.action'; +import { EnvParamsComponent } from '../env-params/env-params.component'; +import { ArtifactFormComponent } from './artifact-form.component'; + +import { + CreateInstanceArtifactAction, + DeleteInstanceArtifactAction, + UpdateInstanceArtifactAction +} from '../../../store/actions/instance-artifacts.actions'; + +@Injectable() +export class ArtifactsService { + + constructor(private serviceLoader: SdcUiServices.LoaderService, + private modalService: SdcUiServices.ModalService, + private topologyTemplateService: TopologyTemplateService, + private translateService: TranslateService, + private store: Store) { + } + + public dispatchArtifactAction = (componentId: string, componentType: string, artifact: ArtifactModel, artifactType: ArtifactGroupType, instanceId: string) => { + const artifactObj = { + componentType, + componentId, + instanceId, + artifact + }; + + // Create or update instance artifact + if (instanceId) { + if (!artifact.uniqueId) { + // create instance artifact + return this.store.dispatch(new CreateInstanceArtifactAction(artifactObj)); + } else { + // update instance artifact + return this.store.dispatch(new UpdateInstanceArtifactAction(artifactObj)); + } + } else { + // Create or update artifact + return this.store.dispatch(new CreateOrUpdateArtifactAction(artifactObj)); + } + } + + public openArtifactModal = (componentId: string, componentType: string, artifact: ArtifactModel, artifactType: ArtifactGroupType, isViewOnly?: boolean, instanceId?: string) => { + + let modalInstance; + + const onOkPressed = () => { + const updatedArtifact = modalInstance.innerModalContent.instance.artifact; + this.serviceLoader.activate(); + this.dispatchArtifactAction(componentId, componentType, updatedArtifact, artifactType, instanceId) + .subscribe().add(() => this.serviceLoader.deactivate()); + }; + + const addOrUpdateArtifactModalConfig = { + title: (artifact && artifact.uniqueId) ? 'Update Artifact' : 'Create Artifact', + size: 'md', + type: SdcUiCommon.ModalType.custom, + testId: 'upgradeVspModal', + buttons: [ + { + id: 'done', + text: 'DONE', + disabled: isViewOnly, + size: 'Add Another', + closeModal: true, + callback: onOkPressed + }, + {text: 'CANCEL', size: 'sm', closeModal: true, type: 'secondary'} + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig; + + modalInstance = this.modalService.openCustomModal(addOrUpdateArtifactModalConfig, ArtifactFormComponent, { + artifact: new ArtifactModel(artifact), + artifactType, + instanceId, + componentType, + isViewOnly + }); + + if (!isViewOnly) { + modalInstance.innerModalContent.instance.onValidationChange.subscribe((isValid) => { + modalInstance.getButtonById('done').disabled = !isValid; + }); + } + } + + public openViewEnvParams(componentType: string, componentId: string, artifact: ArtifactModel, instanceId?: string) { + const envParamsModal = { + title: artifact.artifactDisplayName, + size: 'xl', + type: SdcUiCommon.ModalType.custom, + testId: 'viewEnvParams', + isDisabled: false, + } as SdcUiCommon.IModalConfig; + + this.modalService.openCustomModal(envParamsModal, EnvParamsComponent, { + isInstanceSelected: !!instanceId, // equals to instanceId ? true : false + artifact: new ArtifactModel(artifact), + isViewOnly: true + }); + } + + public openUpdateEnvParams(componentType: string, componentId: string, artifact: ArtifactModel, instanceId?: string) { + let modalInstance; + const onOkPressed = () => { + const updatedArtifact = modalInstance.innerModalContent.instance.artifact; + this.serviceLoader.activate(); + this.dispatchArtifactAction(componentId, componentType, updatedArtifact, ArtifactType.DEPLOYMENT, instanceId) + .subscribe().add(() => this.serviceLoader.deactivate()); + }; + + const envParamsModal = { + title: artifact.artifactDisplayName, + size: 'xl', + type: SdcUiCommon.ModalType.custom, + testId: 'envParams', + isDisabled: false, + buttons: [ + { + id: 'save', + text: 'Save', + spinner_position: 'left', + size: 'sm', + callback: onOkPressed, + closeModal: true + }, + {text: 'Cancel', size: 'sm', closeModal: true, type: 'secondary'} + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig; + + modalInstance = this.modalService.openCustomModal(envParamsModal, EnvParamsComponent, { + isInstanceSelected: !!instanceId, // equals to instanceId ? true : false + artifact: new ArtifactModel(artifact) + }); + + modalInstance.innerModalContent.instance.onValidationChange.subscribe((isValid) => { + modalInstance.getButtonById('save').disabled = !isValid; + }); + } + + public deleteArtifact = (componentType: string, componentId: string, artifact: ArtifactModel, instanceId?: string) => { + + const artifactObject = { + componentType, + componentId, + artifact, + instanceId + }; + + const onOkPressed: Function = () => { + this.serviceLoader.activate(); + this.store.dispatch((instanceId) ? new DeleteInstanceArtifactAction(artifactObject) : new DeleteArtifactAction(artifactObject)) + .subscribe().add(() => this.serviceLoader.deactivate()); + }; + + const title = this.translateService.translate('ARTIFACT_VIEW_DELETE_MODAL_TITLE'); + const text = this.translateService.translate('ARTIFACT_VIEW_DELETE_MODAL_TEXT', {name: artifact.artifactDisplayName}); + const okButton = { + testId: 'OK', + text: 'OK', + type: SdcUiCommon.ButtonType.warning, + callback: onOkPressed, + closeModal: true + } as SdcUiComponents.ModalButtonComponent; + this.modalService.openWarningModal(title, text, 'delete-information-artifact-modal', [okButton]); + } +} @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`environment parameters component should match current snapshot of env-params component 1`] = ` +<env-params + cacheService={[Function Object]} + clearCurrentValue={[Function Function]} + copiedWorkingArtifactHeatParameters={[Function Array]} + defaultDeploymentTimeout={[Function Number]} + displayRegexValid={[Function String]} + maxDeploymentTimeout={[Function Number]} + minDeploymentTimeout={[Function Number]} + onValidationChange={[Function Subject]} + onValidityChange={[Function Function]} + openPopOver={[Function Function]} + popoverService={[Function Object]} + textArea="undefined" +> + <div + class="filter-bar" + > + <sdc-filter-bar /> + </div><ngx-datatable + class="material ngx-datatable" + > + <div + visibilityobserver="" + > + + <datatable-body + class="datatable-body" + > + <datatable-selection> + + + + </datatable-selection> + </datatable-body> + + </div> + </ngx-datatable> +</env-params> +`; @@ -0,0 +1,71 @@ +<div class="filter-bar"> + <sdc-filter-bar + [placeHolder]="'Search...'" + [testId]="'search-env-param-name'" + (keyup)="updateFilter($event)"> + </sdc-filter-bar> +</div> + +<ngx-datatable + class='material' + [rows]='artifact.heatParameters' + [columnMode]="'flex'" + [headerHeight]="40" + [rowHeight]="'auto'" + [scrollbarV]="false"> + + <ngx-datatable-column name="Parameter" [flexGrow]="2"> + <ng-template let-row="row" ngx-datatable-cell-template prop="name"> + {{row.name}} + <span *ngIf="row.description.length > 0" class="info"> + <svg-icon [name]="'comment'" (click)="openPopOver('',row.description,{x:$event.pageX , y:$event.pageY },'bottom')"></svg-icon> + </span> + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column name="DefaultValue"[flexGrow]="1"> + <ng-template let-row="row" let-value="value" ngx-datatable-cell-template> + {{row.defaultValue}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column name="CurrentValue" [flexGrow]="2"> + <ng-template let-row="row" let-value="value" ngx-datatable-cell-template> + <sdc-input class="sdc-input-wrapper" #numberValidator + [placeHolder]="'Enter text'" + [isViewMode]="isViewOnly" + [size]="'medium'" + [(value)]=row.currentValue + [isIconClickable]="true" + (onRighIconClicked)="clearCurrentValue(row.name)" + [righIconName]="'trash-o'" + [testId] = "'value-field-of-' + row.name"> + </sdc-input> + + <sdc-validation [validateElement]="numberValidator" (validityChanged)="onValidityChange($event)" [disabled]="false" [testId]="_testId"> + <sdc-regex-validator *ngIf="displayRegexValid && row.type == 'number' && row.currentValue !== null" [message]="'Value should be of type number.'" [pattern]="displayRegexValid" [disabled]="false"></sdc-regex-validator> + </sdc-validation> + </ng-template> + </ngx-datatable-column> + +</ngx-datatable> + +<div *ngIf="isInstanceSelected" class="artifactTimeout"> + + <sdc-number-input + label="Deployment Timeout ({{minDeploymentTimeout}}-{{maxDeploymentTimeout}} minutes)" + [required]="true" + [disabled]="false" + name="artifactTimeout" + testId="deploymentTimeout" + value="{{artifact.timeout}}" + [maxValue]="maxDeploymentTimeout" + [minValue]="minDeploymentTimeout" + (valueChange)="timeoutChanged($event)" + [isViewMode]="isViewOnly" + [step]="1" + > + + </sdc-number-input> + +</div>
\ No newline at end of file @@ -0,0 +1,21 @@ +.filter-bar { + padding-bottom: 25px; +} + +:host ::ng-deep { + + .ngx-datatable { + //border: 1px solid red; + .datatable-body-cell { + .info { + float: right; + } + } + } +} + +.artifactTimeout{ + padding-top: 25px; + justify-content: start; + width: 230px; +} @@ -0,0 +1,98 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {EnvParamsComponent} from "./env-params.component"; +import {SdcUiServices, SdcUiCommon} from "onap-ui-angular"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ArtifactModel} from "../../../../models/artifacts"; +import { CacheService } from '../../../services/cache.service'; + +describe('environment parameters component', () => { + + let fixture: ComponentFixture<EnvParamsComponent>; + let popoverServiceMock: Partial<SdcUiServices.PopoverService>; + let regexPatterns: any; + + let artifactModel = new ArtifactModel(); + + let mockHeatParameters = [ + {currentValue: "1", defaultValue: null, description: "Description 1", empty:false, name: "Param1", ownerId: null, type: "string", uniqueId: null, envDisplayName:null, version: null, filterTerm:null}, + {currentValue: "2", defaultValue: null, description: "Description 2", empty:false, name: "Param2", ownerId: null, type: "string", uniqueId: null, envDisplayName:null, version: null, filterTerm:null}, + {currentValue: "3", defaultValue: null, description: "Description 3", empty:false, name: "Param3", ownerId: null, type: "string", uniqueId: null, envDisplayName:null, version: null, filterTerm:null} + ]; + + let keyboardEvent = new KeyboardEvent("keyup"); + + beforeEach( + async(() => { + + popoverServiceMock = { + createPopOver : jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [EnvParamsComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: SdcUiServices.PopoverService, useValue: popoverServiceMock }, + { provide: CacheService, useValue: { get: jest.fn() } } + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(EnvParamsComponent); + }); + }) + ); + + it('should match current snapshot of env-params component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should clear CurrentValue for a given name in the heat parameter', () => { + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.heatParameters = mockHeatParameters; + expect(fixture.componentInstance.artifact.heatParameters.length).toBe(3); + expect(fixture.componentInstance.artifact.heatParameters[0].currentValue).toBe("1"); + fixture.componentInstance.clearCurrentValue("Param1"); + expect(fixture.componentInstance.artifact.heatParameters[0].currentValue).toBe(""); + }); + + it("should update filter heatParameters so there won''t be any value to be displayed", () => { + + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.heatParameters = mockHeatParameters; + fixture.componentInstance.ngOnInit(); + + let event = { + target : { + value: 'paramNotExist' + } + } + + expect(fixture.componentInstance.artifact.heatParameters.length).toBe(3); + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.artifact.heatParameters.length).toBe(0); + }); + + it("should update filter heatParameters so there will be only 1 value to be displayed", () => { + + fixture.componentInstance.artifact = artifactModel; + fixture.componentInstance.artifact.heatParameters = mockHeatParameters; + fixture.componentInstance.ngOnInit(); + + let event = { + target : { + value: 'param1' + } + } + + expect(fixture.componentInstance.artifact.heatParameters.length).toBe(3); + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.artifact.heatParameters.length).toBe(1); + }); + +});
\ No newline at end of file @@ -0,0 +1,91 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { SdcUiCommon, SdcUiServices } from 'onap-ui-angular'; +import { Subject } from 'rxjs/Rx'; +import { ArtifactModel } from '../../../../models/artifacts'; +import { CacheService } from '../../../services/cache.service'; + +export interface IPoint { + x: number; + y: number; +} + +@Component({ + selector: 'env-params', + templateUrl: './env-params.component.html', + styleUrls: ['../../../../../assets/styles/table-style.less', './env-params.component.less'] +}) +export class EnvParamsComponent implements OnInit { + + @Input() public artifact: ArtifactModel; + @Input() public isInstanceSelected: boolean; + @Input() public isViewOnly: boolean; + + @ViewChild('textArea') textArea: ElementRef; + private copiedWorkingArtifactHeatParameters = []; + private onValidationChange: Subject<boolean> = new Subject(); + private displayRegexValid = SdcUiCommon.RegexPatterns.numberOrEmpty; + + // Deployment timeout in minutes + private maxDeploymentTimeout: number = 120; + private minDeploymentTimeout: number = 1; + private defaultDeploymentTimeout: number = 60; + + constructor(private cacheService: CacheService, private popoverService: SdcUiServices.PopoverService) { + const configuration = cacheService.get('UIConfiguration'); + if (configuration && configuration.heatDeploymentTimeout) { + this.maxDeploymentTimeout = configuration.heatDeploymentTimeout.maxMinutes; + this.minDeploymentTimeout = configuration.heatDeploymentTimeout.minMinutes; + this.defaultDeploymentTimeout = configuration.heatDeploymentTimeout.defaultMinutes; + } + } + + ngOnInit(): void { + this.copiedWorkingArtifactHeatParameters = [...this.artifact.heatParameters]; + } + + public clearCurrentValue = (name: string) => { + this.artifact.heatParameters.filter((param) => param.name === name)[0].currentValue = ''; + } + + public timeoutChanged(timeout) { + this.artifact.timeout = timeout; + } + + updateFilter(event) { + const val = event.target.value.toLowerCase(); + // filter our data + const temp = this.copiedWorkingArtifactHeatParameters.filter((param) => { + return !val || param.name ? param.name.toLowerCase().indexOf(val) !== -1 : -1 || param.currentValue ? param.currentValue.toLowerCase().indexOf(val) !== -1 : -1; + }); + // update the rows + this.artifact.heatParameters = temp; + } + + private openPopOver = (title: string, content: string, positionOnPage: IPoint, location: string) => { + this.popoverService.createPopOver(title, content, positionOnPage, location); + } + + private onValidityChange = (isValid: boolean): void => { + this.onValidationChange.next(isValid); + } +} @@ -0,0 +1,28 @@ +import { NgModule } from "@angular/core"; +import { EnvParamsComponent } from "./env-params.component"; +import { NgxDatatableModule } from "@swimlane/ngx-datatable"; +import { SdcUiComponentsModule, SdcUiServices } from "onap-ui-angular"; + + +@NgModule({ + declarations: [ + EnvParamsComponent + ], + imports: [ + NgxDatatableModule, + SdcUiComponentsModule + ], + exports: [ + EnvParamsComponent + ], + entryComponents: [ //need to add anything that will be dynamically created + EnvParamsComponent + ], + providers: [ + SdcUiServices.ModalService + ] +}) + +export class EnvParamsModule { + +} @@ -13,7 +13,9 @@ import { TopNavComponent } from "./top-nav/top-nav.component"; FormsModule, TranslateModule ], - exports: [], + exports: [ + TopNavComponent + ], entryComponents: [ //need to add anything that will be dynamically created TopNavComponent ], @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`artifact form component should match current snapshot of top-nav component 1`] = ` +<top-nav + $state={[Function Object]} + _getTopLvlSelectedIndexByState={[Function Function]} + authService={[Function Object]} + sdcConfig={[Function Object]} + searchTermChange={[Function EventEmitter]} + translateService={[Function Object]} +> + <nav + class="top-nav" + > + <div + class="asdc-app-title-wrapper" + > + <a + class="asdc-app-title" + > + + </a> + <div + class="asdc-version" + > + v. + </div> + </div> + + + <div + class="top-search" + > + <input + class="search-text" + data-tests-id="main-menu-input-search" + placeholder="Search" + type="text" + /> + <span + class="w-sdc-search-icon magnification" + /> + </div> + + </nav> +</top-nav> +`; @@ -0,0 +1,161 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { PluginsConfiguration } from 'app/models'; +import { Observable } from 'rxjs'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { MenuItem, MenuItemGroup } from '../../../../utils/menu-handler'; +import { SdcConfigToken } from '../../../config/sdc-config.config'; +import { AuthenticationService } from '../../../services/authentication.service'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { TopNavComponent } from './top-nav.component'; + +describe('artifact form component', () => { + + let fixture: ComponentFixture<TopNavComponent>; + let translateServiceMock: Partial<TranslateService>; + let mockStateService; + let authServiceMock; + + const designerUser = { + email: 'designer@sdc.com', + firstName: 'Carlos', + fullName: 'Carlos Santana', + lastLoginTime: 1555587266566, + lastName: 'Santana', + role: 'DESIGNER', + status: 'ACTIVE', + userId: 'cs0008' + }; + + const pluginDisplayOptions = { + displayName : '', + displayContext : new Array<string>(), + displayRoles : new Array<string>() + }; + + let roleToReturn = designerUser; + + const map1 = new Map(); + map1.otherValue = pluginDisplayOptions; + + const map2 = new Map(); + pluginDisplayOptions.displayRoles = ['DESIGNER']; + pluginDisplayOptions.displayName = 'DCAE-DS'; + map2.tab = pluginDisplayOptions; + + PluginsConfiguration.plugins = + [ + {pluginId: 'DCAED', pluginDiscoveryUrl: 'DCAED_discoveryURL', pluginSourceUrl: 'DCAED_sourceURL', pluginStateUrl: 'DCAED_stateURL', pluginDisplayOptions: map1, isOnline: true}, + {pluginId: 'DCAE-DS', pluginDiscoveryUrl: 'DCAE-DS_discoveryURL', pluginSourceUrl: 'DCAE-DS_sourceURL', pluginStateUrl: 'DCAE-DS_stateURL', pluginDisplayOptions: map2, isOnline: true} + ]; + + beforeEach( + async(() => { + authServiceMock = { + getLoggedinUser: jest.fn().mockImplementation(() => { + return roleToReturn; + }) + }; + + mockStateService = { + go: jest.fn(), + includes: jest.fn(() => true), + current : { + name : 'plugins' + }, + params : {} + }; + + translateServiceMock = { + languageChangedObservable: Mock.of<Observable<string>>( { + subscribe : jest.fn().mockImplementation((cb) => { + cb(); + }) + }), + translate: jest.fn((str: string) => { + if (str === 'TOP_MENU_HOME_BUTTON') { + return 'HOME'; + } else if (str === 'TOP_MENU_CATALOG_BUTTON') { + return 'CATALOG'; + } else if (str === 'TOP_MENU_ON_BOARD_BUTTON') { + return 'ONBOARD'; + } else { + return 'TBD...'; + } + }) + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [TopNavComponent], + imports: [TranslateModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: translateServiceMock}, + {provide: '$state', useValue: mockStateService}, + {provide: AuthenticationService, useValue: authServiceMock}, + {provide: SdcConfigToken, useValue: {csarFileExtension: 'csar', toscaFileExtension: 'yaml,yml'}}, + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(TopNavComponent); + }); + }) + ); + + it('should match current snapshot of top-nav component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('Once a Designer logged in, Menu Items will contain HOME, CATALOG, ONBOARD & DCAE-DS; HOME will be selected', () => { + + // topLvlSelectedIndex = 1 => Ignore the inner call to _getTopLvlSelectedIndexByState. + fixture.componentInstance.topLvlSelectedIndex = 0; + fixture.componentInstance.ngOnInit(); + + expect(fixture.componentInstance.topLvlMenu.itemClick).toBe(true); + + expect(fixture.componentInstance.topLvlMenu.menuItems.length).toBe(4); + + expect(fixture.componentInstance.topLvlMenu.menuItems[0]).toEqual({ + action: 'goToState', + blockedForTypes: null, + callback: null, + params: null, + state: 'dashboard', + text: 'HOME' + }); + expect(fixture.componentInstance.topLvlMenu.menuItems[1]).toEqual({ + action: 'goToState', + blockedForTypes: null, + callback: null, + params: null, + state: 'catalog', + text: 'CATALOG' + }); + expect(fixture.componentInstance.topLvlMenu.menuItems[2]).toEqual({ + action: 'goToState', + blockedForTypes: null, + callback: null, + params: null, + state: 'onboardVendor', + text: 'ONBOARD' + }); + expect(fixture.componentInstance.topLvlMenu.menuItems[3]).toEqual({ + action: 'goToState', + blockedForTypes: null, + callback: null, + params: + {path: 'DCAE-DS_stateURL'}, + state: 'plugins', + text: 'DCAE-DS' + }); + + expect(fixture.componentInstance.topLvlMenu.selectedIndex).toBe(0); + }); + +}); @@ -65,6 +65,6 @@ <span class="w-sdc-search-icon magnification"></span> </div> - <div class="notification-icon" [ngClass]="{'disabled' : progress > 0}" *ngIf="user.role === 'DESIGNER' && notificationIconCallback" (click)="notificationIconCallback()" tooltip="Vendor Software Product Repository" tooltipPlacement="left" data-tests-id="repository-icon"></div> + <div class="notification-icon" [ngClass]="{'disabled' : progress > 0}" *ngIf="user && user.role === 'DESIGNER' && notificationIconCallback" (click)="notificationIconCallback()" tooltip="Vendor Software Product Repository" tooltipPlacement="left" data-tests-id="repository-icon"></div> </nav> @@ -120,7 +120,7 @@ height: 35px; background-color: white; font-size: 13px; - width: 150px; + width: auto; line-height: 35px; padding: 0 10px; @@ -19,14 +19,17 @@ */ import * as _ from "lodash"; -import {Component, Inject, Input, Output, EventEmitter} from "@angular/core"; +import {Component, Inject, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges} from "@angular/core"; import {IHostedApplication, IUserProperties} from "app/models"; import {MenuItemGroup, MenuItem} from "app/utils"; -import {UserService} from "../../../services/user.service"; +import {AuthenticationService} from "../../../services/authentication.service"; import {SdcConfigToken, ISdcConfig} from "../../../config/sdc-config.config"; import {TranslateService} from "../../../shared/translator/translate.service"; import {PluginsConfiguration, Plugin} from "app/models"; - +import { Subscription } from "rxjs"; +// import { Store } from "@ngrx/store"; +// import { AppState } from "app/ng2/store/app.state"; +// import * as unsavedChangesReducer from 'app/ng2/store/reducers/unsaved-changes.reducer'; declare const window:any; @Component({ @@ -34,7 +37,7 @@ declare const window:any; templateUrl: './top-nav.component.html', styleUrls:['./top-nav.component.less'] }) -export class TopNavComponent { +export class TopNavComponent implements OnInit, OnChanges { @Input() public version:string; @Input() public menuModel:Array<MenuItemGroup>; @Input() public topLvlSelectedIndex:number; @@ -48,15 +51,18 @@ export class TopNavComponent { this.searchTermChange.emit(event); } + private subscription: Subscription; + private hasUnsavedChanges: boolean; public topLvlMenu:MenuItemGroup; public user:IUserProperties; private topNavPlugins: Array<Plugin>; constructor(private translateService:TranslateService, @Inject('$state') private $state:ng.ui.IStateService, - private userService:UserService, + private authService:AuthenticationService, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { window.nav = this; + } private _getTopLvlSelectedIndexByState = ():number => { @@ -112,7 +118,7 @@ export class TopNavComponent { ngOnInit() { console.log('Nav is init!', this.menuModel); - this.user = this.userService.getLoggedinUser(); + this.user = this.authService.getLoggedinUser(); this.topNavPlugins = _.filter(PluginsConfiguration.plugins, (plugin: Plugin) => { return plugin.pluginDisplayOptions["tab"] !== undefined; }); @@ -142,7 +148,6 @@ export class TopNavComponent { this.topLvlMenu = new MenuItemGroup(0, tmpArray, true); this.topLvlMenu.selectedIndex = isNaN(this.topLvlSelectedIndex) ? this._getTopLvlSelectedIndexByState() : this.topLvlSelectedIndex; - this.generateMenu(); }); } @@ -160,6 +165,7 @@ export class TopNavComponent { }); } + menuItemClick(itemGroup:MenuItemGroup, item:MenuItem) { let onSuccessFunction = () => { @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`filter-properties-assignemnt component should match current snapshot of artifact-tab component 1`] = ` +<filter-properties-assignment + allSelected="false" + clearAll={[Function Function]} + close={[Function Function]} + filterData={[Function FilterPropertiesAssignmentData]} + filterPopover={[Function ElementRef]} + footerButtons={[Function Object]} + onTypeSelected={[Function Function]} + search={[Function Function]} + searchProperties={[Function EventEmitter]} + selectAll={[Function Function]} + selectedTypes={[Function Object]} + someTypesSelectedAndThereIsPropertyName={[Function Function]} +> + <popover-content + placement="bottom-right" + > + <form> + <div + class="field" + > + <label> + Resource Type + </label> + <div> + <checkbox + data-tests-id="filter-checkbox-all" + /> + </div> + + </div> + <div + class="field" + > + <label> + Property Name + </label> + <input + class="i-sdc-form-input" + data-tests-id="filter-box" + name="propertyName" + placeholder="Type here" + required="" + /> + </div> + </form> + </popover-content><div + class="open-filter-button" + > + <div + class="sprite-new filter-icon" + data-tests-id="filter-button" + /> + </div> +</filter-properties-assignment> +`; @@ -38,6 +38,6 @@ </div> </form> </popover-content> -<div class="open-filter-button" [popover]="filterPopover" [ngClass]="{'open':showPopover}" (onShown)="showPopover = true" (onHidden)="showPopover = false"> +<div class="open-filter-button" [popover]="filterPopover" > <div class="sprite-new filter-icon" data-tests-id="filter-button"></div> </div> @@ -0,0 +1,141 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {FilterPropertiesAssignmentComponent} from "./filter-properties-assignment.component"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {FilterPropertiesAssignmentData} from "../../../../models/filter-properties-assignment-data"; +import {PopoverComponent} from "../../ui/popover/popover.component"; + + + +describe('filter-properties-assignemnt component', () => { + + let fixture: ComponentFixture<FilterPropertiesAssignmentComponent>; + + beforeEach( + async(() => { + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [FilterPropertiesAssignmentComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(FilterPropertiesAssignmentComponent); + + }); + }) + ); + + + it('should match current snapshot of artifact-tab component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('on selectAll', () => { + let filterData:FilterPropertiesAssignmentData = new FilterPropertiesAssignmentData(); + filterData.propertyName = 'testVal'; + let typesOptions:Array<string> = ['option1', 'option2', 'option3']; + let selectedTypes:Object = {}; + + fixture.componentInstance.filterData = filterData; + fixture.componentInstance.typesOptions = typesOptions; + fixture.componentInstance.selectedTypes = selectedTypes; + + fixture.componentInstance.selectAll(); + + let expectedRes = {"option1": false,"option2": false,"option3": false}; + expect(fixture.componentInstance.selectedTypes).toEqual(expectedRes); + }); + + + it ('on onTypeSelected allSelected set to False', () => { + let selectedTypes:Object = {"option1": true,"option2": false,"option3": true}; + fixture.componentInstance.selectedTypes = selectedTypes; + fixture.componentInstance.allSelected = true; + fixture.componentInstance.onTypeSelected('option2'); + + expect(fixture.componentInstance.allSelected).toBe(false); + }); + + it ('on onTypeSelected allSelected remains True', () => { + let selectedTypes:Object = {"option1": true,"option2": true,"option3": true}; + fixture.componentInstance.selectedTypes = selectedTypes; + fixture.componentInstance.allSelected = true; + fixture.componentInstance.onTypeSelected('option2'); + + expect(fixture.componentInstance.allSelected).toBe(true); + }); + + it ('on clearAll', () => { + let filterData:FilterPropertiesAssignmentData = new FilterPropertiesAssignmentData(); + filterData.propertyName = 'testVal'; + let selectedTypes:Object = {"option1": true,"option2": false,"option3": true}; + + fixture.componentInstance.filterData = filterData; + fixture.componentInstance.selectedTypes = selectedTypes; + fixture.componentInstance.allSelected = true; + + fixture.componentInstance.clearAll(); + + expect(fixture.componentInstance.filterData.propertyName).toBe(''); + expect(fixture.componentInstance.allSelected).toBe(false); + }); + + it ('someTypesSelectedAndThereIsPropertyName return True', ()=> { + let res = fixture.componentInstance.someTypesSelectedAndThereIsPropertyName(); + + expect(res).toBe(true) + }); + + it ('someTypesSelectedAndThereIsPropertyName return Null', ()=> { + let selectedTypes:Object = {"option1": true,"option2": false,"option3": true}; + let filterData:FilterPropertiesAssignmentData = new FilterPropertiesAssignmentData(); + filterData.propertyName = 'testVal'; + + fixture.componentInstance.selectedTypes = selectedTypes; + fixture.componentInstance.filterData = filterData; + + let res = fixture.componentInstance.someTypesSelectedAndThereIsPropertyName(); + + expect(res).toBe(null) + }); + + it ('search', ()=> { + + let filterData:FilterPropertiesAssignmentData = new FilterPropertiesAssignmentData(); + filterData.selectedTypes = ["CP"]; + fixture.componentInstance.filterData = filterData; + + let componentType: string = 'resource'; + fixture.componentInstance.componentType = componentType; + + let selectedTypes:Object = {"option1": true,"CP": true,"option3": true}; + fixture.componentInstance.selectedTypes = selectedTypes; + + let temp:any; + let filterPopover: PopoverComponent = new PopoverComponent(temp , temp ); + fixture.componentInstance.filterPopover = filterPopover; + fixture.componentInstance.filterPopover.hide = jest.fn(); + + fixture.componentInstance.search(); + + expect(fixture.componentInstance.filterData.selectedTypes).toEqual(["CP"]); + expect(fixture.componentInstance.filterPopover.hide).toHaveBeenCalled(); + }); + + it('close', () => { + let temp:any; + let filterPopover: PopoverComponent = new PopoverComponent(temp , temp ); + fixture.componentInstance.filterPopover = filterPopover; + fixture.componentInstance.filterPopover.hide = jest.fn(); + + fixture.componentInstance.close(); + + expect(fixture.componentInstance.filterPopover.hide).toHaveBeenCalled(); + }); + +});
\ No newline at end of file @@ -29,8 +29,8 @@ [rowHeight]="200" [reorderable]="false" > - <ngx-datatable-column prop="{{col.prop}}" [minWidth]="100" *ngFor="let col of columns"> - <template let-column="column" height="100" ngx-datatable-header-template> + <ngx-datatable-column *ngFor="let col of columns" prop="{{col.prop}}" [minWidth]="100" > + <ng-template let-column="column" height="100" ngx-datatable-header-template> <span class="datatable-column-span"> <b>{{col.name}}</b> <div *ngIf="canBeDeleted(col.name)" style="width: 45px !important; color: red; " @@ -44,23 +44,23 @@ placeholder='Filter column...' (keyup)='updateColumnFilter($event, col.prop)' /> - </template> + </ng-template> </ngx-datatable-column> <ngx-datatable-column *ngIf="addNewColumn" class="datatable-white-body-cell" [minWidth]="220" [maxWidth]="220" [width]="220" > - <template ngx-datatable-header-template> + <ng-template ngx-datatable-header-template> <gab-column-provider [pathsAndNames]="pathsandnames" (onCancel)="hideAddNewColumn()" (onSave)="refresh()"></gab-column-provider> - </template> - <template class="datatable-white-body-cell" ngx-datatable-cell-template> - </template> + </ng-template> + <ng-template class="datatable-white-body-cell" ngx-datatable-cell-template> + </ng-template> </ngx-datatable-column> <ngx-datatable-column class="datatable-white-body-cell" [minWidth]="50" [maxWidth]="50" [width]="50" > - <template ngx-datatable-header-template> + <ng-template ngx-datatable-header-template> <div data-tests-id="gab-add-btn" class="add-btn add-btn-div" (click)="showAddNewColumn()">Add</div> - </template> - <template class="datatable-white-body-cell" ngx-datatable-cell-template> - </template> + </ng-template> + <ng-template class="datatable-white-body-cell" ngx-datatable-cell-template> + </ng-template> </ngx-datatable-column> </ngx-datatable> -</div>
\ No newline at end of file +</div> @@ -64,8 +64,8 @@ export class GenericArtifactBrowserComponent { this.gabService.getArtifact(this.artifactid, this.resourceid, paths) .subscribe( response => { - let typedServerResponse: IServerResponse = response.json() as IServerResponse; - this.normalizeDataForNgxDatatable(typedServerResponse.data); + // let typedServerResponse: IServerResponse = response.json() as IServerResponse; + this.normalizeDataForNgxDatatable(response['data']); }, () => { this.ready = false; @@ -0,0 +1,52 @@ +import { async, ComponentFixture } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import 'jest-dom/extend-expect'; +import {HierarchyNavigationComponent} from "./hierarchy-navigation.component"; +import {HierarchyDisplayOptions} from "./hierarchy-display-options"; + + +describe('hierarchyNavigationComponent', () => { + let fixture: ComponentFixture<HierarchyNavigationComponent>; + let hierarchyDisplayOptions: HierarchyDisplayOptions; + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [HierarchyNavigationComponent] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(HierarchyNavigationComponent); + hierarchyDisplayOptions = new HierarchyDisplayOptions("id", "name", "children"); + fixture.componentInstance.displayOptions = hierarchyDisplayOptions; + fixture.detectChanges(); + fixture.componentInstance.displayData = [{name: "aaa", id: "1", children: [{name: "bbb", id: "1.1"}, {name: "ccc", id: "1.2", children: [{name: "aaa", id: "1.2.1"}]}]}, {name: "bbb", id: "2"}]; + fixture.detectChanges(); + }); + }) + ); + + it('should have a selected class after user click on a tree node', + () => { + let firstNodeElement = fixture.debugElement.query(By.css('.node-item')); + fixture.componentInstance.updateSelected.subscribe((item) => { + fixture.componentInstance.selectedItem = item.id; + fixture.detectChanges(); + }); + firstNodeElement.nativeElement.click(); + fixture.whenStable().then(() => { + expect(firstNodeElement.children[0].nativeElement).toHaveClass('selected'); + }); + }); + + it('should call onClick function when user click on a tree node', + () => { + spyOn(fixture.componentInstance, 'onClick'); + let firstNodeElement = fixture.debugElement.query(By.css('.node-item')).nativeElement; + firstNodeElement.click(); + expect(fixture.componentInstance.onClick).toHaveBeenCalled(); + }); + +});
\ No newline at end of file @@ -21,7 +21,6 @@ import {Component, Input, Output, EventEmitter} from '@angular/core'; import {HierarchyDisplayOptions} from './hierarchy-display-options'; - @Component({ selector: 'hierarchy-navigation', templateUrl: './hierarchy-navigation.component.html', @@ -32,7 +31,6 @@ export class HierarchyNavigationComponent { @Input() displayData: Array<any>; @Input() selectedItem: any; @Input() displayOptions: HierarchyDisplayOptions; - @Output() updateSelected:EventEmitter<any> = new EventEmitter(); onClick = ($event, item) => { @@ -35,33 +35,37 @@ <div class="no-data" *ngIf="!inputs || !inputs.length">No data to display</div> <div> <div class="table-row" *ngFor="let input of inputs" (click)="selectedInputId = input.path" [ngClass]="{'selected': selectedInputId && selectedInputId === input.path}"> + <!-- Property Name --> <div class="table-cell col1"> <div class="inner-cell-div" tooltip="{{input.name}}"><span class="property-name">{{input.name}}</span></div> <span *ngIf="input.description" class="property-description-icon sprite-new show-desc" tooltip="{{input.description}}" tooltipDelay="0"></span> </div> + <!-- From Instance --> <div class="table-cell col3"> <div class="inner-cell-div" tooltip="{{instanceNamesMap[input.instanceUniqueId]?.name}}"> <span>{{instanceNamesMap[input.instanceUniqueId]?.name}}</span> </div> </div> + <!-- Type --> <div class="table-cell col2"> <div class="inner-cell-div" tooltip="{{input.type | contentAfterLastDot}}"> <span>{{input.type | contentAfterLastDot}}</span> </div> </div> + <!-- Value --> <div class="table-cell valueCol input-value-col" [class.inner-table-container]="input.childrenProperties || !input.isSimpleType"> <dynamic-element class="value-input" - *ngIf="input.isSimpleType" + *ngIf="checkInstanceFePropertiesMapIsFilled() && input.isSimpleType" pattern="validationUtils.getValidationPattern(input.type)" [value]="input.defaultValueObj" [type]="input.type" - [constraints]="input.constraints" [name]="input.name" (elementChanged)="onInputChanged(input, $event)" [readonly]="readonly" - [testId]="'input-' + input.name"> + [testId]="'input-' + input.name" + [constraints] = "getConstraints(input)"> </dynamic-element> <div class="delete-button-container"> <span *ngIf="input.instanceUniqueId && !readonly" class="sprite-new delete-btn" (click)="openDeleteModal(input)" data-tests-id="delete-input-button"></span> @@ -26,6 +26,8 @@ import { Component, Input, Output, EventEmitter } from "@angular/core"; import { InputFEModel } from "app/models"; import { ModalService } from "../../../services/modal.service"; import { InstanceFeDetails } from "app/models/instance-fe-details"; +import { InstanceFePropertiesMap } from "../../../../models/properties-inputs/property-fe-map"; +import { DataTypeService } from "../../../services/data-type.service"; @Component({ selector: 'inputs-table', @@ -41,6 +43,8 @@ export class InputsTableComponent { @Output() inputChanged: EventEmitter<any> = new EventEmitter<any>(); @Output() deleteInput: EventEmitter<any> = new EventEmitter<any>(); + @Input() fePropertiesMap: InstanceFePropertiesMap; + sortBy: String; reverse: boolean; selectedInputToDelete: InputFEModel; @@ -74,7 +78,8 @@ export class InputsTableComponent { }; - constructor(private modalService: ModalService) { + constructor(private modalService: ModalService, private dataTypeService: DataTypeService){ + var x = 5 } @@ -89,9 +94,40 @@ export class InputsTableComponent { }; openDeleteModal = (input: InputFEModel) => { + console.log('exist inputs: ' + this.inputs) + + this.selectedInputToDelete = input; this.modalService.createActionModal("Delete Input", "Are you sure you want to delete this input?", "Delete", this.onDeleteInput, "Close").instance.open(); } + + getConstraints(input:InputFEModel): string[]{ + + if (input.inputPath){ + const pathValuesName = input.inputPath.split('#'); + const rootPropertyName = pathValuesName[0]; + const propertyName = pathValuesName[1]; + let filterredRootPropertyType = _.values(this.fePropertiesMap)[0].filter(property => + property.name == rootPropertyName); + if (filterredRootPropertyType.length > 0){ + let rootPropertyType = filterredRootPropertyType[0].type; + return this.dataTypeService.getConstraintsByParentTypeAndUniqueID(rootPropertyType, propertyName); + }else{ + return null; + } + + } + // else if(input.constraints.length > 0){ + // return input.constraints[0].validValues + // } + else{ + return null; + } + } + + checkInstanceFePropertiesMapIsFilled(){ + return _.keys(this.fePropertiesMap).length > 0 + } } @@ -14,12 +14,11 @@ * permissions and limitations under the License. */ - -import { Component, Input, Output, EventEmitter } from "@angular/core"; -import { PolicyInstance } from "app/models"; -import { ModalService } from "../../../services/modal.service"; -import { InstanceFeDetails } from "app/models/instance-fe-details"; -import {TranslateService} from 'app/ng2/shared/translator/translate.service'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { PolicyInstance } from 'app/models'; +import { InstanceFeDetails } from 'app/models/instance-fe-details'; +import { TranslateService } from 'app/ng2/shared/translator/translate.service'; +import { ModalService } from '../../../services/modal.service'; @Component({ selector: 'policies-table', @@ -28,13 +27,13 @@ import {TranslateService} from 'app/ng2/shared/translator/translate.service'; }) export class PoliciesTableComponent { - @Input() policies: Array<PolicyInstance>; + @Input() policies: PolicyInstance[]; @Input() instanceNamesMap: Map<string, InstanceFeDetails>; @Input() readonly: boolean; @Input() isLoading: boolean; @Output() deletePolicy: EventEmitter<any> = new EventEmitter<any>(); - sortBy: String; + sortBy: string; reverse: boolean; selectedPolicyToDelete: PolicyInstance; deleteMsgTitle: string; @@ -42,43 +41,39 @@ export class PoliciesTableComponent { modalDeleteBtn: string; modalCancelBtn: string; + constructor(private modalService: ModalService, private translateService: TranslateService) { + } + sort = (sortBy) => { this.reverse = (this.sortBy === sortBy) ? !this.reverse : true; - let reverse = this.reverse ? 1 : -1; + const reverse = this.reverse ? 1 : -1; this.sortBy = sortBy; - let instanceNameMapTemp = this.instanceNamesMap; - let itemIdx1Val = ""; - let itemIdx2Val = ""; - this.policies.sort(function (itemIdx1, itemIdx2) { - if (sortBy == 'instanceUniqueId') { - itemIdx1Val = (itemIdx1[sortBy] && instanceNameMapTemp[itemIdx1[sortBy]] !== undefined) ? instanceNameMapTemp[itemIdx1[sortBy]].name : ""; - itemIdx2Val = (itemIdx2[sortBy] && instanceNameMapTemp[itemIdx2[sortBy]] !== undefined) ? instanceNameMapTemp[itemIdx2[sortBy]].name : ""; - } - else { + const instanceNameMapTemp = this.instanceNamesMap; + let itemIdx1Val = ''; + let itemIdx2Val = ''; + this.policies.sort((itemIdx1, itemIdx2) => { + if (sortBy === 'instanceUniqueId') { + itemIdx1Val = (itemIdx1[sortBy] && instanceNameMapTemp[itemIdx1[sortBy]] !== undefined) ? instanceNameMapTemp[itemIdx1[sortBy]].name : ''; + itemIdx2Val = (itemIdx2[sortBy] && instanceNameMapTemp[itemIdx2[sortBy]] !== undefined) ? instanceNameMapTemp[itemIdx2[sortBy]].name : ''; + } else { itemIdx1Val = itemIdx1[sortBy]; itemIdx2Val = itemIdx2[sortBy]; - } + } if (itemIdx1Val < itemIdx2Val) { return -1 * reverse; - } - else if (itemIdx1Val > itemIdx2Val) { + } else if (itemIdx1Val > itemIdx2Val) { return 1 * reverse; - } - else { + } else { return 0; } }); - }; - - - constructor(private modalService: ModalService, private translateService: TranslateService) { } ngOnInit() { - this.translateService.languageChangedObservable.subscribe(lang => { - this.deleteMsgTitle = this.translateService.translate("DELETE_POLICY_TITLE"); - this.modalDeleteBtn = this.translateService.translate("MODAL_DELETE"); - this.modalCancelBtn = this.translateService.translate("MODAL_CANCEL"); + this.translateService.languageChangedObservable.subscribe((lang) => { + this.deleteMsgTitle = this.translateService.translate('DELETE_POLICY_TITLE'); + this.modalDeleteBtn = this.translateService.translate('MODAL_DELETE'); + this.modalCancelBtn = this.translateService.translate('MODAL_CANCEL'); }); } @@ -86,15 +81,13 @@ export class PoliciesTableComponent { onDeletePolicy = () => { this.deletePolicy.emit(this.selectedPolicyToDelete); this.modalService.closeCurrentModal(); - }; + } openDeleteModal = (policy: PolicyInstance) => { this.selectedPolicyToDelete = policy; - this.translateService.languageChangedObservable.subscribe(lang => { - this.deleteMsgBodyTxt = this.translateService.translate("DELETE_POLICY_MSG", {policyName: policy.name}); + this.translateService.languageChangedObservable.subscribe((lang) => { + this.deleteMsgBodyTxt = this.translateService.translate('DELETE_POLICY_MSG', {policyName: policy.name}); this.modalService.createActionModal(this.deleteMsgTitle, this.deleteMsgBodyTxt, this.modalDeleteBtn, this.onDeletePolicy, this.modalCancelBtn).instance.open(); }); } } - - @@ -14,12 +14,12 @@ * permissions and limitations under the License. */ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {PoliciesTableComponent} from "./policies-table.component"; -import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from 'app/ng2/shared/translator/translate.module'; +import { GlobalPipesModule } from '../../../pipes/global-pipes.module'; +import { PoliciesTableComponent } from './policies-table.component'; @NgModule({ declarations: [ @@ -38,4 +38,4 @@ import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; providers: [] }) export class PoliciesTableModule { -}
\ No newline at end of file +} @@ -12,8 +12,8 @@ * See the License for the specific language governing permissions and * limitations under the License. --> - - + + <div *ngIf="!property.hidden" class="dynamic-property-row nested-level-{{nestedLevel}}" [@fadeIn] [ngClass]="{'selected': selectedPropertyId && selectedPropertyId === property.propertiesName, 'readonly': property.isDisabled ||property.isDeclared}" [class.with-top-border]="property.isChildOfListOrMap" @@ -48,13 +48,13 @@ pattern="validationUtils.getValidationPattern(property.type)" [value]="property.isDeclared ? property.value : property.valueObj" [type]="property.isDeclared ? 'string' : property.type" - [childType]="property.schema.property.type" [name]="property.name" [path]="property.propertiesName" - [constraints]="property.constraints" (elementChanged)="onElementChanged($event)" [readonly]="readonly || property.isDeclared || property.isDisabled" [testId]="'prop-' + propertyTestsId" + [declared] = "property.isDeclared" + [constraints] = "constraints" ></dynamic-element> </div> </ng-container> @@ -69,7 +69,7 @@ <ng-container *ngIf="!property.isDeclared"> <a *ngIf="(propType == derivedPropertyTypes.LIST || propType == derivedPropertyTypes.MAP) && !property.isChildOfListOrMap" class="property-icon add-item" (click)="createNewChildProperty();" [ngClass]="{'disabled':readonly || preventInsertItem(property)}" [attr.data-tests-id]="'add-to-list-' + propertyTestsId">Add value to list</a> <span *ngIf="property.isChildOfListOrMap" (click)="deleteItem.emit(property);" class="property-icon sprite-new delete-item-icon" [ngClass]="{'disabled':readonly}" [attr.data-tests-id]="'delete-from-list-' + propertyTestsId"></span> - <span *ngIf="!isPropertyFEModel && (propType == derivedPropertyTypes.COMPLEX || ((propType == derivedPropertyTypes.LIST || propType == derivedPropertyTypes.MAP) && hasChildren))" (click)="expandChildById(propPath)" class="property-icon sprite-new round-expand-icon" [class.open]="expandedChildId.indexOf(propPath) == 0" [attr.data-tests-id]="'expand-' + propertyTestsId"></span> + <span *ngIf="!isPropertyFEModel && (propType == derivedPropertyTypes.COMPLEX || ((propType == derivedPropertyTypes.LIST || propType == derivedPropertyTypes.MAP) && hasChildren))" (click)="expandChildById(propPath)" class="property-icon sprite-new round-expand-icon" [class.open]="expandedChildId.indexOf(propPath) == 0" [attr.data-tests-id]="'expand-' + propertyTestsId" ></span> </ng-container> </div> @@ -60,7 +60,7 @@ align-self:center; cursor:pointer; } - + } .filtered { @@ -23,7 +23,7 @@ import {Component, Input, Output, EventEmitter, ViewChild, ComponentRef} from "@ import { PropertyFEModel, DerivedFEProperty, DerivedPropertyType } from "app/models"; import { PROPERTY_TYPES } from 'app/utils'; import { DataTypeService } from "../../../../services/data-type.service"; -import { trigger, state, style, transition, animate } from '@angular/core'; +import { trigger, state, style, transition, animate } from '@angular/animations'; import {PropertiesUtils} from "../../../../pages/properties-assignment/services/properties.utils"; import {IUiElementChangeEvent} from "../../../ui/form-components/ui-element-base.component"; import {DynamicElementComponent} from "../../../ui/dynamic-element/dynamic-element.component"; @@ -42,6 +42,7 @@ export class DynamicPropertyComponent { isPropertyFEModel: boolean; nestedLevel: number; propertyTestsId: string; + constraints:string[]; @Input() canBeDeclared: boolean; @Input() property: PropertyFEModel | DerivedFEProperty; @@ -72,7 +73,30 @@ export class DynamicPropertyComponent { this.propPath = (this.property instanceof PropertyFEModel) ? this.property.name : this.property.propertiesName; this.nestedLevel = (this.property.propertiesName.match(/#/g) || []).length; this.rootProperty = (this.rootProperty) ? this.rootProperty : <PropertyFEModel>this.property; - this.propertyTestsId = this.getPropertyTestsId(); + this.propertyTestsId = this.getPropertyTestsId(); + + this.initConsraintsValues(); + + + } + + initConsraintsValues(){ + let primitiveProperties = ['string', 'integer', 'float', 'boolean']; + + //Property has constraints + if(this.property.constraints){ + this.constraints = this.property.constraints[0].validValues + } + + //Complex Type + else if (primitiveProperties.indexOf(this.rootProperty.type) == -1 && primitiveProperties.indexOf(this.property.type) >= 0 ){ + this.constraints = this.dataTypeService.getConstraintsByParentTypeAndUniqueID(this.rootProperty.type, this.property.name); + } + + else{ + this.constraints = null; + } + } ngDoCheck() { @@ -37,18 +37,18 @@ <div class="no-data" *ngIf="!fePropertiesMap || !(fePropertiesMap | keys).length">No data to display</div> <ng-container *ngFor="let instanceId of fePropertiesMap | keys; trackBy:vspId"> + <!-- Icon & Instance Name --> <div class="table-rows-header white-sub-header" *ngIf="feInstanceNamesMap"> - - <span [ngClass]="['prop-instance-icon', feInstanceNamesMap[instanceId].iconClass, 'small']"></span> - {{feInstanceNamesMap[instanceId].name}} + {{feInstanceNamesMap[instanceId].name}} <div class="sprite-new archive-label" *ngIf="feInstanceNamesMap[instanceId].originArchived == true"></div> </div> - - <div class="table-row" *ngFor="let property of fePropertiesMap[instanceId] | searchFilter:'name':searchTerm | orderBy:{path: path, direction: direction}; trackBy:property?.name " + + <div class="table-row" *ngFor="let property of fePropertiesMap[instanceId] | searchFilter:'name':searchTerm | propertiesOrderBy:{path: path, direction: direction}; trackBy:property?.name " (click)="onClickPropertyRow(property, instanceId, $event)" [ngClass]="{'selected': selectedPropertyId && selectedPropertyId === property.name, 'readonly': property.isDisabled || property.isDeclared}"> <div class="table-cell col1" [ngClass]="{'filtered':property.name === propertyNameSearchText}" [class.round-checkbox]="property.isDeclared"> + <!-- Property Name --> <div class="property-name"> <checkbox *ngIf="hasDeclareOption" [(checked)]="property.isSelected" [disabled]="property.isDisabled || property.isDeclared || readonly" (checkedChange)="propertyChecked(property)" [attr.data-tests-id]="property.name"></checkbox> @@ -62,16 +62,19 @@ <span *ngIf="showDelete" class="sprite-new delete-btn" [ngClass]="{'disabled' : property.isDisabled || property.isDeclared}" (click)="openDeleteModal(property)" data-tests-id="delete-input-button"></span> </div> </div> + <!-- Property Type --> <div class="table-cell col2" *ngIf="!hidePropertyType"> <div class="inner-cell-div" tooltip="{{property.type | contentAfterLastDot}}"> <span>{{property.type | contentAfterLastDot}}</span> </div> </div> + <!-- Property ES (Entry Schema) --> <div class="table-cell col3" *ngIf="!hidePropertyType"> <div *ngIf="property.schema && property.schema.property && property.schema.property.type" class="inner-cell-div" tooltip="{{property.schema.property.type | contentAfterLastDot}}"> <span>{{property.schema.property.type | contentAfterLastDot}}</span> </div> </div> + <!-- Property Value --> <div class="table-cell valueCol"> <!-- [ngClass]="{'filtered':property.name === propertyNameSearchText}" (selectProperty)="propertySelected(property, $event, flatProperty.propertiesName)" [propType]="property.type" [propSchema]="property.schema" [propKey]="" [propValue]="property.value"--> <dynamic-property @@ -86,9 +89,10 @@ (expandChild)="property.updateExpandedChildPropertyId($event)" (clickOnPropertyRow)="onClickPropertyInnerRow($event, instanceId)" (checkProperty)="propertyChecked(property, $event)" + > + </dynamic-property> - </div> </div> </ng-container> @@ -0,0 +1,175 @@ +import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { DerivedFEProperty } from '../../../../models/properties-inputs/derived-fe-property'; +import { PropertyBEModel } from '../../../../models/properties-inputs/property-be-model'; +import { PropertyFEModel } from '../../../../models/properties-inputs/property-fe-model'; +import { ContentAfterLastDotPipe } from '../../../pipes/contentAfterLastDot.pipe'; +import { KeysPipe } from '../../../pipes/keys.pipe'; +import { PropertiesOrderByPipe } from '../../../pipes/properties-order-by.pipe'; +import { SearchFilterPipe } from '../../../pipes/searchFilter.pipe'; +import { ModalService } from '../../../services/modal.service'; +import { PropertiesService } from '../../../services/properties.service'; +import { PropertiesTableComponent, PropertyRowSelectedEvent } from './properties-table.component'; + +describe('properties-table component', () => { + + let fixture: ComponentFixture<PropertiesTableComponent>; + let propertiesServiceMock: Partial<PropertiesService>; + let modalServiceMock: Partial<ModalService>; + + beforeEach( + () => { + propertiesServiceMock = { + undoDisableRelatedProperties: jest.fn(), + disableRelatedProperties: jest.fn() + }; + modalServiceMock = { + + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [ + PropertiesTableComponent, + KeysPipe, + PropertiesOrderByPipe, + SearchFilterPipe, + ContentAfterLastDotPipe + ], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: PropertiesService, useValue: propertiesServiceMock}, + {provide: ModalService, useValue: modalServiceMock} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(PropertiesTableComponent); + }); + } + ); + + it('When Properties assignment page is loaded, it is sorted by property name (acsending)', () => { + const fePropertiesMapValues = new SimpleChange('previousValue', 'currentValue', true); + const changes = { + fePropertiesMap: fePropertiesMapValues + }; + + // init values before ngOnChanges was called + fixture.componentInstance.sortBy = 'existingValue'; + + fixture.componentInstance.ngOnChanges(changes); + + expect (fixture.componentInstance.reverse).toEqual(true); + // expect (fixture.componentInstance.direction).toEqual(1); + expect (fixture.componentInstance.direction).toEqual(fixture.componentInstance.ascUpperLettersFirst); + expect (fixture.componentInstance.sortBy).toEqual('name'); + expect (fixture.componentInstance.path.length).toEqual(1); + expect (fixture.componentInstance.path[0]).toEqual('name'); + }); + + it('When ngOnChanges is called without fePropertiesMap,' + + ' sortBy will remain as it was', () => { + const fePropertiesMapValues = new SimpleChange('previousValue', 'currentValue', true); + const changes = { + dummyKey: fePropertiesMapValues + }; + + // init values before ngOnChanges was called + fixture.componentInstance.sortBy = 'existingValue'; + fixture.componentInstance.sort = jest.fn(); + + fixture.componentInstance.ngOnChanges(changes); + + expect (fixture.componentInstance.sortBy).toEqual('existingValue'); + }); + + it ('When sort is called init this.direction to 1', () => { + // init values + fixture.componentInstance.reverse = false; + fixture.componentInstance.direction = 0; + fixture.componentInstance.sortBy = 'initialize.Value'; + fixture.componentInstance.path = []; + + // call sore function + fixture.componentInstance.sort('initialize.Value'); + + // expect that + expect (fixture.componentInstance.reverse).toBe(true); + expect (fixture.componentInstance.direction).toBe(fixture.componentInstance.ascUpperLettersFirst); + expect (fixture.componentInstance.sortBy).toBe('initialize.Value'); + expect (fixture.componentInstance.path.length).toBe(2); + expect (fixture.componentInstance.path[0]).toBe('initialize'); + expect (fixture.componentInstance.path[1]).toBe('Value'); + }); + + it ('When sort is called init this.direction to -1', () => { + // init values + fixture.componentInstance.reverse = true; + fixture.componentInstance.direction = 0; + fixture.componentInstance.sortBy = 'initialize.Value'; + fixture.componentInstance.path = []; + + // call sore function + fixture.componentInstance.sort('initialize.Value'); + + // expect that + expect (fixture.componentInstance.reverse).toBe(false); + expect (fixture.componentInstance.direction).toBe(fixture.componentInstance.descLowerLettersFirst); + }); + + it ('When onPropertyChanged is called, event is emitted' , () => { + spyOn(fixture.componentInstance.emitter, 'emit'); + fixture.componentInstance.onPropertyChanged('testProperty'); + expect(fixture.componentInstance.emitter.emit).toHaveBeenCalledWith('testProperty'); + }); + + it ('When onClickPropertyRow is called, selectedPropertyId is updated and event is emitted.' , () => { + const propertyFEModel = new PropertyFEModel(new PropertyBEModel()); + propertyFEModel.name = 'propertyName'; + const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(propertyFEModel, 'instanceName'); + + spyOn(fixture.componentInstance.selectPropertyRow, 'emit'); + fixture.componentInstance.onClickPropertyRow(propertyFEModel, 'instanceName'); + + expect (fixture.componentInstance.selectedPropertyId).toBe('propertyName'); + expect (fixture.componentInstance.selectPropertyRow.emit).toHaveBeenCalledWith(propertyRowSelectedEvent); + }); + + it ('When onClickPropertyInnerRow is called, event is emitted.' , () => { + const derivedFEProperty = new DerivedFEProperty(new PropertyBEModel()); + const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(derivedFEProperty, 'instanceName'); + spyOn(fixture.componentInstance.selectPropertyRow, 'emit'); + fixture.componentInstance.onClickPropertyInnerRow(derivedFEProperty, 'instanceName'); + + expect (fixture.componentInstance.selectPropertyRow.emit).toHaveBeenCalledWith(propertyRowSelectedEvent); + }); + + it ('When propertyChecked is called, propertiesService.undoDisableRelatedProperties is called and event is emitted.' , () => { + + const propertyFEModel = new PropertyFEModel(new PropertyBEModel()); + propertyFEModel.isSelected = false; + const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(propertyFEModel, 'instanceName1'); + + spyOn(fixture.componentInstance.updateCheckedPropertyCount, 'emit'); + fixture.componentInstance.propertyChecked(propertyFEModel); + expect (propertiesServiceMock.undoDisableRelatedProperties).toHaveBeenCalledWith(propertyFEModel, undefined); + expect (fixture.componentInstance.updateCheckedPropertyCount.emit).toHaveBeenCalledWith(false); + }); + + it ('When propertyChecked is called, propertiesService.disableRelatedProperties is called and event is emitted.' , () => { + + const propertyFEModel = new PropertyFEModel(new PropertyBEModel()); + propertyFEModel.isSelected = true; + const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(propertyFEModel, 'instanceName1'); + + spyOn(fixture.componentInstance.updateCheckedPropertyCount, 'emit'); + fixture.componentInstance.propertyChecked(propertyFEModel); + expect (propertiesServiceMock.disableRelatedProperties).toHaveBeenCalledWith(propertyFEModel, undefined); + expect (fixture.componentInstance.updateCheckedPropertyCount.emit).toHaveBeenCalledWith(true); + }); + +}); @@ -8,9 +8,9 @@ * 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. @@ -19,33 +19,33 @@ * ============LICENSE_END========================================================= */ -import { Component, Input, Output, EventEmitter} from "@angular/core"; -import {PropertyFEModel, DerivedFEProperty, InstanceFePropertiesMap} from "app/models"; -import {PropertiesService} from "../../../services/properties.service"; -import {ModalService} from "../../../services/modal.service"; -import { InstanceFeDetails } from "../../../../models/instance-fe-details"; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { DerivedFEProperty, InstanceFePropertiesMap, PropertyFEModel } from 'app/models'; +import { InstanceFeDetails } from '../../../../models/instance-fe-details'; +import { PropertiesService } from '../../../services/properties.service'; +import { ModalService } from '../../../services/modal.service'; @Component({ selector: 'properties-table', templateUrl: './properties-table.component.html', styleUrls: ['./properties-table.component.less'] }) -export class PropertiesTableComponent { +export class PropertiesTableComponent implements OnChanges { @Input() fePropertiesMap: InstanceFePropertiesMap; @Input() feInstanceNamesMap: Map<string, InstanceFeDetails>; @Input() selectedPropertyId: string; - @Input() propertyNameSearchText:string; - @Input() searchTerm:string; - @Input() readonly:boolean; - @Input() isLoading:boolean; - @Input() hasDeclareOption:boolean; - @Input() hidePropertyType:boolean; + @Input() propertyNameSearchText: string; + @Input() searchTerm: string; + @Input() readonly: boolean; + @Input() isLoading: boolean; + @Input() hasDeclareOption: boolean; + @Input() hidePropertyType: boolean; @Input() showDelete:boolean; - + @Output('propertyChanged') emitter: EventEmitter<PropertyFEModel> = new EventEmitter<PropertyFEModel>(); @Output() selectPropertyRow: EventEmitter<PropertyRowSelectedEvent> = new EventEmitter<PropertyRowSelectedEvent>(); - @Output() updateCheckedPropertyCount: EventEmitter<boolean> = new EventEmitter<boolean>();//only for hasDeclareOption and hasDeclareListOption + @Output() updateCheckedPropertyCount: EventEmitter<boolean> = new EventEmitter<boolean>(); // only for hasDeclareOption @Output() updateCheckedChildPropertyCount: EventEmitter<boolean> = new EventEmitter<boolean>();//only for hasDeclareListOption @Output() deleteProperty: EventEmitter<PropertyFEModel> = new EventEmitter<PropertyFEModel>(); private selectedPropertyToDelete: PropertyFEModel; @@ -53,41 +53,48 @@ export class PropertiesTableComponent { sortBy: String; reverse: boolean; direction: number; - path:string[]; + path: string[]; - sort(sortBy){ - this.reverse = (this.sortBy === sortBy) ? !this.reverse : true; - this.direction = this.reverse ? 1 : -1; - this.sortBy = sortBy; - this.path = sortBy.split('.'); + readonly ascUpperLettersFirst = 1; + readonly descLowerLettersFirst = -1; + + constructor(private propertiesService: PropertiesService, private modalService: ModalService ) { } - constructor (private propertiesService:PropertiesService, private modalService: ModalService){ + ngOnChanges(changes: SimpleChanges): void { + if (changes.fePropertiesMap) { + this.sortBy = ''; + this.sort('name'); + } } - - ngOnInit() { + + sort(sortBy) { + this.reverse = (this.sortBy === sortBy) ? !this.reverse : true; + this.direction = this.reverse ? this.ascUpperLettersFirst : this.descLowerLettersFirst; + this.sortBy = sortBy; + this.path = sortBy.split('.'); } onPropertyChanged = (property) => { this.emitter.emit(property); - }; + } // Click on main row (row of propertyFEModel) - onClickPropertyRow = (property:PropertyFEModel, instanceName:string, event?) => { - //event && event.stopPropagation(); + onClickPropertyRow = (property: PropertyFEModel, instanceName: string, event?) => { + // event && event.stopPropagation(); this.selectedPropertyId = property.name; - let propertyRowSelectedEvent:PropertyRowSelectedEvent = new PropertyRowSelectedEvent(property, instanceName); + const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(property, instanceName); this.selectPropertyRow.emit(propertyRowSelectedEvent); - }; + } // Click on inner row (row of DerivedFEProperty) - onClickPropertyInnerRow = (property:DerivedFEProperty, instanceName:string) => { - let propertyRowSelectedEvent:PropertyRowSelectedEvent = new PropertyRowSelectedEvent(property, instanceName); + onClickPropertyInnerRow = (property: DerivedFEProperty, instanceName: string) => { + const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(property, instanceName); this.selectPropertyRow.emit(propertyRowSelectedEvent); } propertyChecked = (prop: PropertyFEModel, childPropName?: string) => { - let isChecked: boolean = (!childPropName)? prop.isSelected : prop.flattenedChildren.find(prop => prop.propertiesName == childPropName).isSelected; + const isChecked: boolean = (!childPropName) ? prop.isSelected : prop.flattenedChildren.find((prop) => prop.propertiesName == childPropName).isSelected; if (!isChecked) { this.propertiesService.undoDisableRelatedProperties(prop, childPropName); @@ -116,11 +123,10 @@ export class PropertiesTableComponent { } export class PropertyRowSelectedEvent { - propertyModel:PropertyFEModel | DerivedFEProperty; - instanceName:string; - constructor ( propertyModel:PropertyFEModel | DerivedFEProperty, instanceName:string ){ + propertyModel: PropertyFEModel | DerivedFEProperty; + instanceName: string; + constructor( propertyModel: PropertyFEModel | DerivedFEProperty, instanceName: string ) { this.propertyModel = propertyModel; this.instanceName = instanceName; } } - @@ -4,7 +4,6 @@ import {DynamicPropertyComponent} from "./dynamic-property/dynamic-property.comp import {FormsModule} from "@angular/forms"; import {UiElementsModule} from "../../ui/ui-elements.module"; import {CommonModule} from "@angular/common"; -import {HttpModule} from "@angular/http"; import {FilterChildPropertiesPipe} from "./pipes/filterChildProperties.pipe"; import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; import {PropertiesService} from "../../../services/properties.service"; @@ -13,7 +12,6 @@ import {MultilineEllipsisModule} from "../../../shared/multiline-ellipsis/multil @NgModule({ imports: [ FormsModule, - HttpModule, CommonModule, GlobalPipesModule, UiElementsModule, @@ -9,6 +9,7 @@ import {ComponentInstanceServiceNg2} from "../../../services/component-instance- import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils"; import {Requirement} from "../../../../models/requirement"; import {Capability, RequirementCapabilityModel} from "../../../../models/capability"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; const REQUIREMENT = 'Requirement'; const CAPABILITY = 'Capability'; @@ -24,14 +25,9 @@ export class SelectRequirementOrCapabilityComponent implements OnInit { @Input() optionalRequirementsMap:Dictionary<Requirement[]>; //optional requirement map - key is type, value is array of requirements @Input() optionalCapabilitiesMap:Dictionary<Capability[]>; //optional capabilities map - key is type, value is array of capabilities - @Input() selectedReqOrCapOption:string; // the selection value chosen by the user (options: requirement / capability ) - - @Input() currentComponent:ComponentModel; @Input() componentInstanceId:string; - @Input() selectedReqOrCapModel:RequirementCapabilityModel; - @Output() updateSelectedReqOrCap:EventEmitter<RequirementCapabilityModel> = new EventEmitter<RequirementCapabilityModel>(); types:Array<string> = []; @@ -51,7 +47,8 @@ export class SelectRequirementOrCapabilityComponent implements OnInit { private _loadingCapabilityProperties: Array<Capability>; constructor(private componentInstanceServiceNg2:ComponentInstanceServiceNg2, - private propertiesUtils:PropertiesUtils) { + private propertiesUtils:PropertiesUtils, + private workspaceService: WorkspaceService) { this.selectOptions = [new RadioButtonModel(REQUIREMENT, REQUIREMENT), new RadioButtonModel(CAPABILITY, CAPABILITY)]; this._loadingCapabilityProperties = []; } @@ -171,14 +168,13 @@ export class SelectRequirementOrCapabilityComponent implements OnInit { } } - private setCapabilityProperties = ():void => { let selectedCapability = <Capability>this.selectedReqOrCapModel; if (!selectedCapability.properties) { this.loadingCapabilityProperties = true; if (this._loadingCapabilityProperties.indexOf(selectedCapability) == -1) { this._loadingCapabilityProperties.push(selectedCapability); - this.componentInstanceServiceNg2.getInstanceCapabilityProperties(this.currentComponent, this.componentInstanceId, selectedCapability) + this.componentInstanceServiceNg2.getInstanceCapabilityProperties(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.componentInstanceId, selectedCapability) .subscribe((response: Array<PropertyModel>) => { if (this.selectedReqOrCapModel === selectedCapability) { delete this.loadingCapabilityProperties; @@ -50,7 +50,7 @@ } .operation-name { - .s_1; + // .s_1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -14,34 +14,38 @@ * permissions and limitations under the License. */ -import {Component, Input, ComponentRef} from '@angular/core'; -import {ComponentServiceNg2} from 'app/ng2/services/component-services/component.service'; -import {ComponentInstanceServiceNg2} from 'app/ng2/services/component-instance-services/component-instance.service'; -import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; -import {ModalService} from 'app/ng2/services/modal.service'; -import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component'; +import { Component, ComponentRef, Input } from '@angular/core'; import { - ModalModel, ButtonModel, - OperationModel, - Service, - ServiceInstanceObject, - PropertyFEModel, - PropertyBEModel, + CapabilitiesGroup, + Capability, + Component as TopologyTemplate, InputBEModel, InterfaceModel, - CapabilitiesGroup, - Capability + ModalModel, + OperationModel, + PropertyBEModel, + PropertyFEModel } from 'app/models'; -import {ServiceConsumptionCreatorComponent} from 'app/ng2/pages/service-consumption-editor/service-consumption-editor.component'; - +import { ModalComponent } from 'app/ng2/components/ui/modal/modal.component'; +import { ServiceConsumptionCreatorComponent } from 'app/ng2/pages/service-consumption-editor/service-consumption-editor.component'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { ComponentServiceNg2 } from 'app/ng2/services/component-services/component.service'; +import { ModalService } from 'app/ng2/services/modal.service'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { Resource } from '../../../../models/components/resource'; +import { FullComponentInstance } from '../../../../models/componentsInstances/fullComponentInstance'; +import { ServiceInstanceObject } from '../../../../models/service-instance-properties-and-interfaces'; +import { ComponentFactory } from '../../../../utils/component-factory'; +import { ComponentType } from '../../../../utils/constants'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; export class ConsumptionInput extends PropertyFEModel{ inputId: string; type: string; source: string; value: any; - constraints: Array<any>; + constraints: any[]; constructor(input?: any) { super(input); @@ -49,19 +53,20 @@ export class ConsumptionInput extends PropertyFEModel{ this.inputId = input.inputId; this.type = input.type; this.source = input.source; - this.value = input.value || ""; + this.value = input.value || ''; this.constraints = input.constraints; } } } +// tslint:disable-next-line:max-classes-per-file export class ConsumptionInputDetails extends ConsumptionInput { name: string; expanded: boolean; assignValueLabel: string; - associatedProps: Array<string>; - associatedInterfaces: Array<any>; - associatedCapabilities: Array<Capability>; + associatedProps: string[]; + associatedInterfaces: any[]; + associatedCapabilities: Capability[]; origVal: string; isValid: boolean; @@ -74,7 +79,7 @@ export class ConsumptionInputDetails extends ConsumptionInput { this.associatedProps = input.associatedProps; this.associatedInterfaces = input.associatedInterfaces; this.associatedCapabilities = input.associatedCapabilities; - this.origVal = input.value || ""; + this.origVal = input.value || ''; this.isValid = input.isValid; } } @@ -84,9 +89,10 @@ export class ConsumptionInputDetails extends ConsumptionInput { } } +// tslint:disable-next-line:max-classes-per-file export class ServiceOperation { operation: OperationModel; - consumptionInputs: Array<ConsumptionInputDetails>; + consumptionInputs: ConsumptionInputDetails[]; constructor(input?: any) { if (input) { @@ -96,24 +102,24 @@ export class ServiceOperation { } } +// tslint:disable-next-line:max-classes-per-file export class InterfaceWithServiceOperation { interfaceId: string; displayName: string; - operationsList: Array<ServiceOperation>; + operationsList: ServiceOperation[]; isExpanded: boolean; constructor(input?: InterfaceModel) { if (input) { this.interfaceId = input.uniqueId; this.displayName = input.displayType(); - this.operationsList = _.map(input.operations, operation => new ServiceOperation({operation: operation})); + this.operationsList = _.map(input.operations, (operation) => new ServiceOperation({operation: operation})); this.isExpanded = true; } } } - - +// tslint:disable-next-line:max-classes-per-file @Component({ selector: 'service-consumption', templateUrl: './service-consumption.component.html', @@ -125,21 +131,23 @@ export class ServiceConsumptionComponent { modalInstance: ComponentRef<ModalComponent>; isLoading: boolean = false; - interfacesList: Array<InterfaceWithServiceOperation>; - operationsGroup: Array<ServiceOperation>; - @Input() parentServiceInputs: Array<InputBEModel> = []; - @Input() parentService: Service; - @Input() selectedService: Service; + interfacesList: InterfaceWithServiceOperation[]; + operationsGroup: ServiceOperation[]; + @Input() parentServiceInputs: InputBEModel[] = []; + @Input() parentService: ComponentMetadata; + @Input() selectedService: TopologyTemplate | FullComponentInstance; @Input() selectedServiceInstanceId: string; - @Input() instancesMappedList: Array<ServiceInstanceObject>; - @Input() instancesCapabilitiesMap: Map<string, Array<Capability>>; + @Input() instancesMappedList: ServiceInstanceObject[]; + @Input() instancesCapabilitiesMap: Map<string, Capability[]>; @Input() readonly: boolean; - selectedInstanceSiblings: Array<ServiceInstanceObject>; - selectedInstancePropertiesList: Array<PropertyBEModel> = []; - selectedInstanceCapabilitisList: Array<Capability> = []; + selectedInstanceSiblings: ServiceInstanceObject[]; + selectedInstancePropertiesList: PropertyBEModel[] = []; + selectedInstanceCapabilitisList: Capability[] = []; - constructor(private ModalServiceNg2: ModalService, private serviceServiceNg2: ServiceServiceNg2, private componentServiceNg2: ComponentServiceNg2, private componentInstanceServiceNg2:ComponentInstanceServiceNg2) {} + constructor(private modalServiceNg2: ModalService, private topologyTemplateService: TopologyTemplateService, + private componentServiceNg2: ComponentServiceNg2, private componentInstanceServiceNg2: ComponentInstanceServiceNg2, + private componentFactory: ComponentFactory) {} ngOnInit() { this.updateSelectedInstancePropertiesAndSiblings(); @@ -147,15 +155,15 @@ export class ServiceConsumptionComponent { } ngOnChanges(changes) { - if(changes.selectedServiceInstanceId && changes.selectedServiceInstanceId.currentValue !== changes.selectedServiceInstanceId.previousValue) { + if (changes.selectedServiceInstanceId && changes.selectedServiceInstanceId.currentValue !== changes.selectedServiceInstanceId.previousValue) { this.selectedServiceInstanceId = changes.selectedServiceInstanceId.currentValue; - if(changes.selectedService && changes.selectedService.currentValue !== changes.selectedService.previousValue) { + if (changes.selectedService && changes.selectedService.currentValue !== changes.selectedService.previousValue) { this.selectedService = changes.selectedService.currentValue; } this.updateSelectedInstancePropertiesAndSiblings(); this.updateSelectedServiceCapabilities(); } - if(changes.instancesMappedList && !_.isEqual(changes.instancesMappedList.currentValue, changes.instancesMappedList.previousValue)) { + if (changes.instancesMappedList && !_.isEqual(changes.instancesMappedList.currentValue, changes.instancesMappedList.previousValue)) { this.updateSelectedInstancePropertiesAndSiblings(); this.updateSelectedServiceCapabilities(); } @@ -163,22 +171,22 @@ export class ServiceConsumptionComponent { updateSelectedInstancePropertiesAndSiblings() { this.interfacesList = []; - let selectedInstanceMetadata: ServiceInstanceObject = _.find(this.instancesMappedList, coInstance => coInstance.id === this.selectedServiceInstanceId); + const selectedInstanceMetadata: ServiceInstanceObject = _.find(this.instancesMappedList, (coInstance) => coInstance.id === this.selectedServiceInstanceId); if (selectedInstanceMetadata) { - _.forEach(selectedInstanceMetadata.interfaces, (interfaceData:InterfaceModel) => { + _.forEach(selectedInstanceMetadata.interfaces, (interfaceData: InterfaceModel) => { this.interfacesList.push(new InterfaceWithServiceOperation(interfaceData)); }); } - this.interfacesList.sort((interf1:InterfaceWithServiceOperation, interf2:InterfaceWithServiceOperation) => interf1.displayName.localeCompare(interf2.displayName)); + this.interfacesList.sort((interf1: InterfaceWithServiceOperation, interf2: InterfaceWithServiceOperation) => interf1.displayName.localeCompare(interf2.displayName)); this.selectedInstancePropertiesList = selectedInstanceMetadata && selectedInstanceMetadata.properties; - this.selectedInstanceSiblings = _.filter(this.instancesMappedList, coInstance => coInstance.id !== this.selectedServiceInstanceId); + this.selectedInstanceSiblings = _.filter(this.instancesMappedList, (coInstance) => coInstance.id !== this.selectedServiceInstanceId); } updateSelectedServiceCapabilities() { this.selectedInstanceCapabilitisList = _.filter( CapabilitiesGroup.getFlattenedCapabilities(this.selectedService.capabilities), - cap => cap.properties && cap.ownerId === this.selectedService.uniqueId + (cap) => cap.properties && cap.ownerId === this.selectedService.uniqueId ); } @@ -186,15 +194,15 @@ export class ServiceConsumptionComponent { currInterface.isExpanded = !currInterface.isExpanded; } - onSelectOperation(event, currInterface:InterfaceWithServiceOperation, opIndex: number) { + onSelectOperation(event, currInterface: InterfaceWithServiceOperation, opIndex: number) { event.stopPropagation(); - if(!this.readonly) { + if (!this.readonly) { this.operationsGroup = currInterface.operationsList; - let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.ModalServiceNg2.closeCurrentModal); - let saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createOrUpdateOperationInput, this.getDisabled); - let modalModel: ModalModel = new ModalModel('l', 'Modify Operation Consumption', '', [saveButton, cancelButton], 'standard'); - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); - this.ModalServiceNg2.addDynamicContentToModal( + const cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalServiceNg2.closeCurrentModal); + const saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createOrUpdateOperationInput, this.getDisabled); + const modalModel: ModalModel = new ModalModel('l', 'Modify Operation Consumption', '', [saveButton, cancelButton], 'standard'); + this.modalInstance = this.modalServiceNg2.createCustomModal(modalModel); + this.modalServiceNg2.addDynamicContentToModal( this.modalInstance, ServiceConsumptionCreatorComponent, { @@ -215,11 +223,11 @@ export class ServiceConsumptionComponent { } } - createOrUpdateOperationInput = ():void => { + createOrUpdateOperationInput = (): void => { this.isLoading = true; - let consumptionInputsList:Array<{[id: string]: Array<ConsumptionInput>}> = _.map(this.operationsGroup, (serviceOp) => { - let consumptionInputsArr: Array<any> = []; - if(serviceOp.consumptionInputs) { + const consumptionInputsList: Array<{[id: string]: ConsumptionInput[]}> = _.map(this.operationsGroup, (serviceOp) => { + let consumptionInputsArr: any[] = []; + if (serviceOp.consumptionInputs) { consumptionInputsArr = _.map(serviceOp.consumptionInputs, (input: ConsumptionInputDetails) => { return { inputId: input.inputId, @@ -233,16 +241,24 @@ export class ServiceConsumptionComponent { [serviceOp.operation.uniqueId]: consumptionInputsArr }; }); - this.serviceServiceNg2.createOrUpdateServiceConsumptionInputs(this.parentService,this.selectedServiceInstanceId, consumptionInputsList).subscribe(() => { + this.topologyTemplateService.createOrUpdateServiceConsumptionInputs(this.convertMetaDataToComponent(this.parentService).uniqueId, this.selectedServiceInstanceId, consumptionInputsList) + .subscribe(() => { this.isLoading = false; - }, err=> { + }, (err) => { this.isLoading = false; }); - this.ModalServiceNg2.closeCurrentModal(); - }; + this.modalServiceNg2.closeCurrentModal(); + } - getDisabled = ():boolean => { + getDisabled = (): boolean => { return !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit(); - }; + } + + //TODO remove when workspace page convert to angular5 + convertMetaDataToComponent(componentMetadata: ComponentMetadata) { + const newResource: Resource = this.componentFactory.createEmptyComponent(ComponentType.RESOURCE) as Resource; + newResource.setComponentMetadata(componentMetadata); + return newResource; + } }
\ No newline at end of file @@ -14,11 +14,11 @@ * permissions and limitations under the License. */ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServiceConsumptionComponent} from "./service-consumption.component"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from 'app/ng2/shared/translator/translate.module'; +import { ServiceConsumptionComponent } from './service-consumption.component'; @NgModule({ declarations: [ @@ -29,11 +29,13 @@ import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; UiElementsModule, TranslateModule ], - exports: [], + exports: [ + ServiceConsumptionComponent + ], entryComponents: [ ServiceConsumptionComponent ], providers: [] }) export class ServiceConsumptionModule { -}
\ No newline at end of file +} @@ -38,7 +38,7 @@ justify-content: space-between; .rule-details { - .s_1; + // .s_1; display: flex; flex: 1; align-items: center; @@ -67,4 +67,13 @@ } } + .w-sdc-designer-sidebar-section-footer { + margin-top: 10px; + text-align: center; + width: 100%; + } + .w-sdc-designer-sidebar-section-footer-action { + width: 180px; + margin-top: 10px; + } }
\ No newline at end of file @@ -13,22 +13,22 @@ * or implied. See the License for the specific language governing * permissions and limitations under the License. */ -import {Component, Input, Output, EventEmitter, ComponentRef} from '@angular/core'; -import {ModalService} from 'app/ng2/services/modal.service'; +import { Component, ComponentRef, EventEmitter, Input, Output } from '@angular/core'; import { - Service, + ButtonModel, ComponentInstance, + InputBEModel, ModalModel, - ButtonModel, PropertyBEModel, - InputBEModel, - ServiceInstanceObject } from 'app/models'; -import {ServiceDependenciesEditorComponent} from 'app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component'; -import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component'; -import {ComponentServiceNg2} from 'app/ng2/services/component-services/component.service'; -import {TranslateService} from 'app/ng2/shared/translator/translate.service'; -import {ComponentGenericResponse} from 'app/ng2/services/responses/component-generic-response'; +import { ModalComponent } from 'app/ng2/components/ui/modal/modal.component'; +import { ServiceDependenciesEditorComponent } from 'app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component'; +import { ModalService } from 'app/ng2/services/modal.service'; +import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response'; +import { TranslateService } from 'app/ng2/shared/translator/translate.service'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ServiceInstanceObject } from '../../../../models/service-instance-properties-and-interfaces'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; export class ConstraintObject { servicePropertyName: string; @@ -48,12 +48,13 @@ export class ConstraintObject { } } +// tslint:disable-next-line:max-classes-per-file export class ConstraintObjectUI extends ConstraintObject{ isValidValue: boolean; constructor(input?: any) { super(input); - if(input) { + if (input) { this.isValidValue = input.isValidValue ? input.isValidValue : input.value !== ''; } } @@ -63,7 +64,7 @@ export class ConstraintObjectUI extends ConstraintObject{ } public isValidRule(isStatic) { - let isValidValue = isStatic ? this.isValidValue : true; + const isValidValue = isStatic ? this.isValidValue : true; return this.servicePropertyName != null && this.servicePropertyName !== '' && this.value != null && this.value !== '' && isValidValue; } @@ -75,6 +76,7 @@ export const OPERATOR_TYPES = { LESS_THAN: 'less_than' }; +// tslint:disable-next-line:max-classes-per-file class I18nTexts { static uncheckModalTitle: string; static uncheckModalText: string; @@ -89,21 +91,21 @@ class I18nTexts { static deleteRuleMsg: string; public static translateTexts(translateService) { - I18nTexts.uncheckModalTitle = translateService.translate("SERVICE_DEPENDENCY_UNCHECK_TITLE"); - I18nTexts.uncheckModalText = translateService.translate("SERVICE_DEPENDENCY_UNCHECK_TEXT"); - I18nTexts.modalApprove = translateService.translate("MODAL_APPROVE"); - I18nTexts.modalCancel = translateService.translate("MODAL_CANCEL"); - I18nTexts.modalCreate = translateService.translate("MODAL_CREATE"); - I18nTexts.modalSave = translateService.translate("MODAL_SAVE"); - I18nTexts.modalDelete = translateService.translate("MODAL_DELETE"); - I18nTexts.addRuleTxt = translateService.translate("SERVICE_DEPENDENCY_ADD_RULE"); - I18nTexts.updateRuleTxt = translateService.translate("SERVICE_DEPENDENCY_UPDATE_RULE"); - I18nTexts.deleteRuleTxt = translateService.translate("SERVICE_DEPENDENCY_DELETE_RULE"); - I18nTexts.deleteRuleMsg = translateService.translate("SERVICE_DEPENDENCY_DELETE_RULE_MSG"); + I18nTexts.uncheckModalTitle = translateService.translate('SERVICE_DEPENDENCY_UNCHECK_TITLE'); + I18nTexts.uncheckModalText = translateService.translate('SERVICE_DEPENDENCY_UNCHECK_TEXT'); + I18nTexts.modalApprove = translateService.translate('MODAL_APPROVE'); + I18nTexts.modalCancel = translateService.translate('MODAL_CANCEL'); + I18nTexts.modalCreate = translateService.translate('MODAL_CREATE'); + I18nTexts.modalSave = translateService.translate('MODAL_SAVE'); + I18nTexts.modalDelete = translateService.translate('MODAL_DELETE'); + I18nTexts.addRuleTxt = translateService.translate('SERVICE_DEPENDENCY_ADD_RULE'); + I18nTexts.updateRuleTxt = translateService.translate('SERVICE_DEPENDENCY_UPDATE_RULE'); + I18nTexts.deleteRuleTxt = translateService.translate('SERVICE_DEPENDENCY_DELETE_RULE'); + I18nTexts.deleteRuleMsg = translateService.translate('SERVICE_DEPENDENCY_DELETE_RULE_MSG'); } } - +// tslint:disable-next-line:max-classes-per-file @Component({ selector: 'service-dependencies', templateUrl: './service-dependencies.component.html', @@ -115,56 +117,55 @@ export class ServiceDependenciesComponent { modalInstance: ComponentRef<ModalComponent>; isDependent: boolean; isLoading: boolean; - parentServiceInputs: Array<InputBEModel> = []; - rulesList: Array<ConstraintObject> = []; - operatorTypes: Array<any>; + parentServiceInputs: InputBEModel[] = []; + rulesList: ConstraintObject[] = []; + operatorTypes: any[]; @Input() readonly: boolean; - @Input() compositeService: Service; + @Input() compositeService: ComponentMetadata; @Input() currentServiceInstance: ComponentInstance; - @Input() selectedInstanceSiblings: Array<ServiceInstanceObject>; - @Input() selectedInstanceConstraints: Array<ConstraintObject> = []; - @Input() selectedInstanceProperties: Array<PropertyBEModel> = []; - @Output() updateRulesListEvent:EventEmitter<Array<ConstraintObject>> = new EventEmitter<Array<ConstraintObject>>(); + @Input() selectedInstanceSiblings: ServiceInstanceObject[]; + @Input() selectedInstanceConstraints: ConstraintObject[] = []; + @Input() selectedInstanceProperties: PropertyBEModel[] = []; + @Output() updateRulesListEvent: EventEmitter<ConstraintObject[]> = new EventEmitter<ConstraintObject[]>(); @Output() loadRulesListEvent:EventEmitter<any> = new EventEmitter(); @Output() dependencyStatus = new EventEmitter<boolean>(); - - constructor(private componentServiceNg2: ComponentServiceNg2, private ModalServiceNg2: ModalService, private translateService: TranslateService) { + constructor(private topologyTemplateService: TopologyTemplateService, private modalServiceNg2: ModalService, private translateService: TranslateService) { } ngOnInit() { this.isLoading = false; this.operatorTypes = [ - {label: ">", value: OPERATOR_TYPES.GREATER_THAN}, - {label: "<", value: OPERATOR_TYPES.LESS_THAN}, - {label: "=", value: OPERATOR_TYPES.EQUAL} + {label: '>', value: OPERATOR_TYPES.GREATER_THAN}, + {label: '<', value: OPERATOR_TYPES.LESS_THAN}, + {label: '=', value: OPERATOR_TYPES.EQUAL} ]; - this.componentServiceNg2.getComponentInputsWithProperties(this.compositeService).subscribe((result: ComponentGenericResponse) => { + this.topologyTemplateService.getComponentInputsWithProperties(this.compositeService.componentType, this.compositeService.uniqueId).subscribe((result: ComponentGenericResponse) => { this.parentServiceInputs = result.inputs; }); this.loadRules(); - this.translateService.languageChangedObservable.subscribe(lang => { + this.translateService.languageChangedObservable.subscribe((lang) => { I18nTexts.translateTexts(this.translateService); }); } ngOnChanges(changes) { - if(changes.currentServiceInstance) { + if (changes.currentServiceInstance) { this.currentServiceInstance = changes.currentServiceInstance.currentValue; this.isDependent = this.currentServiceInstance.isDependent(); } - if(changes.selectedInstanceConstraints && changes.selectedInstanceConstraints.currentValue !== changes.selectedInstanceConstraints.previousValue) { + if (changes.selectedInstanceConstraints && changes.selectedInstanceConstraints.currentValue !== changes.selectedInstanceConstraints.previousValue) { this.selectedInstanceConstraints = changes.selectedInstanceConstraints.currentValue; this.loadRules(); } } public openRemoveDependencyModal = (): ComponentRef<ModalComponent> => { - let actionButton: ButtonModel = new ButtonModel(I18nTexts.modalApprove, 'blue', this.onUncheckDependency); - let cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'grey', this.onCloseRemoveDependencyModal); - let modalModel: ModalModel = new ModalModel('sm', I18nTexts.uncheckModalTitle, I18nTexts.uncheckModalText, [actionButton, cancelButton]); - return this.ModalServiceNg2.createCustomModal(modalModel); + const actionButton: ButtonModel = new ButtonModel(I18nTexts.modalApprove, 'blue', this.onUncheckDependency); + const cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'grey', this.onCloseRemoveDependencyModal); + const modalModel: ModalModel = new ModalModel('sm', I18nTexts.uncheckModalTitle, I18nTexts.uncheckModalText, [actionButton, cancelButton]); + return this.modalServiceNg2.createCustomModal(modalModel); } loadRules() { @@ -178,60 +179,59 @@ export class ServiceDependenciesComponent { } onUncheckDependency = () => { - this.ModalServiceNg2.closeCurrentModal(); + this.modalServiceNg2.closeCurrentModal(); this.isLoading = true; - let isDepOrig = this.isDependent; - let rulesListOrig = this.rulesList; + const isDepOrig = this.isDependent; + const rulesListOrig = this.rulesList; this.currentServiceInstance.unmarkAsDependent(); this.updateComponentInstance(isDepOrig, rulesListOrig); } onCloseRemoveDependencyModal = () => { this.isDependent = true; - this.ModalServiceNg2.closeCurrentModal(); + this.modalServiceNg2.closeCurrentModal(); } onCheckDependency = () => { - let isDepOrig = this.isDependent; - let rulesListOrig = this.rulesList; + const isDepOrig = this.isDependent; + const rulesListOrig = this.rulesList; this.currentServiceInstance.markAsDependent(); this.rulesList = []; this.updateComponentInstance(isDepOrig, rulesListOrig); } onMarkAsDependent() { - if(!this.currentServiceInstance.isDependent()) { + if (!this.currentServiceInstance.isDependent()) { this.onCheckDependency(); - } - else { + } else { this.openRemoveDependencyModal().instance.open(); } } - updateComponentInstance(isDependent_origVal : boolean, rulesList_orig: Array<ConstraintObject>) { + updateComponentInstance(isDependentOrigVal: boolean, rulesListOrig: ConstraintObject[]) { this.isLoading = true; - this.componentServiceNg2.updateComponentInstance(this.compositeService, this.currentServiceInstance).subscribe((updatedServiceIns: ComponentInstance) => { + this.topologyTemplateService.updateComponentInstance(this.compositeService.uniqueId, this.currentServiceInstance).subscribe((updatedServiceIns: ComponentInstance) => { this.currentServiceInstance = new ComponentInstance(updatedServiceIns); this.isDependent = this.currentServiceInstance.isDependent(); this.dependencyStatus.emit(this.isDependent); - if(this.isDependent) { + if (this.isDependent) { this.loadRulesListEvent.emit(); } this.isLoading = false; - }, err=> { - this.isDependent = isDependent_origVal; - this.rulesList = rulesList_orig; + }, (err) => { + this.isDependent = isDependentOrigVal; + this.rulesList = rulesListOrig; this.isLoading = false; console.log('An error has occurred.'); }); } - onAddRule () { - let cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'outline white', this.ModalServiceNg2.closeCurrentModal); - let saveButton: ButtonModel = new ButtonModel(I18nTexts.modalCreate, 'blue', this.createRule, this.getDisabled); - let modalModel: ModalModel = new ModalModel('l', I18nTexts.addRuleTxt, '', [saveButton, cancelButton], 'standard'); - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); - this.ModalServiceNg2.addDynamicContentToModal( + onAddRule() { + const cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'outline white', this.modalServiceNg2.closeCurrentModal); + const saveButton: ButtonModel = new ButtonModel(I18nTexts.modalCreate, 'blue', this.createRule, this.getDisabled); + const modalModel: ModalModel = new ModalModel('l', I18nTexts.addRuleTxt, '', [saveButton, cancelButton], 'standard'); + this.modalInstance = this.modalServiceNg2.createCustomModal(modalModel); + this.modalServiceNg2.addDynamicContentToModal( this.modalInstance, ServiceDependenciesEditorComponent, { @@ -247,16 +247,16 @@ export class ServiceDependenciesComponent { } onSelectRule(index: number) { - let cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'outline white', this.ModalServiceNg2.closeCurrentModal); - let saveButton: ButtonModel = new ButtonModel(I18nTexts.modalSave, 'blue', () => this.updateRules(), this.getDisabled); - let modalModel: ModalModel = new ModalModel('l', I18nTexts.updateRuleTxt, '', [saveButton, cancelButton], 'standard'); - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); - this.ModalServiceNg2.addDynamicContentToModal( + const cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'outline white', this.modalServiceNg2.closeCurrentModal); + const saveButton: ButtonModel = new ButtonModel(I18nTexts.modalSave, 'blue', () => this.updateRules(), this.getDisabled); + const modalModel: ModalModel = new ModalModel('l', I18nTexts.updateRuleTxt, '', [saveButton, cancelButton], 'standard'); + this.modalInstance = this.modalServiceNg2.createCustomModal(modalModel); + this.modalServiceNg2.addDynamicContentToModal( this.modalInstance, ServiceDependenciesEditorComponent, { serviceRuleIndex: index, - serviceRules: _.map(this.rulesList, rule => new ConstraintObjectUI(rule)), + serviceRules: _.map(this.rulesList, (rule) => new ConstraintObjectUI(rule)), currentServiceName: this.currentServiceInstance.name, operatorTypes: this.operatorTypes, compositeServiceName: this.compositeService.name, @@ -268,40 +268,40 @@ export class ServiceDependenciesComponent { this.modalInstance.instance.open(); } - getDisabled = ():boolean => { + getDisabled = (): boolean => { return !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit(); - }; + } - createRule = ():void => { - let newRuleToCreate: ConstraintObject = new ConstraintObject(this.modalInstance.instance.dynamicContent.instance.currentRule); + createRule = (): void => { + const newRuleToCreate: ConstraintObject = new ConstraintObject(this.modalInstance.instance.dynamicContent.instance.currentRule); this.isLoading = true; - this.componentServiceNg2.createServiceFilterConstraints( - this.compositeService, - this.currentServiceInstance, + this.topologyTemplateService.createServiceFilterConstraints( + this.compositeService.uniqueId, + this.currentServiceInstance.uniqueId, newRuleToCreate ).subscribe( (response) => { this.updateRulesListEvent.emit(response.properties); this.isLoading = false; - }, err=> { + }, (err) => { this.isLoading = false; }); - this.ModalServiceNg2.closeCurrentModal(); - }; + this.modalServiceNg2.closeCurrentModal(); + } - updateRules = ():void => { - let allRulesToUpdate: Array<ConstraintObject> = this.modalInstance.instance.dynamicContent.instance.serviceRulesList.map(rule => new ConstraintObject(rule)); + updateRules = (): void => { + const allRulesToUpdate: ConstraintObject[] = this.modalInstance.instance.dynamicContent.instance.serviceRulesList.map((rule) => new ConstraintObject(rule)); this.isLoading = true; - this.componentServiceNg2.updateServiceFilterConstraints( - this.compositeService, - this.currentServiceInstance, + this.topologyTemplateService.updateServiceFilterConstraints( + this.compositeService.uniqueId, + this.currentServiceInstance.uniqueId, allRulesToUpdate ).subscribe((response) => { this.updateRulesListEvent.emit(response.properties); this.isLoading = false; - }, err => { + }, (err) => { this.isLoading = false; }); - this.ModalServiceNg2.closeCurrentModal(); + this.modalServiceNg2.closeCurrentModal(); } getSymbol(constraintOperator) { @@ -312,23 +312,23 @@ export class ServiceDependenciesComponent { } } - onDeleteRule = (index:number) => { + onDeleteRule = (index: number) => { this.isLoading = true; - this.componentServiceNg2.deleteServiceFilterConstraints( - this.compositeService, - this.currentServiceInstance, + this.topologyTemplateService.deleteServiceFilterConstraints( + this.compositeService.uniqueId, + this.currentServiceInstance.uniqueId, index ).subscribe( (response) => { this.updateRulesListEvent.emit(response.properties); this.isLoading = false; - }, err=> { + }, (err) => { this.isLoading = false; }); - this.ModalServiceNg2.closeCurrentModal(); - }; + this.modalServiceNg2.closeCurrentModal(); + } - openDeleteModal = (index:number) => { - this.ModalServiceNg2.createActionModal(I18nTexts.deleteRuleTxt, I18nTexts.deleteRuleMsg, + openDeleteModal = (index: number) => { + this.modalServiceNg2.createActionModal(I18nTexts.deleteRuleTxt, I18nTexts.deleteRuleMsg, I18nTexts.modalDelete, () => this.onDeleteRule(index), I18nTexts.modalCancel).instance.open(); } -}
\ No newline at end of file +} @@ -1,9 +1,9 @@ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServiceDependenciesComponent} from "./service-dependencies.component"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from 'app/ng2/shared/translator/translate.module'; +import { ServiceDependenciesComponent } from './service-dependencies.component'; @NgModule({ declarations: [ @@ -14,11 +14,13 @@ import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; UiElementsModule, TranslateModule ], - exports: [], + exports: [ + ServiceDependenciesComponent + ], entryComponents: [ ServiceDependenciesComponent ], providers: [] }) export class ServiceDependenciesModule { -}
\ No newline at end of file +} @@ -1,133 +0,0 @@ -import {Component, Input, KeyValueDiffer, IterableDiffers, KeyValueDiffers, DoCheck} from '@angular/core'; -import {Service} from "app/models/components/service"; -import {TranslateService} from "app/ng2/shared/translator/translate.service"; -import {ForwardingPath} from "app/models/forwarding-path"; -import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; - -@Component({ - selector: 'service-path-selector', - templateUrl: './service-path-selector.component.html', - styleUrls:['service-path-selector.component.less'], - providers: [TranslateService] -}) - -export class ServicePathSelectorComponent implements DoCheck { - - defaultSelectedId: string; - hideAllValue: string; - hideAllId: string = '0'; - showAllValue: string; - showAllId: string = '1'; - - paths: Array<ForwardingPath> = []; - dropdownOptions: Array<DropdownValue>; - differ: KeyValueDiffer; - - @Input() service: Service; - @Input() drawPath: Function; - @Input() deletePaths: Function; - @Input() selectedPathId: string; - - constructor(private differs: KeyValueDiffers, private translateService: TranslateService) { - - this.defaultSelectedId = this.hideAllId; - this.convertPathsToDropdownOptions(); - - this.translateService.languageChangedObservable.subscribe(lang => { - this.hideAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_HIDE_ALL_VALUE"); - this.showAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_SHOW_ALL_VALUE"); - this.convertPathsToDropdownOptions(); - }); - - } - - ngOnInit(): void { - - this.selectedPathId = this.defaultSelectedId; - this.differ = this.differs.find(this.service.forwardingPaths).create(null); - - } - - ngDoCheck(): void { - - const pathsChanged = this.differ.diff(this.service.forwardingPaths); - - if (pathsChanged) { - let oldPaths = _.cloneDeep(this.paths); - this.populatePathsFromService(); - - if (!(_.isEqual(oldPaths, this.paths))) { - this.convertPathsToDropdownOptions(); - - let temp = this.selectedPathId; - this.selectedPathId = '-1'; - - setTimeout(() => { - this.selectedPathId = temp; - this.onSelectPath(); - }, 0); - } - } - - } - - populatePathsFromService(): void { - - this.paths = []; - let {forwardingPaths} = this.service; - - _.forEach(forwardingPaths, path => { - this.paths.push(path); - }); - this.paths.sort((a:ForwardingPath, b:ForwardingPath)=> { - return a.name.localeCompare(b.name); - }); - - } - - convertPathsToDropdownOptions(): void { - - let result = [ - new DropdownValue(this.hideAllId, this.hideAllValue), - new DropdownValue(this.showAllId, this.showAllValue) - ]; - - _.forEach(this.paths, (value: ForwardingPath) => { - result[result.length] = new DropdownValue(value.uniqueId, value.name); - }); - - this.dropdownOptions = result; - - } - - onSelectPath = (): void => { - - if (this.selectedPathId !== '-1') { - this.deletePaths(); - - switch (this.selectedPathId) { - case this.hideAllId: - break; - - case this.showAllId: - _.forEach(this.paths, path => - this.drawPath(path) - ); - break; - - default: - let path = this.paths.find(path => - path.uniqueId === this.selectedPathId - ); - if (!path) { - this.selectedPathId = this.defaultSelectedId; - this.onSelectPath(); // currently does nothing in default case, but if one day it does, we want the selection to behave accordingly. - break; - } - this.drawPath(path); - break; - } - } - - } -} @@ -1,31 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<div class='service-path'> - <button class='zoom-icons create-path-button' data-tests-id="pathsMenuBtn" (click)="showServicePathMenu = !showServicePathMenu">...</button> - <div class="service-path-menu" *ngIf="showServicePathMenu"> - <div > - <ul> - <li *ngIf='!isViewOnly'><div class="hand" (click)="onCreateServicePath()" data-tests-id="createPathMenuItem"> - Create Service Flow - </div></li> - <li><div class="hand" (click)="onListServicePath()" data-tests-id="pathsListMenuItem"> - Service Flows List - </div></li> - </ul> - </div> - </div> -</div>
\ No newline at end of file @@ -1,51 +0,0 @@ -//@import 'src/assets/styles/variables.less'; -@import './../../../../../assets/styles/variables.less'; -.service-path { - position: relative; - .create-path-button{ - &:extend(.search-bar-button); - width: 30px; - height: 30px; - &:hover { - color: @main_color_a; - } - &:active { - background: @main_color_a; - color: @main_color_p; - } - &:focus { - outline: none; - } - } - .service-path-menu { - border: 1px solid @main_color_o; - border-radius: 0 0 2px 2px; - border-top-color: @main_color_a; - border-top-width: 3px; - - box-sizing: border-box; - box-shadow: 0 2px 4px 0 rgba(0,0,0,0.30); - - background-color: @main_color_p; - - padding: 5px 0; - right: 34px; - position: absolute; - top: 10px; - width: 150px; - font-size: 13px; - font-family: @font-opensans-regular; - - li { - color: @main_color_m; - padding: 0 10px; - line-height: 20px; - &:hover { - cursor: pointer; - color: @main_color_a; - background-color: fade(@main_color_a, 5%); - } - } - - } -} @@ -1,84 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import {Component, Input, ComponentRef} from '@angular/core'; -import {ModalService} from 'app/ng2/services/modal.service'; -import {ModalModel, ButtonModel} from 'app/models'; -import {ServicePathCreatorComponent} from 'app/ng2/pages/service-path-creator/service-path-creator.component'; -import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component'; -import ServicePathsListComponent from "app/ng2/pages/service-paths-list/service-paths-list.component"; -import {Service} from "app/models/components/service"; - -@Component({ - selector: 'service-path', - templateUrl: './service-path.component.html', - styleUrls: ['service-path.component.less'], - providers: [ModalService] -}) - -export class ServicePathComponent { - showServicePathMenu: boolean = false; - modalInstance: ComponentRef<ModalComponent>; - @Input() service: Service; - @Input() onCreate: Function; - @Input() onSave: Function; - @Input() isViewOnly:boolean; - - constructor(private ModalServiceNg2: ModalService) {} - - onCreateServicePath = ():void => { - this.showServicePathMenu = false; - let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.ModalServiceNg2.closeCurrentModal); - let saveButton: ButtonModel = new ButtonModel('Create', 'blue', this.createPath, this.getDisabled ); - let modalModel: ModalModel = new ModalModel('l', 'Create Service Flow', '', [saveButton, cancelButton], 'standard', true); - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); - this.ModalServiceNg2.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {service: this.service}); - this.modalInstance.instance.open(); - }; - - onListServicePath = ():void => { - this.showServicePathMenu = false; - let cancelButton: ButtonModel = new ButtonModel('Close', 'outline white', this.ModalServiceNg2.closeCurrentModal); - let modalModel: ModalModel = new ModalModel('md', 'Service Flows List','', [cancelButton], 'standard', true); - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); - this.ModalServiceNg2.addDynamicContentToModal(this.modalInstance, ServicePathsListComponent, {service: this.service, - onCreateServicePath: this.onCreateServicePath, onEditServicePath: this.onEditServicePath, isViewOnly: this.isViewOnly}); - this.modalInstance.instance.open(); - }; - - createPath = ():void => { - this.onCreate(this.modalInstance.instance.dynamicContent.instance.createServicePathData()); - this.ModalServiceNg2.closeCurrentModal(); - }; - - onEditServicePath = (id:string):void => { - let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.ModalServiceNg2.closeCurrentModal); - let saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createPath, this.getDisabled ); - let modalModel: ModalModel = new ModalModel('l', 'Edit Path', '', [saveButton, cancelButton], 'standard', true); - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); - this.ModalServiceNg2.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {service: this.service, pathId: id}); - this.modalInstance.instance.open(); - }; - - getDisabled = ():boolean => { - return this.isViewOnly || !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit(); - }; -} - @@ -0,0 +1,18 @@ +<form> + <div> + <div class="comment-modal-text" [innerHTML]="message"></div> + <sdc-textarea #comment1 + [(value)]="comment.text" + placeHolder="{{'CONFIRMATION_MODAL_PLACEHOLDER' | translate }}" + [required]="true" + name="comment1" + testId="checkindialog" + [maxLength]="256"> + </sdc-textarea> + <sdc-validation [validateElement]="comment1" (validityChanged)="onValidityChange($event)"> + <sdc-required-validator message="{{ 'VALIDATION_ERROR_REQUIRED' | translate:{'field': 'Comment' } }}"></sdc-required-validator> + <sdc-regex-validator message="{{ 'VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED' | translate}}" + [pattern]="commentValidationPattern"></sdc-regex-validator> + </sdc-validation> + </div> +</form> @@ -0,0 +1,25 @@ +/** + * Created by rc2122 on 5/31/2018. + */ +import { Component, Input } from "@angular/core"; +import { ValidationConfiguration } from "app/models"; +import { Subject } from "rxjs/Subject"; + +@Component({ + selector: 'comment-modal', + templateUrl: './comment-modal.component.html', + styleUrls: ['./comment-modal.less'] +}) + +export class CommentModalComponent { + + @Input() message:string; + onValidationChange: Subject<boolean> = new Subject(); + //@Input() showComment:boolean; + private comment = {"text": ''}; + private commentValidationPattern = ValidationConfiguration.validation.validationPatterns.comment; + + private onValidityChange = (isValid: boolean):void => { + this.onValidationChange.next(isValid); + } +}
\ No newline at end of file @@ -0,0 +1,3 @@ +.comment-modal-text { + padding-bottom: 5px; +}
\ No newline at end of file @@ -0,0 +1,29 @@ +/** + * Created by rc2122 on 5/24/2018. + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { CommentModalComponent } from 'app/ng2/components/modals/comment-modal/comment-modal.component'; +import { PopoverModule } from 'app/ng2/components/ui/popover/popover.module'; +import { TranslateModule } from 'app/ng2/shared/translator/translate.module'; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { OnboardingService } from '../../services/onboarding.service'; +import { ImportVSPService } from './onboarding-modal/import-vsp.service'; +import { OnboardingModalComponent } from './onboarding-modal/onboarding-modal.component'; + +@NgModule({ + declarations: [CommentModalComponent, OnboardingModalComponent], + imports: [TranslateModule, + SdcUiComponentsModule, + CommonModule, + PopoverModule, + NgxDatatableModule], + exports: [CommentModalComponent, OnboardingModalComponent], + entryComponents: [CommentModalComponent, OnboardingModalComponent], + providers: [OnboardingService, ImportVSPService], + bootstrap: [] +}) + +export class ModalsModule { +} @@ -0,0 +1,36 @@ +import { Injectable, Inject } from "@angular/core"; +import { OnboardingModalComponent } from "./onboarding-modal.component"; +import { SdcUiServices, SdcUiCommon } from "onap-ui-angular"; +import { Observable, Subject } from "rxjs"; +import { CHANGE_COMPONENT_CSAR_VERSION_FLAG } from "../../../../utils/constants"; +import { CacheService } from "../../../services/cache.service"; + + +@Injectable() +export class ImportVSPService { + + constructor(private modalService: SdcUiServices.ModalService, + private cacheService:CacheService, + @Inject("$state") private $state:ng.ui.IStateService){ + + } + + openOnboardingModal(csarUUID?: string, csarVersion?: string): Observable<any> { + var subject = new Subject<any>(); + const onboardingModalConfig = { + size: SdcUiCommon.ModalSize.xlarge, + title: 'Import VSP', + type: SdcUiCommon.ModalType.custom, + testId: 'sampleTestIdModal1', + } as SdcUiCommon.IModalConfig; + const onboardingModalInstance = this.modalService.openCustomModal(onboardingModalConfig, OnboardingModalComponent, {currentCsarUUID: csarUUID, currentCsarVersion: csarVersion}); + onboardingModalInstance.innerModalContent.instance.closeModalEvent.subscribe( + (result: any) => { + subject.next(result); + onboardingModalInstance.closeModal(); + }, (err) =>{} + ) + return subject.asObservable(); + } +} + @@ -0,0 +1,106 @@ +<div class="onboarding-modal"> + <div class="search-wrapper"> + <span class="sub-title-wrapper"> + <svg-icon class="info-button" + (click) = "openPopover($event, 'ON_BOARDING_GENERAL_INFO')" + [name]="'info-circle-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span class="sub-title">{{ 'ON_BOARDING_MODAL_SUB_TITLE' | translate }}</span> + </span> + <span class="sdc-filter-bar-wrapper"> + <sdc-filter-bar + [placeHolder]="'Search'" + (keyup)="updateFilter($event)" + [testId]="'onboarding-search'"> + </sdc-filter-bar> + </span> + </div> + <div class="datatable-components-wapper"> + <ngx-datatable #componentsMetadataTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rows]="componentsMetadataList" + [sorts]="[{prop: 'name', dir: 'asc'}]" + (select)='onSelectComponent($event)' + [selectionType]="'single'"> + <ngx-datatable-row-detail [rowHeight]="undefiend"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template > + <div class="onboarding-components-details"> + <span class="row-details-description"> + <div> + <div class="th">VSP Description:</div> + <div>{{row.description}}</div> + </div> + </span> + <span class="row-details-metadata1"> + <div *ngIf="isCsarComponentExists"> + <div class="th">VF'S Meta Data:</div> + <div><span class="th">Name:</span>{{componentFromServer.name}}</div> + <div><span class="th">Lifecycle:</span>{{componentFromServer.lifecycleState}}</div> + <div><span class="th">Creator:</span>{{componentFromServer.creatorFullName}}</div> + </div> + </span> + <span class="row-details-metadata2"> + <div *ngIf="isCsarComponentExists"> + <div class="th"> </div> + <div><span class="th">UUID:</span> {{componentFromServer.uuid}}</div> + <div><span class="th">Version:</span> {{componentFromServer.version}}</div> + <div><span class="th">Modifier:</span> {{componentFromServer.lastUpdaterFullName}}</div> + <div *ngIf="checkNotCertified()"> + <span class="note">Designers cannot update a VSP if the VF is <br>checked out by another user.</span> + </div> + </div> + </span> + <span class="row-details-metadata3"> + <svg-icon class="info-button" + (click) = "openPopover($event, isCsarComponentExists ? 'ON_BOARDING_UPDATE_INFO' : 'ON_BOARDING_IMPORT_INFO')" + [name]="'info-circle-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + </span> + <span class="row-details-icon"> + <div> + <sdc-button class="import-update-file-btn" + [text]="isCsarComponentExists ? 'Update VSP' : 'Import VSP'" + [testId]="isCsarComponentExists ? 'update-csar' : 'import-csar'" + [type]="'primary'" + [icon_name]="isCsarComponentExists ? 'sync-o' : 'alert-triangle-o'" + [icon_position]="'left'" + [icon_mode] = "'white'" + [size] = "'medium'" + (click)="importOrUpdateCsar()" + [disabled]="checkNotCertified()" + > + </sdc-button> + <svg-icon class="download-file-btn" sdc-tooltip [tooltip-text]="'Download-csar'" + [mode]="'primary'" [clickable]="true" [name]="'download-o'" + [testId]="'download-csar'" [size]="'medium'" (click)="downloadCsar(row.packageId)"> + </svg-icon> + </div> + </span> + </div> + <sdc-loader [global]="false" [active]="isLoad" [size]="'small'"[relative]="true"></sdc-loader> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column *ngFor="let column of columns" [ngSwitch]="column.prop" [resizeable]="false" [draggable]="false" name={{column.name}} + [flexGrow]="column.flexGrow"> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'name'"> + <span data-tests-id="csar-row" class="sprite table-arrow" [ngClass]="{'opened': selectedComponent && row.packageId === selectedComponent.packageId}"></span> + {{row[column.prop]}} + </ng-template> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'categories'"> + <span *ngIf="row[column.prop][0]"> + {{row[column.prop][0].name}} + <span *ngIf="row[column.prop][0].subcategories[0]">{{row[column.prop][0].subcategories[0].name}}</span> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchDefault> + {{row[column.prop]}} + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div> +</div> + + + @@ -0,0 +1,97 @@ +@import "../../../../../assets/styles/variables"; +@import "../../../../../assets/styles/mixins"; + + +.onboarding-components-details{ + display: flex; +} +.row-details-description, +.row-details-metadata1, +.row-details-metadata2, +.row-details-metadata3{ + .th { .m_14_m; } + flex-basis: 0; + overflow: hidden; + padding: 5px 15px; + white-space: normal; +} +.row-details-description, +.row-details-metadata3 { + border-right: 1px solid @main_color_o; +} + +.row-details-icon { + flex-basis: 0; + overflow: hidden; + padding: 5px 10px; + align-self: center; +} + +.row-details-description { + flex-grow: 19; +} +.row-details-metadata1 { + flex-grow: 26.5; +} +.row-details-metadata2 { + flex-grow: 35; + .note { + color: @func_color_q; + } +} +.row-details-metadata3 { + flex-grow: 8; +} +.info-button{ + cursor: pointer; + float: right; +} +.row-details-icon { + flex-grow: 18; +} +.download-file-btn { + cursor: pointer; + margin-left: 6px; +} + +.import-update-file-btn { + cursor: pointer; +} +.sprite.table-arrow{ + margin-right: 7px; +} +.search-wrapper { + .sdc-filter-bar-wrapper { + flex: 0 0 30%; + } + .sub-title-wrapper { + flex: 0 0 70%; + font-size: 15px; + line-height: 35px; + font-family: OpenSans-Regular, sans-serif; + align-items: center; + display: inline-flex; + .sub-title{ + padding-left: 5px; + } + } + display: flex; + margin-top: 15px; + margin-bottom: 10px; + } + + :host ::ng-deep { + .datatable-row-detail{ + width: 1120px; + } + .datatable-body-row { + cursor: pointer; + } + } + + + + + + + @@ -0,0 +1,122 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA} from "@angular/core"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; + +import {Observable} from "rxjs/Observable"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {SdcUiServices, SdcUiCommon} from "onap-ui-angular"; +import 'rxjs/add/observable/of'; +import {OnboardingService} from "../../../services/onboarding.service"; +import {TranslateService} from "../../../shared/translator/translate.service"; +import {CacheService} from "../../../services/cache.service"; +import {FileUtilsService} from "../../../services/file-utils.service"; +import {onboardingModalVSPMock, onboardingModalUniqueVSPMock, vspFromServerMock} from "../../../../../jest/mocks/onboarding-vsp.mock"; +import {OnboardingModalComponent} from "./onboarding-modal.component"; +import {TranslatePipe} from "../../../shared/translator/translate.pipe"; + +describe('onboarding modal component', () => { + + let fixture: ComponentFixture<OnboardingModalComponent>; + let onboardingServiceMock: Partial<OnboardingService>; + let translateServiceMock: Partial<TranslateService>; + let cacheServiceMock: Partial<CacheService>; + let fileUtilsServiceMock: Partial<FileUtilsService>; + let popoverServiceMock: Partial<SdcUiServices.PopoverService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + + beforeEach( + async(() => { + + onboardingServiceMock = { + getOnboardingComponents: jest.fn().mockImplementation(()=> Observable.of(onboardingModalUniqueVSPMock)), + getComponentFromCsarUuid: jest.fn().mockImplementation(()=> Observable.of(vspFromServerMock)) + }; + + cacheServiceMock = { + set: jest.fn() + }; + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + } + + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [OnboardingModalComponent, TranslatePipe], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: OnboardingService, useValue: onboardingServiceMock }, + { provide: TranslateService, useValue: translateServiceMock }, + { provide: CacheService, useValue: cacheServiceMock }, + { provide: FileUtilsService, useValue: fileUtilsServiceMock }, + { provide: SdcUiServices.PopoverService, useValue: popoverServiceMock }, + { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(OnboardingModalComponent); + }); + }) + ); + + /*it('should match current snapshot of onboarding modal component', () => { + expect(fixture).toMatchSnapshot(); + });*/ + + it('should see exactly 2 vsp in onboarding modal and call initOnboardingComponentsList', () => { + fixture.componentInstance.initOnboardingComponentsList(); + expect(fixture.componentInstance.componentsMetadataList.length).toBe(2); + }); + + it('should see exactly 1 vsp in onboarding modal and call initOnboardingComponentsList', () => { + fixture.componentInstance.currentCsarUUID = "6348841e79a64871ba064ce340a968a4"; + fixture.componentInstance.initOnboardingComponentsList(); + expect(fixture.componentInstance.componentsMetadataList.length).toBe(1); + }); + + it('when get a list of vsp initMaxVersionOfItemsInList will return a list with unique items with the latest versions for each packageId', () => { + onboardingServiceMock.getOnboardingComponents = jest.fn().mockImplementation(() => Observable.of(onboardingModalVSPMock)); + fixture.componentInstance.initOnboardingComponentsList(); + expect(fixture.componentInstance.componentsMetadataList.length).toBe(2); + }); + + it('should filter out 1 vsp when searching and call updateFilter function', () => { + fixture.componentInstance.initOnboardingComponentsList(); + let event = { + target : { + value : 'test new vsp' + } + } + + expect(fixture.componentInstance.componentsMetadataList.length).toBe(2); + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.componentsMetadataList.length).toBe(1); + }); + + it('When select the selected vsp the row details closed and call onSelectComponent function', () => { + fixture.componentInstance.initOnboardingComponentsList(); + fixture.componentInstance.onSelectComponent({selected: []}); + expect(fixture.componentInstance.selectedComponent).toEqual(undefined); + expect(fixture.componentInstance.componentFromServer).toEqual(undefined); + }); + + it('When select vsp a row with its details will be opened and call onSelectComponent function', () => { + fixture.componentInstance.initOnboardingComponentsList(); + fixture.componentInstance.onSelectComponent({selected: onboardingModalVSPMock}); + expect(fixture.componentInstance.selectedComponent).not.toEqual(null); + expect(fixture.componentInstance.componentFromServer).not.toEqual(undefined); + expect(fixture.componentInstance.isCsarComponentExists).toEqual(true); + }); + it('When select new vsp a row with import and download buttons will be opened and call onSelectComponent function', () => { + fixture.componentInstance.initOnboardingComponentsList(); + onboardingServiceMock.getComponentFromCsarUuid.mockImplementation(() => Observable.of(undefined)); + fixture.componentInstance.onSelectComponent({selected: onboardingModalVSPMock}); + expect(fixture.componentInstance.selectedComponent).not.toEqual(null); + expect(fixture.componentInstance.componentFromServer).toEqual(undefined); + expect(fixture.componentInstance.isCsarComponentExists).toEqual(false); + }); +}); @@ -0,0 +1,193 @@ +/** + * Created by rc2122 on 6/3/2018. + */ +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ComponentMetadata, IComponentMetadata } from '../../../../models/component-metadata'; +import { IUserProperties } from '../../../../models/user'; + +import { Resource } from '../../../../models/components/resource'; +import { ComponentType } from '../../../../utils/constants'; +import { CacheService } from '../../../services/cache.service'; +import { FileUtilsService } from '../../../services/file-utils.service'; +import { OnboardingService } from '../../../services/onboarding.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; + +export interface ImportVSPdata { + componentCsar: Resource; + previousComponent?: Resource; + type: string; +} + +// tslint:disable-next-line:interface-name +export interface IPoint { + x: number; + y: number; +} + +@Component({ + selector: 'onboarding-modal', + templateUrl: './onboarding-modal.component.html', + styleUrls: ['onboarding-modal.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class OnboardingModalComponent implements OnInit { + @Input() currentCsarUUID: string; + @Input() currentCsarVersion: string; + @ViewChild('componentsMetadataTable') table: any; + @Output() closeModalEvent: EventEmitter<ImportVSPdata> = new EventEmitter<ImportVSPdata>(); + + private columns = [ + {name: 'Name', prop: 'name', flexGrow: 22}, + {name: 'Vendor', prop: 'vendorName', flexGrow: 26}, + {name: 'Category', prop: 'categories', flexGrow: 33}, + {name: 'Version', prop: 'csarVersion', flexGrow: 10}, + {name: 'Type', prop: 'resourceType', flexGrow: 10}, + {name: '#', prop: '', flexGrow: 20} + ]; + private componentsMetadataList: IComponentMetadata[] = []; + private temp: IComponentMetadata[] = []; + private componentFromServer: ComponentMetadata; + private isCsarComponentExists: boolean = false; + private selectedComponent: ComponentMetadata; + private isLoading: boolean; + private user: IUserProperties; + + constructor(private onBoardingService: OnboardingService, + private translateService: TranslateService, + private cacheService: CacheService, + private fileUtilsService: FileUtilsService, + private popoverService: SdcUiServices.PopoverService, + private loaderService: SdcUiServices.LoaderService) { + } + + public ngOnInit(): void { + this.initOnboardingComponentsList(); + this.user = this.cacheService.get('user'); + } + + initMaxVersionOfItemsInList = (onboardingResponse: IComponentMetadata[]): void => { + // Get only the latest version of each item + this.componentsMetadataList = []; + + // group all items according to packageId + const groupByPackageIdItems = _.groupBy(onboardingResponse, 'packageId'); + // Loop on all the groups and push to componentsMetadataList the max version for each package + _.each(groupByPackageIdItems, (items: any): void => { + let maxItem: any = items[0]; + items.forEach((item) => { + if (parseFloat(maxItem.csarVersion) < parseFloat(item.csarVersion)) { + maxItem = item; + } + }); + if (maxItem) { + this.componentsMetadataList.push(maxItem); + } + }); + } + + onSelectComponent({selected}) { + this.table.rowDetail.collapseAllRows(); + if (selected[0] === this.selectedComponent) { + this.selectedComponent = undefined; + this.componentFromServer = undefined; + this.table.rowDetail.toggleExpandRow(null); + return; + } + this.isLoading = true; + this.componentFromServer = undefined; + this.selectedComponent = selected[0]; + this.onBoardingService.getComponentFromCsarUuid(this.selectedComponent.csarUUID).subscribe( + (componentFromServer: ComponentMetadata) => { + this.isLoading = false; + if (componentFromServer) { + this.componentFromServer = componentFromServer; + this.populateRowDetails(true); + } else { + this.populateRowDetails(false); + } + }, (error) => { + this.isLoading = false; + this.populateRowDetails(false); + }); + } + + populateRowDetails(isCsarComponentExists: boolean) { + this.isCsarComponentExists = isCsarComponentExists; + this.table.rowDetail.toggleExpandRow(this.selectedComponent); + } + + importOrUpdateCsar = (): void => { + const selectedComponentConverted = this.onBoardingService.convertMetaDataToComponent(this.selectedComponent); + const componentFromServerConverted = this.componentFromServer ? + this.onBoardingService.convertMetaDataToComponent(this.componentFromServer) : undefined; + const importVSPdata: ImportVSPdata = { + componentCsar: selectedComponentConverted, + previousComponent: componentFromServerConverted, + type: ComponentType.RESOURCE.toLowerCase() + }; + this.closeModalEvent.emit(importVSPdata); + } + + downloadCsar = (packageId: string): void => { + this.isLoading = true; + this.onBoardingService.downloadOnboardingCsar(packageId).subscribe( + (file: any): void => { + this.isLoading = false; + if (file.body) { + this.fileUtilsService.downloadFile(file.body, packageId + '.csar'); + } + }, (): void => { + this.isLoading = false; + } + ); + } + + updateFilter(event) { + const val = event.target.value.toLowerCase(); + + // filter our data + const temp = this.temp.filter((componentMetadata: ComponentMetadata) => { + return !val || + (componentMetadata.name && componentMetadata.name.toLowerCase().indexOf(val) !== -1) || + (componentMetadata.vendorName && componentMetadata.vendorName.toLowerCase().indexOf(val) !== -1) || + (componentMetadata.categories[0] && componentMetadata.categories[0].name.toLowerCase().indexOf(val) !== -1) || + (componentMetadata.categories[0] && componentMetadata.categories[0].subcategories[0] && componentMetadata.categories[0].subcategories[0].name.toLowerCase().indexOf(val) !== -1) || + (componentMetadata.csarVersion && componentMetadata.csarVersion.toLowerCase().indexOf(val) !== -1) || + (componentMetadata.description && componentMetadata.description.toLowerCase().indexOf(val) !== -1); + }); + + // update the rows + this.componentsMetadataList = temp; + } + + checkNotCertified = (): boolean => { + return this.componentFromServer && this.componentFromServer.lifecycleState === 'NOT_CERTIFIED_CHECKOUT' && + this.componentFromServer.lastUpdaterUserId !== this.user.userId; + } + + openPopover = ($event: any, popoverContent): void => { + this.popoverService.createPopOver('', this.translateService.translate(popoverContent), { + x: $event.pageX, + y: $event.pageY + }, 'bottom'); + } + + private initOnboardingComponentsList = (): void => { + this.loaderService.activate(); + this.onBoardingService.getOnboardingComponents().subscribe( + (onboardingResponse: IComponentMetadata[]) => { + this.loaderService.deactivate(); + if (this.currentCsarUUID) { + onboardingResponse = _.filter(onboardingResponse, (input): boolean => { + return (input as ComponentMetadata).csarUUID === this.currentCsarUUID; + }); + } + this.initMaxVersionOfItemsInList(onboardingResponse); + this.temp = [...this.componentsMetadataList]; + }, (error) => { + this.loaderService.deactivate(); + } + ); + } +} @@ -0,0 +1,145 @@ +import { Component, Input } from "@angular/core"; +import {IFileDownload, Component as TopologyTemplate, ArtifactModel, FullComponentInstance} from "app/models"; +import {EventListenerService} from "app/services"; +import {CacheService} from "app/services-ng2"; +import {EVENTS} from "app/utils"; +import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; + +@Component({ + selector: 'download-artifact', + template: ` + <svg-icon [mode]="'primary2'" [disabled]="disabled" [clickable]="!disabled" [name]="iconType" [testId]="testId" mode="info" clickable="true" size="medium" (click)="download($event)"></svg-icon> +` +}) +export class DownloadArtifactComponent { + + @Input() showLoader:boolean; + @Input() artifact:ArtifactModel; + @Input() isInstance: boolean; + @Input() downloadIconClass: string; + @Input() componentType: string; + @Input() componentId: string; + @Input() testId: string; + @Input() disabled: boolean; + + public iconType:string; + + private DOWNLOAD_CSS_CLASSES = { + DOWNLOAD_ICON: "download-o", + LOADER_ICON: "spinner" + } + constructor(private cacheService:CacheService, private EventListenerService:EventListenerService, private topologyTemplateService:TopologyTemplateService, + private componentInstanceService: ComponentInstanceServiceNg2, private workspaceService:WorkspaceService) { + + } + + ngOnInit () { + this.iconType = this.DOWNLOAD_CSS_CLASSES.DOWNLOAD_ICON; + this.initDownloadLoader(); + + } + + private initDownloadLoader = ()=> { + //if the artifact is in a middle of download progress register form callBack & change icon from download to loader + if (this.showLoader && this.cacheService.get(this.artifact.uniqueId)) { + this.EventListenerService.registerObserverCallback(EVENTS.DOWNLOAD_ARTIFACT_FINISH_EVENT + this.artifact.uniqueId, this.updateDownloadIcon); + window.setTimeout(():void => { + if (this.cacheService.get(this.artifact.uniqueId)) { + this.iconType = this.DOWNLOAD_CSS_CLASSES.LOADER_ICON; + } + }, 1000); + } + }; + + private updateDownloadIcon = () => { + this.iconType = this.downloadIconClass || this.DOWNLOAD_CSS_CLASSES.DOWNLOAD_ICON; + }; + + public download = (event) => { + event.stopPropagation(); + let onFaild = (response):void => { + console.info('onFaild', response); + this.removeDownloadedFileLoader(); + }; + + let onSuccess = (data:IFileDownload):void => { + this.downloadFile(data); + this.removeDownloadedFileLoader(); + }; + + this.setDownloadedFileLoader(); + + if (this.isInstance) { + this.componentInstanceService.downloadInstanceArtifact(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.componentId, this.artifact.uniqueId).subscribe(onSuccess, onFaild); + } else { + this.topologyTemplateService.downloadArtifact(this.componentType, this.componentId, this.artifact.uniqueId).subscribe(onSuccess, onFaild); + } + }; + + private setDownloadedFileLoader = ()=> { + if (this.showLoader) { + //set in cache service thet the artifact is in download progress + this.cacheService.set(this.artifact.uniqueId, true); + this.initDownloadLoader(); + } + }; + + private removeDownloadedFileLoader = ()=> { + if (this.showLoader) { + this.cacheService.set(this.artifact.uniqueId, false); + this.EventListenerService.notifyObservers(EVENTS.DOWNLOAD_ARTIFACT_FINISH_EVENT + this.artifact.uniqueId); + } + }; + + private downloadFile = (file:IFileDownload):void => { + if (file) { + let blob = this.base64toBlob(file.base64Contents, ''); + let fileName = file.artifactName; + this.triggerFileDownload(blob, fileName); + } + }; + + public base64toBlob = (base64Data, contentType):any => { + let byteCharacters = atob(base64Data); + return this.byteCharactersToBlob(byteCharacters, contentType); + }; + + public byteCharactersToBlob = (byteCharacters, contentType):any => { + contentType = contentType || ''; + let sliceSize = 1024; + let bytesLength = byteCharacters.length; + let slicesCount = Math.ceil(bytesLength / sliceSize); + let byteArrays = new Array(slicesCount); + + for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) { + let begin = sliceIndex * sliceSize; + let end = Math.min(begin + sliceSize, bytesLength); + + let bytes = new Array(end - begin); + for (let offset = begin, i = 0; offset < end; ++i, ++offset) { + bytes[i] = byteCharacters[offset].charCodeAt(0); + } + byteArrays[sliceIndex] = new Uint8Array(bytes); + } + return new Blob(byteArrays, {type: contentType}); + }; + + public triggerFileDownload = (blob, fileName):void=> { + let url = window.URL.createObjectURL(blob); + let downloadLink = document.createElement("a"); + + downloadLink.setAttribute('href', url); + downloadLink.setAttribute('download', fileName); + document.body.appendChild(downloadLink); + + var clickEvent = new MouseEvent("click", { + "view": window, + "bubbles": true, + "cancelable": true + }); + downloadLink.dispatchEvent(clickEvent); + + } +} @@ -37,11 +37,13 @@ enum DynamicElementComponentCreatorIdentifier { BOOLEAN, SUBNETPOOLID, ENUM, + LIST, DEFAULT } @Component({ selector: 'dynamic-element', + // Span - if constraints not empty template: `<div #target></div>`, styleUrls: ['./dynamic-element.component.less'], entryComponents: [ @@ -61,6 +63,7 @@ export class DynamicElementComponent { @Input() readonly:boolean; @Input() constraints: Array<any>; @Input() path:string;//optional param. used only for for subnetpoolid type + @Input() declared:boolean; @Input() value: any; @Output() valueChange: EventEmitter<any> = new EventEmitter<any>(); @@ -72,16 +75,19 @@ export class DynamicElementComponent { validation = ValidationConfiguration.validation; constructor( + private componentFactoryResolver: ComponentFactoryResolver, private compiler: Compiler, private el: ElementRef) { + + } updateComponent() { if (!this.isViewInitialized) { return; } - + // Factory to create component based on type or other property attributes. const prevElementCreatorIdentifier: DynamicElementComponentCreatorIdentifier = this.elementCreatorIdentifier; switch(true) { @@ -112,7 +118,7 @@ export class DynamicElementComponent { } // In case the dynamic element creator is changed, then destroy old and build new. - if (this.elementCreatorIdentifier !== prevElementCreatorIdentifier) { + if (this.declared || this.elementCreatorIdentifier !== prevElementCreatorIdentifier) { if (this.cmpRef) { this.cmpRef.destroy(); } @@ -149,64 +155,81 @@ export class DynamicElementComponent { } createComponentByIdentifier() { - switch(this.elementCreatorIdentifier) { - case DynamicElementComponentCreatorIdentifier.SUBNETPOOLID: - if(this.name.toUpperCase().indexOf("SUBNETPOOLID") == -1){//if it's an item of subnetpoolid list get the parent name - let pathArray = this.path.split("#"); - this.name = pathArray[pathArray.length - 2]; - } - this.createComponent(UiElementPopoverInputComponent); - break; + // if(!this.constraints || this.declared){ + switch(this.elementCreatorIdentifier) { + case DynamicElementComponentCreatorIdentifier.SUBNETPOOLID: + if(this.name.toUpperCase().indexOf("SUBNETPOOLID") == -1){//if it's an item of subnetpoolid list get the parent name + let pathArray = this.path.split("#"); + this.name = pathArray[pathArray.length - 2]; + } + this.createComponent(UiElementPopoverInputComponent); + break; + case DynamicElementComponentCreatorIdentifier.ENUM: + this.createComponent(UiElementDropDownComponent); + let validVals:Array<DropdownValue> = [...this.getValidValues()].map(val => new DropdownValue(val, val)); + if (this.type === 'float' || this.type === 'integer') { + this.value = this.value && Number(this.value); + validVals = _.map( + validVals, + (val) => new DropdownValue(Number(val.value), val.value) + ); + } + this.cmpRef.instance.values = validVals; + break; + case DynamicElementComponentCreatorIdentifier.INTEGER: + this.createComponent(UiElementIntegerInputComponent); + this.cmpRef.instance.pattern = this.validation.validationPatterns.integer; + break; - case DynamicElementComponentCreatorIdentifier.ENUM: - this.createComponent(UiElementDropDownComponent); - let validVals:Array<DropdownValue> = [...this.getValidValues()].map(val => new DropdownValue(val, val)); - if (this.type === 'float' || this.type === 'integer') { - this.value = this.value && Number(this.value); - validVals = _.map( - validVals, - val => new DropdownValue(Number(val.value), val.value) - ); - } - this.cmpRef.instance.values = validVals; - break; + case DynamicElementComponentCreatorIdentifier.FLOAT: + this.createComponent(UiElementIntegerInputComponent); + this.cmpRef.instance.pattern = /^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.source; + break; - case DynamicElementComponentCreatorIdentifier.INTEGER: - this.createComponent(UiElementIntegerInputComponent); - this.cmpRef.instance.pattern = this.validation.validationPatterns.integer; - break; + case DynamicElementComponentCreatorIdentifier.STRING: + this.createComponent(UiElementInputComponent); + break; - case DynamicElementComponentCreatorIdentifier.FLOAT: - this.createComponent(UiElementIntegerInputComponent); - this.cmpRef.instance.pattern = /^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.source; - break; + case DynamicElementComponentCreatorIdentifier.BOOLEAN: + this.createComponent(UiElementDropDownComponent); - case DynamicElementComponentCreatorIdentifier.STRING: - this.createComponent(UiElementInputComponent); - break; - - case DynamicElementComponentCreatorIdentifier.BOOLEAN: - this.createComponent(UiElementDropDownComponent); - - // Build drop down values - let tmp = []; - tmp.push(new DropdownValue(true,'TRUE')); - tmp.push(new DropdownValue(false,'FALSE')); - this.cmpRef.instance.values = tmp; - try { - if (typeof this.value === 'string') { - this.value = JSON.parse(this.value); + // Build drop down values + let tmp = []; + tmp.push(new DropdownValue(true,'TRUE')); + tmp.push(new DropdownValue(false,'FALSE')); + this.cmpRef.instance.values = tmp; + try { + if(typeof this.value === 'string'){ + this.value = JSON.parse(this.value); + } + } catch (err) { + this.value = null; } - } catch(err) { - this.value = null; - } - break; + break; - case DynamicElementComponentCreatorIdentifier.DEFAULT: - default: - this.createComponent(UiElementInputComponent); - console.log("ERROR: No ui-models component to handle type: " + this.type); - } + case DynamicElementComponentCreatorIdentifier.DEFAULT: + default: + this.createComponent(UiElementInputComponent); + console.log("ERROR: No ui-models component to handle type: " + this.type); + } + // } + // //There are consraints + // else { + + // this.createComponent(UiElementDropDownComponent); + + // // Build drop down values + // let items = []; + // this.constraints.forEach( (element) => { + // items.push(new DropdownValue(element,element)); + // }); + + // items.push(new DropdownValue(this.value,this.value, true, true)); + // this.cmpRef.instance.values = items; + + + + // } // Subscribe to change event of of ui-models-element-basic and fire event to change the value this.cmpRef.instance.baseEmitter.subscribe((event) => { this.emitter.emit(event); }); @@ -23,6 +23,12 @@ import {PopoverModule} from "../popover/popover.module"; import {TooltipModule} from "../tooltip/tooltip.module"; import {DynamicElementComponent} from "./dynamic-element.component"; import {FormElementsModule} from "../form-components/form-elements.module"; +// import {SdcUiComponentsModule} from "sdc-ui/lib/angular"; +// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; +// import {SdcUiComponentsModule} from "onap-ui/lib/angular"; +import { SdcUiComponentsModule } from "onap-ui-angular"; + +import { CommonModule } from '@angular/common'; @NgModule({ declarations: [ @@ -31,7 +37,9 @@ import {FormElementsModule} from "../form-components/form-elements.module"; imports: [ PopoverModule, TooltipModule, - FormElementsModule + FormElementsModule, + SdcUiComponentsModule, + CommonModule ], exports: [ DynamicElementComponent @@ -14,16 +14,15 @@ ~ limitations under the License. --> -<h1 class="w-sdc-designer-sidebar-section-title" - tooltip="{{titleTooltip}}" - [ngClass]="{'expanded': state == 0, 'collapsed': state == 1}" - (click)="toggleState()"> - {{caption}}<span class="w-sdc-designer-sidebar-section-title-icon"></span> - <ng-content select="header"></ng-content> - <span class="w-sdc-designer-sidebar-section-title-icon"></span> -</h1> +<div class="expand-collapse-container"> + <h1 class="expand-collapse-title" [ngClass]="{'expanded': state == 0, 'collapsed': state == 1}" + tooltip="{{titleTooltip}}" (click)="toggleState()">{{caption}} + <ng-content select="header"></ng-content> + <svg-icon name="caret1-down-o" mode="info" size="small" class="expand-collapse-title-icon"></svg-icon> + </h1> -<div class="expand-collapse-content" [ngClass]="{'visible': state === 0, 'hidden': state === 1}"> - <ng-content></ng-content> - <ng-content select="content"></ng-content> -</div> + <div class="expand-collapse-content" [ngClass]="{'expanded': state === 0, 'collapsed': state === 1}"> + <ng-content></ng-content> + <ng-content select="content"></ng-content> + </div> +</div>
\ No newline at end of file @@ -5,3 +5,51 @@ text-decoration: underline; text-align: left; } + + +.expand-collapse-title { + display:flex; + align-items: center; + cursor: pointer; + font-size: 14px; + text-transform: uppercase; + line-height: 32px; + padding: 0 10px 0 20px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + header { + flex: 1; + } + + .expand-collapse-title-icon { + padding-top: 10px; + padding-left: 2px; + transition: transform .3s ease-out; + + } + + &.expanded .expand-collapse-title-icon { + transform: rotate(0); + } + + &.collapsed .expand-collapse-title-icon { + transform: rotate(-180deg); + } + +} + +.expand-collapse-content { + overflow: hidden; + transition: max-height .3s ease-in; + + &.collapsed { + max-height: 0px; + overflow:hidden; + transition: all .3s cubic-bezier(0, 1, 0, 1); + + } + +} @@ -0,0 +1,6 @@ +<label> + <input type="file" class="i-sdc-dashboard-item-upload-input" + (change)="onFileSelect($event)" + [accept]="extensionsWithDot" + [attr.data-tests-id]="'file-' + testsId" /> +</label> @@ -0,0 +1,45 @@ +import {Component, Input, Output, EventEmitter, SimpleChanges} from "@angular/core"; + +@Component({ + selector: 'file-opener', + templateUrl: './file-opener.component.html', + styleUrls: ['./file-opener.component.less'] +}) +export class FileOpenerComponent { + @Input() public testsId: string; + @Input() public extensions: string; + @Output() public onFileUpload: EventEmitter<any>; + + public extensionsWithDot: string; + + constructor() { + this.onFileUpload = new EventEmitter<any>(); + } + + public ngOnChanges(changes:SimpleChanges) { + if (changes.extensions) { + this.extensionsWithDot = this.getExtensionsWithDot(changes.extensions.currentValue); + } + } + + public onFileSelect(event) { + const importFile:any = event.target.files[0]; + const reader = new FileReader(); + reader.readAsBinaryString(importFile); + reader.onload = () => { + this.onFileUpload.emit({ + filename: importFile.name, + filetype: importFile.type, + filesize: importFile.size, + base64: btoa(reader.result) + }); + }; + } + + public getExtensionsWithDot(extensions:string):string { + extensions = extensions || this.extensions || ''; + return extensions.split(',') + .map(ext => '.' + ext.toString()) + .join(','); + } +} @@ -19,7 +19,7 @@ */ import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core'; -//import { trigger, state, style, transition, animate, keyframes } from '@angular/core'; +//import { trigger, state, style, transition, animate, keyframes } from '@angular/animations'; @Component({ selector: 'checkbox', @@ -15,5 +15,5 @@ --> <select name='{{name}}' [(ngModel)]="value" (change)="onChange()" [ngClass]="{'disabled':readonly}" [attr.data-tests-id]="'value-' + testId"> - <option *ngFor="let ddvalue of values" [ngValue]="ddvalue.label != undefined ? ddvalue.value : ddvalue">{{ddvalue.label||ddvalue}}</option> + <option *ngFor="let ddvalue of values" [ngValue]="ddvalue.label != undefined ? ddvalue.value : ddvalue" [hidden]="ddvalue.hidden" [selected] = "ddvalue.selected">{{ddvalue.label||ddvalue}}</option> </select> @@ -24,11 +24,21 @@ import { UiElementBase, UiElementBaseInterface } from './../ui-element-base.comp export class DropdownValue { value:any; label:string; + hidden?: boolean; + selected?: boolean; - constructor(value:any,label:string) { + + constructor(value:any,label:string, hidden: boolean = false, selected: boolean = false) { this.value = value; this.label = label; + this.hidden = hidden; + this.selected = selected; + + // this.hidden = hidden ? true : "hidden": ""; + // this.selected = selected ? true : "selected": ""; } + + } @Component({ @@ -37,8 +47,8 @@ export class DropdownValue { styleUrls: ['./ui-element-dropdown.component.less'], }) export class UiElementDropDownComponent extends UiElementBase implements UiElementBaseInterface { - @Input() - values: DropdownValue[]|string[]; + + @Input() values: DropdownValue[]; constructor() { super(); @@ -1,19 +1,19 @@ /** * Created by rc2122 on 9/5/2017. */ -import {NgModule} from "@angular/core"; -import {BrowserModule} from "@angular/platform-browser"; -import {FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {UiElementPopoverInputComponent} from "./popover-input/ui-element-popover-input.component"; -import {UiElementIntegerInputComponent} from "./integer-input/ui-element-integer-input.component"; -import {UiElementInputComponent} from "./input/ui-element-input.component"; -import {UiElementDropDownComponent} from "./dropdown/ui-element-dropdown.component"; -import {UiElementBase} from "./ui-element-base.component"; -import {CheckboxModule} from "./checkbox/checkbox.module"; -import {RadioButtonComponent} from "./radio-buttons/radio-buttons.component"; -import {PopoverModule} from "../popover/popover.module"; -import {TooltipModule} from "../tooltip/tooltip.module"; - +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { SdcUiComponentsModule } from 'onap-ui-angular/dist'; +import { PopoverModule } from '../popover/popover.module'; +import { TooltipModule } from '../tooltip/tooltip.module'; +import { CheckboxModule } from './checkbox/checkbox.module'; +import { UiElementDropDownComponent } from './dropdown/ui-element-dropdown.component'; +import { UiElementInputComponent } from './input/ui-element-input.component'; +import { UiElementIntegerInputComponent } from './integer-input/ui-element-integer-input.component'; +import { UiElementPopoverInputComponent } from './popover-input/ui-element-popover-input.component'; +import { RadioButtonComponent } from './radio-buttons/radio-buttons.component'; +import { UiElementBase } from './ui-element-base.component'; @NgModule({ imports: [ @@ -22,7 +22,8 @@ import {TooltipModule} from "../tooltip/tooltip.module"; PopoverModule, ReactiveFormsModule, TooltipModule, - CheckboxModule], + CheckboxModule, + SdcUiComponentsModule], declarations: [UiElementDropDownComponent, UiElementInputComponent, @@ -39,4 +40,4 @@ import {TooltipModule} from "../tooltip/tooltip.module"; TooltipModule, CheckboxModule] }) -export class FormElementsModule { }
\ No newline at end of file +export class FormElementsModule { } @@ -13,19 +13,18 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - -<input - class="value-input" - [ngClass]="{'error': control.invalid, 'disabled':readonly}" - type="text" - [name]="name" - [(ngModel)]="value" - (input)="onChange()" - [attr.maxlength]="validation.propertyValue.max" - [attr.minlength]="validation.propertyValue.min" - [pattern]="pattern" - [formControl]="control" - tooltip="{{value}}" - [readonly]="readonly" - [attr.data-tests-id]="'value-' + testId" - /> +<div sdc-tooltip [tooltip-text]="value"> + <input + class="value-input" + [ngClass]="{'error': control.invalid, 'disabled':readonly}" + type="text" + [name]="name" + [(ngModel)]="value" + (input)="onChange()" + [attr.maxlength]="validation.propertyValue.max" + [attr.minlength]="validation.propertyValue.min" + [pattern]="pattern" + [formControl]="control" + [attr.data-tests-id]="'value-' + testId" + /> +</div>
\ No newline at end of file @@ -1,15 +1,11 @@ import { NgModule } from "@angular/core"; import { CommonModule } from '@angular/common'; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { ValueEditComponent } from './value-edit/value-edit.component'; +import { SdcUiComponentsModule } from "onap-ui-angular"; import { UnsavedChangesComponent } from "./unsaved-changes/unsaved-changes.component"; import { UiElementsModule } from "../ui-elements.module"; - - @NgModule({ declarations: [ - ValueEditComponent, UnsavedChangesComponent ], imports: [ @@ -17,7 +13,7 @@ import { UiElementsModule } from "../ui-elements.module"; SdcUiComponentsModule, UiElementsModule ], - exports: [ValueEditComponent, UnsavedChangesComponent], + exports: [UnsavedChangesComponent], entryComponents: [ UnsavedChangesComponent ], providers: [] @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<div *ngIf="isVisible" data-tests-id="tlv-loader" [ngClass]="relative ? 'loader-relative' : 'loader-fixed'" +<div *ngIf="isVisible" data-tests-id="loader" [ngClass]="relative ? 'loader-relative' : 'loader-fixed'" [style.top]="offset.top" [style.left]="offset.left" [style.width]="offset.width" [style.height]="offset.height"> <div class="tlv-loader-back" [ngClass]="{'tlv-loader-relative':relative}"></div> <div class="tlv-loader {{size}}"></div> @@ -79,6 +79,13 @@ export class LoaderComponent { width: (parentElement.offsetWidth !== undefined) ? parentElement.offsetWidth + "px" : undefined, height: (parentElement.offsetHeight !== undefined) ? parentElement.offsetHeight + "px" : undefined }; + } else { + this.offset = { + left: '0px', + top: '0px', + width: '100%', + height: '100%' + } } this.isVisible = true; } @@ -1,5 +1,9 @@ /*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ * Copyright (C) 2017 AT&T Intellectual Property. 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 @@ -11,9 +15,9 @@ * 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. + * ============LICENSE_END========================================================= */ - import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ @@ -1,5 +1,9 @@ /*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ * Copyright (C) 2017 AT&T Intellectual Property. 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 @@ -11,9 +15,9 @@ * 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. + * ============LICENSE_END========================================================= */ - import { Component, Input, ContentChildren, SimpleChanges, QueryList } from '@angular/core'; import { MenuItemComponent } from "./menu-item.component"; import { Point } from "app/models"; @@ -1,9 +1,9 @@ /** * Created by ob0695 on 11.04.2018. */ -import {Component, Input} from "@angular/core"; -import {UiBaseObject} from "../../../../../models/ui-models/ui-base-object"; -import {IDropDownOption} from "sdc-ui/lib/angular/form-elements/dropdown/dropdown-models"; +import { Component, Input } from "@angular/core"; +import { UiBaseObject } from "../../../../../models/ui-models/ui-base-object"; +import { IDropDownOption } from "onap-ui-angular/dist/form-elements/dropdown/dropdown-models"; @Component({ selector: 'add-elements', @@ -1,14 +1,11 @@ /** * Created by ob0695 on 11.04.2018. */ -import {NgModule} from "@angular/core"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; -import {AddElementsComponent} from "./add-elements.component"; -import {CommonModule} from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { AddElementsComponent } from "./add-elements.component"; +import { CommonModule } from "@angular/common"; -/** - * Created by ob0695 on 9.04.2018. - */ @NgModule({ declarations: [ AddElementsComponent @@ -36,7 +36,7 @@ export class ModalComponent implements OnInit, OnDestroy { @Input() isMovable: boolean; @Input() input: ModalModel; @Input() dynamicContent: any; - @ViewChild('dynamicContentContainer', { read: ViewContainerRef }) dynamicContentContainer: ViewContainerRef; //Allows for custom component as body instead of simple message. See ModalService.createActionModal for implementation details, and HttpService's catchError() for example. + @ViewChild('dynamicContentContainer', { read: ViewContainerRef }) dynamicContentContainer: ViewContainerRef; //Allows for custom component as body instead of simple message. See ModalService.createActionModal for implementation details, and HttpHelperService's catchError() for example. private modalElement: JQuery; constructor( el: ElementRef ) { @@ -20,10 +20,8 @@ * limitations under the License. * ============LICENSE_END========================================================= */ -import { - Component, ElementRef, forwardRef, Inject, Input, trigger, state, style, - transition, animate, ViewChild, ViewContainerRef, ComponentRef -} from "@angular/core"; +import { Component, ElementRef, forwardRef, Inject, Input, ViewChild, ViewContainerRef, ComponentRef} from "@angular/core"; +import {trigger, state, style, transition, animate} from '@angular/animations'; import {StepModel} from "app/models"; import {ModalService} from "../../../services/modal.service"; import {ModalComponent} from "../modal/modal.component"; @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; import {MultiStepsWizardComponent} from "./multi-steps-wizard.component"; import {CommonModule} from "@angular/common"; -import {ConnectionWizardModule} from "../../../pages/connection-wizard/connection-wizard.module"; +import {ConnectionWizardModule} from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module"; @NgModule({ declarations: [ @@ -0,0 +1,11 @@ +<div class="w-sdc-designer-sidebar-toggle" [class.active]="(withSidebar$ | async)" (click)="toggleSidebarDisplay()"> + <div class="w-sdc-designer-sidebar-toggle-icon sprite-new pointer menu-open-left"></div> +</div> + +<div class="w-sdc-designer-sidebar"> + <ng-content></ng-content> +</div> + + + + @@ -0,0 +1,55 @@ +@import '../../../../../assets/styles/variables'; + +.w-sdc-designer-sidebar { + background-color:@main_color_p ; + font-family: @font-opensans-regular; + font-size: 13px; + color: #191919; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + bottom: 0; + position: fixed; + right: -302px; + width: 302px; + top: 103px; + transition: right 0.2s; + z-index: 1010; + box-shadow: -7px -3px 6px -8px @main_color_n; +} + +.w-sdc-designer-sidebar-toggle { + background-color: @main_color_p; + border-left: 1px solid @main_color_o; + border-bottom: 1px solid @main_color_o; + height: 21px; + position: absolute; + right: 0; + top: 53px; + width: 17px; + transition: right 0.2s; + z-index: 1005; + box-shadow: -1px 1px 3px 0 @main_color_n; + cursor: pointer; + + &.active { + right: 302px; + .w-sdc-designer-sidebar-toggle-icon{ + transform: rotate(180deg); + } + } +} + +.w-sdc-designer-sidebar-toggle-icon { + margin-left: 6px; + margin-top: 6px; +} + +.w-sdc-designer-sidebar-toggle.active + .w-sdc-designer-sidebar { + right: 0; + display: flex; + flex-direction: column; +}
\ No newline at end of file @@ -0,0 +1,25 @@ +import {Component} from "@angular/core"; +import {Select, Store} from "@ngxs/store"; +import {Subscription} from "rxjs/Subscription"; +import {GraphState} from "../../../pages/composition/common/store/graph.state"; +import {OnSidebarOpenOrCloseAction} from "../../../pages/composition/common/store/graph.actions"; + +@Component({ + selector: 'panel-wrapper-component', + templateUrl: './panel-wrapper.component.html', + styleUrls: ['./panel-wrapper.component.less'] +}) +export class PanelWrapperComponent { + @Select(GraphState.withSidebar) withSidebar$: boolean; + + tabs: Array<any>; + subscription: Subscription; + + constructor(public store: Store) { + } + + private toggleSidebarDisplay = () => { + // this.withSidebar = !this.withSidebar; + this.store.dispatch(new OnSidebarOpenOrCloseAction()); + } +} @@ -0,0 +1,51 @@ +import {Directive, Input, ElementRef} from '@angular/core'; +import * as PerfectScrollbar from 'perfect-scrollbar'; + +interface IPerfectScrollbarOptions { + wheelSpeed?: number; + wheelPropagation?: boolean; + minScrollbarLength?: number; + useBothWheelAxes?: boolean; + useKeyboard?: boolean; + suppressScrollX?: boolean; + suppressScrollY?: boolean; + scrollXMarginOffset?: number; + scrollYMarginOffset?: number; + includePadding?: boolean; +} + +@Directive({ + selector: '[perfectScrollbar]' +}) +export class PerfectScrollbarDirective { + @Input() public perfectScrollbarOptions: IPerfectScrollbarOptions; + + private psOptions: IPerfectScrollbarOptions; + private updatingPS: boolean; + + constructor(public elemRef:ElementRef) { + console.log('PSbar: Constructor'); + this.psOptions = Object.assign({}, this.perfectScrollbarOptions); + this.updatingPS = false; + } + + public ngOnInit() { + console.log('PSbar: Initializing'); + PerfectScrollbar.initialize(this.elemRef.nativeElement, this.psOptions); + } + + public ngAfterContentChecked() { + // update perfect-scrollbar after content is checked (updated) - bounced + if (!this.updatingPS) { + this.updatingPS = true; + setTimeout(() => { + this.updatingPS = false; + PerfectScrollbar.update(this.elemRef.nativeElement); + }, 100); + } + } + + public ngOnDestroy() { + PerfectScrollbar.destroy(this.elemRef.nativeElement); + } +} @@ -17,6 +17,7 @@ <div class="plugin-frame"> <div class="w-sdc-main-container"> <iframe *ngIf="plugin.isOnline" class="plugin-iframe" [src]="pluginUrl | safeUrlSanitizer"></iframe> - <plugin-not-connected [pluginName]="plugin.pluginId" *ngIf="!plugin.isOnline && isPluginCheckDone"></plugin-not-connected> + <plugin-not-connected [pluginName]="plugin.pluginId" + *ngIf="!plugin.isOnline && isPluginCheckDone"></plugin-not-connected> </div> </div> @@ -38,8 +38,7 @@ export class PluginFrameComponent implements OnInit { } else { this.onLoadingDone.emit(); } - }) - + }); } private initPlugin() { @@ -19,7 +19,7 @@ [style.left]="left + 'px'" [class.in]="isIn" [class.fade]="animation" - style="display: block" + [style.display] = "displayType" role="popover" [ngClass]="{'hide-arrow':hideArrow}"> <div [hidden]="!closeOnMouseOutside" class="virtual-area"></div> @@ -29,13 +29,11 @@ <span class="close-button" (click)="popover.hide()"></span> </div> <ng-content></ng-content> - <div class="popover-footer"> + <div class="popover-footer" *ngIf="buttons"> <button *ngFor="let buttonName of buttonsNames" - class="tlv-btn {{buttons[buttonName].cssClass}}" + class="tlv-btn {{buttons[buttonName].cssClass}}" [attr.data-tests-id]="'filter-' + buttons[buttonName].text.toLowerCase() + '-button'" [disabled] = "buttons[buttonName].getDisabled && buttons[buttonName].getDisabled()" (click) = "buttons[buttonName].callback()">{{buttons[buttonName].text}}</button> </div> </div> - - @@ -7,9 +7,9 @@ * 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. @@ -18,25 +18,25 @@ * ============LICENSE_END========================================================= */ -import {Component, Input, Output, AfterViewInit, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, EventEmitter, Renderer } from "@angular/core"; -import {ButtonsModelMap} from "app/models"; -import {PopoverComponent} from "./popover.component"; +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, Renderer, ViewChild } from '@angular/core'; +import { ButtonsModelMap } from 'app/models'; +import { PopoverComponent } from './popover.component'; @Component({ - selector: "popover-content", - templateUrl:'./popover-content.component.html', - styleUrls:['popover-content.component.less'] + selector: 'popover-content', + templateUrl: './popover-content.component.html', + styleUrls: ['popover-content.component.less'] }) export class PopoverContentComponent implements AfterViewInit, OnDestroy { @Input() public title: string; - @Input() public buttons:ButtonsModelMap; + @Input() public buttons: ButtonsModelMap; @Input() content: string; @Input() - placement: "top"|"bottom"|"left"|"right"|"auto"|"auto top"|"auto bottom"|"auto left"|"auto right" = "bottom"; + placement: 'top'|'bottom'|'left'|'right'|'auto'|'auto top'|'auto bottom'|'auto left'|'auto right' = 'bottom'; @Input() animation: boolean = true; @@ -50,52 +50,56 @@ export class PopoverContentComponent implements AfterViewInit, OnDestroy { @Input() hideArrow: boolean = false; - @ViewChild("popoverDiv") + @ViewChild('popoverDiv') popoverDiv: ElementRef; - buttonsNames:Array<string>; + buttonsNames: string[]; popover: PopoverComponent; onCloseFromOutside = new EventEmitter(); - top: number = -10000; - left: number = -10000; + top: number = 250; + left: number = 300; isIn: boolean = false; - displayType: string = "none"; + displayType: string = 'none'; effectivePlacement: string; - onDocumentMouseDown = (event: any) => { - const element = this.element.nativeElement; - if (!element || !this.popover) return; - if (element.contains(event.target) || this.popover.getElement().contains(event.target)) return; - this.hide(); - this.onCloseFromOutside.emit(undefined); - }; - + listenClickFunc: any; + listenMouseFunc: any; constructor(protected element: ElementRef, protected cdr: ChangeDetectorRef, protected renderer: Renderer) { } - listenClickFunc: any; - listenMouseFunc: any; + onDocumentMouseDown = (event: any) => { + const element = this.element.nativeElement; + if (!element || !this.popover) { return; } + if (element.contains(event.target) || this.popover.getElement().contains(event.target)) { return; } + this.hide(); + this.onCloseFromOutside.emit(undefined); + } ngAfterViewInit(): void { - this.buttonsNames = Object.keys(this.buttons); - if (this.closeOnClickOutside) - this.listenClickFunc = this.renderer.listenGlobal("document", "mousedown", (event: any) => this.onDocumentMouseDown(event)); - if (this.closeOnMouseOutside) - this.listenMouseFunc = this.renderer.listenGlobal("document", "mouseover", (event: any) => this.onDocumentMouseDown(event)); + if ( this.buttons ) { + this.buttonsNames = Object.keys(this.buttons); + } + if (this.closeOnClickOutside) { + this.listenClickFunc = this.renderer.listenGlobal('document', 'mousedown', (event: any) => this.onDocumentMouseDown(event)); + } + if (this.closeOnMouseOutside) { + this.listenMouseFunc = this.renderer.listenGlobal('document', 'mouseover', (event: any) => this.onDocumentMouseDown(event)); + } - this.show(); this.cdr.detectChanges(); } ngOnDestroy() { - if (this.closeOnClickOutside) + if (this.closeOnClickOutside) { this.listenClickFunc(); - if (this.closeOnMouseOutside) + } + if (this.closeOnMouseOutside) { this.listenMouseFunc(); + } } // ------------------------------------------------------------------------- @@ -103,11 +107,12 @@ export class PopoverContentComponent implements AfterViewInit, OnDestroy { // ------------------------------------------------------------------------- show(): void { - if (!this.popover || !this.popover.getElement()) + if (!this.popover || !this.popover.getElement()) { return; + } const p = this.positionElements(this.popover.getElement(), this.popoverDiv.nativeElement, this.placement); - this.displayType = "block"; + this.displayType = 'block'; this.top = p.top; this.left = p.left; this.isIn = true; @@ -121,8 +126,7 @@ export class PopoverContentComponent implements AfterViewInit, OnDestroy { } hideFromPopover() { - this.top = -10000; - this.left = -10000; + this.displayType = 'none'; this.isIn = true; } @@ -131,56 +135,56 @@ export class PopoverContentComponent implements AfterViewInit, OnDestroy { // ------------------------------------------------------------------------- protected positionElements(hostEl: HTMLElement, targetEl: HTMLElement, positionStr: string, appendToBody: boolean = false): { top: number, left: number } { - let positionStrParts = positionStr.split("-"); + const positionStrParts = positionStr.split('-'); let pos0 = positionStrParts[0]; - let pos1 = positionStrParts[1] || "center"; - let hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); - let targetElWidth = targetEl.offsetWidth; - let targetElHeight = targetEl.offsetHeight; + const pos1 = positionStrParts[1] || 'center'; + const hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + const targetElWidth = targetEl.offsetWidth; + const targetElHeight = targetEl.offsetHeight; this.effectivePlacement = pos0 = this.getEffectivePlacement(pos0, hostEl, targetEl); - let shiftWidth: any = { - center: function (): number { + const shiftWidth: any = { + center(): number { return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; }, - left: function (): number { + left(): number { return hostElPos.left; }, - right: function (): number { + right(): number { return hostElPos.left + hostElPos.width - targetElWidth; } }; - let shiftHeight: any = { - center: function (): number { + const shiftHeight: any = { + center(): number { return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; }, - top: function (): number { + top(): number { return hostElPos.top; }, - bottom: function (): number { + bottom(): number { return hostElPos.top + hostElPos.height - targetElHeight; } }; let targetElPos: { top: number, left: number }; switch (pos0) { - case "right": + case 'right': targetElPos = { top: shiftHeight[pos1](), left: hostElPos.left + hostElPos.width }; break; - case "left": + case 'left': targetElPos = { top: shiftHeight[pos1](), left: hostElPos.left - targetElWidth }; break; - case "bottom": + case 'bottom': targetElPos = { top: hostElPos.top + hostElPos.height, left: shiftWidth[pos1]() @@ -228,18 +232,20 @@ export class PopoverContentComponent implements AfterViewInit, OnDestroy { } protected getStyle(nativeEl: HTMLElement, cssProp: string): string { - if ((nativeEl as any).currentStyle) // IE + if ((nativeEl as any).currentStyle) { // IE return (nativeEl as any).currentStyle[cssProp]; + } - if (window.getComputedStyle) + if (window.getComputedStyle) { return (window.getComputedStyle as any)(nativeEl)[cssProp]; + } // finally try and get inline style return (nativeEl.style as any)[cssProp]; } protected isStaticPositioned(nativeEl: HTMLElement): boolean { - return (this.getStyle(nativeEl, "position") || "static" ) === "static"; + return (this.getStyle(nativeEl, 'position') || 'static' ) === 'static'; } protected parentOffsetEl(nativeEl: HTMLElement): any { @@ -251,26 +257,26 @@ export class PopoverContentComponent implements AfterViewInit, OnDestroy { } protected getEffectivePlacement(placement: string, hostElement: HTMLElement, targetElement: HTMLElement): string { - const placementParts = placement.split(" "); - if (placementParts[0] !== "auto") { + const placementParts = placement.split(' '); + if (placementParts[0] !== 'auto') { return placement; } const hostElBoundingRect = hostElement.getBoundingClientRect(); - const desiredPlacement = placementParts[1] || "bottom"; + const desiredPlacement = placementParts[1] || 'bottom'; - if (desiredPlacement === "top" && hostElBoundingRect.top - targetElement.offsetHeight < 0) { - return "bottom"; + if (desiredPlacement === 'top' && hostElBoundingRect.top - targetElement.offsetHeight < 0) { + return 'bottom'; } - if (desiredPlacement === "bottom" && hostElBoundingRect.bottom + targetElement.offsetHeight > window.innerHeight) { - return "top"; + if (desiredPlacement === 'bottom' && hostElBoundingRect.bottom + targetElement.offsetHeight > window.innerHeight) { + return 'top'; } - if (desiredPlacement === "left" && hostElBoundingRect.left - targetElement.offsetWidth < 0) { - return "right"; + if (desiredPlacement === 'left' && hostElBoundingRect.left - targetElement.offsetWidth < 0) { + return 'right'; } - if (desiredPlacement === "right" && hostElBoundingRect.right + targetElement.offsetWidth > window.innerWidth) { - return "left"; + if (desiredPlacement === 'right' && hostElBoundingRect.right + targetElement.offsetWidth > window.innerWidth) { + return 'left'; } return desiredPlacement; @@ -0,0 +1,12 @@ +<div class="sdc-element-icon"> + <svg-icon [ngClass]="{'service-proxy': elementType === 'ServiceProxy'}" + [type]="elementIcon.type" + [name]="elementIcon.iconName" + [mode]="elementIcon.color" + [size]="elementIcon.size" + [backgroundShape]="elementIcon.shape" + [backgroundColor]="elementIcon.backgroundColor"></svg-icon> + <span *ngIf="uncertified" class="uncertified-icon-wapper"> + <svg-icon class="element-non-certified" [name]="'alert-circle'" [mode]="'error'"></svg-icon> + </span> +</div> @@ -0,0 +1,25 @@ +@import "./../../../../../assets/styles/override"; +.service-proxy { + /deep/.svg-icon.bg-type-circle { + border: 2px solid @sdcui_color_blue; + } +} + +.element-non-certified { + position: absolute; + top: 0.4px; + left: 0.4px; +} +.sdc-element-icon { + position: relative; +} + +.uncertified-icon-wapper{ + height: 17px; + width: 17px; + background-color: white; + border-radius: 50%; + position: absolute; + top: -2px; + left: -1px; +}
\ No newline at end of file @@ -0,0 +1,69 @@ +import {Component, Input, OnInit} from "@angular/core"; +import {ComponentType, SdcElementType, ResourceType} from "../../../../utils/constants"; + + +export class ElementIcon { + iconName: string; + color: string; + backgroundColor: string; + type: string + shape: string; + size: string; + + constructor(name?: string, type?:string, backgroundColor?:string, color?:string, shape?: string, size?:string) { + this.iconName = name || 'default'; + this.type = type || 'resource_24'; + this.backgroundColor = backgroundColor || 'primary'; + this.color = color || "white"; + this.shape = shape || "circle"; + this.size = size || "x_large"; + } +} + +@Component({ + selector: 'sdc-element-icon', + templateUrl: './sdc-element-icon.component.html', + styleUrls: ['./sdc-element-icon.component.less'] +}) +export class SdcElementIconComponent { + + @Input() iconName: string; + @Input() elementType: string; + @Input() uncertified: boolean = false; + + public elementIcon; + + private createIconForDisplay = () => { + switch (this.elementType) { + + case ComponentType.SERVICE: + this.elementIcon = new ElementIcon(this.iconName, "services_24", "lightBlue"); + break; + case ComponentType.SERVICE_PROXY: + this.elementIcon = new ElementIcon(this.iconName, "services_24", "white", "primary"); + break; + case ResourceType.CONFIGURATION: + this.elementIcon = new ElementIcon(this.iconName, "resources_24", "purple", "white", 'circle', "medium"); + break; + case SdcElementType.GROUP: + this.elementIcon = new ElementIcon("group", "resources_24", "blue", 'white', 'rectangle'); + break; + case SdcElementType.POLICY: + this.elementIcon = new ElementIcon("policy", "resources_24", "darkBlue2", 'white', 'rectangle'); + break; + case ResourceType.CP: + case ResourceType.VL: + this.elementIcon = new ElementIcon(this.iconName, "resources_24", "purple", '', '', 'medium'); + break; + default: + this.elementIcon = new ElementIcon(this.iconName, "resources_24", "purple"); + } + } + + ngOnChanges():void { + this.createIconForDisplay(); + } +} + + + @@ -1,21 +1,5 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<div class="search-bar-container {{class}}"> - <input class="search-bar-input" type="text" [placeholder]="placeholder" [(ngModel)]="searchQuery" (ngModelChange)="searchQueryChange($event)"/> - <span class="clear-search-x" *ngIf="searchQuery" (click)="clearSearchQuery()">x</span> - <button class="search-bar-button" (click)="searchButtonClick()"></button> +<div class="sdc-search-bar-container {{class}}"> + <input class="sdc-search-bar-input" type="text" [placeholder]="placeholder" [(ngModel)]="searchQuery" (ngModelChange)="searchQueryChange($event)"/> + <span class="sdc-clear-search-x" *ngIf="searchQuery" (click)="clearSearchQuery()">x</span> + <button class="sdc-search-bar-button" (click)="searchButtonClick()"></button> </div>
\ No newline at end of file @@ -1,9 +1,9 @@ -.search-bar-container { +.sdc-search-bar-container { display:flex; border-radius: 4px; box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29); - .search-bar-input { + .sdc-search-bar-input { border: 1px solid #cdcdcd; border-radius: 4px; border-right:none; @@ -14,9 +14,11 @@ color: #5a5a5a; font-size: 1em; font-style: italic; + + } - .clear-search-x { + .sdc-clear-search-x { position:absolute; right:40px; top:5px; @@ -29,7 +31,7 @@ } } - .search-bar-button { + .sdc-search-bar-button { background: url('../../../../../assets/styles/images/sprites/sprite-global.png') no-repeat -206px -1275px; background-color: rgba(234, 234, 234, 0.88); width: 30px; @@ -14,9 +14,9 @@ ~ limitations under the License. --> -<div class="search-with-autocomplete-container {{searchBarClass}}" [class.autocomplete-visible]="autoCompleteValues && autoCompleteValues.length" [class.active]="searchQuery && searchQuery.length"> +<div class="sdc-search-with-autocomplete-container {{searchBarClass}}" [class.sdc-autocomplete-visible]="autoCompleteValues && autoCompleteValues.length" [class.active]="searchQuery && searchQuery.length"> <search-bar [placeholder]="searchPlaceholder" [searchQuery]="searchQuery" (searchButtonClicked)="updateSearch($event)" (searchChanged)="searchChange($event)"></search-bar> - <div class="autocomplete-results"> - <div *ngFor="let item of autoCompleteValues" class="autocomplete-result-item" (click)="updateSearch(item)">{{item}}</div> + <div class="sdc-autocomplete-results"> + <div *ngFor="let item of autoCompleteValues" class="sdc-autocomplete-result-item" (click)="updateSearch(item)">{{item}}</div> </div> </div>
\ No newline at end of file @@ -1,13 +1,13 @@ -.search-with-autocomplete-container{ - &.autocomplete-visible { +.sdc-search-with-autocomplete-container{ + &.sdc-autocomplete-visible { - .search-bar-input { + .sdc-search-bar-input { border-bottom-left-radius: 0; } - .search-bar-button { + .sdc-search-bar-button { border-bottom-right-radius: 0; } - .autocomplete-results { + .sdc-autocomplete-results { border: solid 1px #d2d2d2; border-top:none; border-bottom-left-radius: 4px; @@ -20,7 +20,7 @@ overflow-y: scroll; } - .autocomplete-result-item { + .sdc-autocomplete-result-item { color:#5a5a5a; padding: 5px 0; cursor:pointer; @@ -21,7 +21,7 @@ import { Component, ContentChildren, QueryList, AfterContentInit, Input, Output, EventEmitter } from '@angular/core'; import { Tab } from './tab/tab.component'; import { ViewEncapsulation } from '@angular/core'; -import { trigger, state, style, transition, animate, keyframes } from '@angular/core'; +import { trigger, state, style, transition, animate, keyframes } from '@angular/animations'; export {Tab}; @@ -3,13 +3,13 @@ import { BrowserModule } from '@angular/platform-browser'; import { TileComponent } from './tile.component'; import { GlobalPipesModule } from "../../../pipes/global-pipes.module"; import { TooltipModule } from "../tooltip/tooltip.module"; -import {MultilineEllipsisModule} from "../../../shared/multiline-ellipsis/multiline-ellipsis.module"; +import {SdcUiComponentsModule} from "onap-ui-angular"; @NgModule({ - imports: [BrowserModule, GlobalPipesModule, TooltipModule, MultilineEllipsisModule], + imports: [BrowserModule, GlobalPipesModule, SdcUiComponentsModule, TooltipModule], declarations: [TileComponent], exports: [TileComponent], entryComponents: [TileComponent] }) -export class TileModule { } +export class SdcTileModule { } @@ -23,15 +23,21 @@ <div class='sdc-tile-content' data-tests-id="dashboard-Elements" (click)="tileClicked()"> <div class='sdc-tile-content-icon'> - <div [ngClass]="[component.iconSprite, component.icon]" [ngClass]="{'sprite-resource-icons': component.isResource(), 'sprite-services-icons': component.isService()}" + <div [ngClass]="[component.iconSprite, component.icon]" + [ngClass]="{'sprite-resource-icons': component.isResource(), 'sprite-services-icons': component.isService()}" [attr.data-tests-id]="component.name"></div> </div> <div class='sdc-tile-content-info'> - <div class="sdc-tile-info-line title" [attr.data-tests-id]="component.name | resourceName" [tooltip]="component.name | resourceName" [tooltipDisabled]="!hasEllipsis"> - <multiline-ellipsis className="w-sdc-tile-multiline-ellipsis" [lines]="3" (hasEllipsisChanged)="hasEllipsis = $event">{{component.name | resourceName}}</multiline-ellipsis> + <div class="sdc-tile-info-line title" [attr.data-tests-id]="component.name | resourceName" + [tooltip]="component.name | resourceName" [tooltipDisabled]="!hasEllipsis"> + <multiline-ellipsis className="w-sdc-tile-multiline-ellipsis" [lines]="3" + (hasEllipsisChanged)="hasEllipsis = $event">{{component.name | resourceName}} + </multiline-ellipsis> + </div> + <div class="sdc-tile-info-line subtitle" [attr.data-tests-id]="component.name+'Version'">V + {{component.version}} </div> - <div class="sdc-tile-info-line subtitle" [attr.data-tests-id]="component.name+'Version'">V {{component.version}}</div> </div> </div> @@ -42,3 +48,32 @@ </div> </div> + +<!--<sdc-tile>--> +<!--<sdc-tile-header>--> +<!--<div [ngClass]="{'blue': component.isService(), 'purple': component.isResource()}">{{component.isResource() ?--> +<!--component.getComponentSubType(): 'S' }}--> +<!--</div>--> +<!--</sdc-tile-header>--> +<!--<sdc-tile-content>--> +<!--<div class='sdc-tile-content-icon blue'>--> +<!--<svg-icon--> +<!--[type]="catalogIcon.type"--> +<!--[name]="catalogIcon.iconName"--> +<!--[size]="catalogIcon.size"--> +<!--[mode]="catalogIcon.color"--> +<!--[backgroundShape]="catalogIcon.shape"--> +<!--[backgroundColor]="catalogIcon.backgroundColor"--> +<!--[disabled]="false"--> +<!-->--> +<!--</svg-icon>--> +<!--</div>--> +<!--<div class="sdc-tile-content-info">--> +<!--<span class="sdc-tile-info-line title">{{component.name | resourceName}}</span>--> +<!--<div class="sdc-tile-info-line subtitle">{{'V'+ component.version}}</div>--> +<!--</div>--> +<!--</sdc-tile-content>--> +<!--<sdc-tile-footer>--> +<!--<span class="sdc-tile-footer-cell">{{component.getStatus(sdcMenu)}}</span>--> +<!--</sdc-tile-footer>--> +<!--</sdc-tile>-->
\ No newline at end of file @@ -0,0 +1,15 @@ + + +//This was done as a fix for the wrong dispaly that was in every ui-tile. +//All other styles located in mail.less + +.sdc-tile { + display: block; + + .sdc-tile-content { + overflow: visible; + height: 80%; + .sdc-tile-content-icon { + } + } +} @@ -1,6 +1,8 @@ import {Component, Input, Output, Inject, EventEmitter} from '@angular/core'; import {Component as ComponentModel} from 'app/models'; import {SdcMenuToken, IAppMenu} from "../../../config/sdc-menu.config"; +import {ElementIcon} from "../sdc-element-icon/sdc-element-icon.component"; +import {ComponentType, ResourceType, SdcElementType} from "../../../../utils/constants"; @Component({ selector: 'ui-tile', @@ -10,14 +12,42 @@ import {SdcMenuToken, IAppMenu} from "../../../config/sdc-menu.config"; export class TileComponent { @Input() public component: ComponentModel; @Output() public onTileClick: EventEmitter<ComponentModel>; - + public catalogIcon: ElementIcon; public hasEllipsis: boolean; - constructor(@Inject(SdcMenuToken) public sdcMenu:IAppMenu) { + constructor(@Inject(SdcMenuToken) public sdcMenu: IAppMenu) { this.onTileClick = new EventEmitter<ComponentModel>(); this.hasEllipsis = false; } + ngOnInit(): void { + switch (this.component.componentType) { + + case ComponentType.SERVICE: + if (this.component.icon === 'defaulticon') { + this.catalogIcon = new ElementIcon(this.component.icon, "services_60", 'lightBlue', 'white'); + } else { + this.catalogIcon = new ElementIcon(this.component.icon, "services_60", '', 'lightBlue'); + } + break; + case ComponentType.RESOURCE: + switch (this.component.getComponentSubType()) { + case ResourceType.CP: + case ResourceType.VL: + this.catalogIcon = new ElementIcon(this.component.icon, "resources_24", "purple", "white", "circle", 'medium'); + break; + default: + if (this.component.icon === 'defaulticon') { + this.catalogIcon = new ElementIcon(this.component.icon, "resources_60", "purple", "white", "circle", 'x_large'); + } else { + this.catalogIcon = new ElementIcon(this.component.icon, "resources_60", '', "error"); + } + + } + + } + } + public tileClicked() { this.onTileClick.emit(this.component); } @@ -18,82 +18,89 @@ * ============LICENSE_END========================================================= */ -import { NgModule } from '@angular/core'; -import { NavbarModule } from "./navbar/navbar.module"; -import { DynamicElementModule } from "./dynamic-element/dynamic-element.module"; -import { FormElementsModule } from "./form-components/form-elements.module"; -import { LoaderComponent } from "./loader/loader.component"; -import { ModalModule } from "./modal/modal.module"; -import { PopoverModule } from "./popover/popover.module"; -import { SearchBarComponent } from "./search-bar/search-bar.component"; -import { SearchWithAutoCompleteComponent } from "./search-with-autocomplete/search-with-autocomplete.component"; -import { PalettePopupPanelComponent } from "./palette-popup-panel/palette-popup-panel.component"; -import { ZoneContainerComponent } from "./canvas-zone/zone-container.component"; -import { ZoneInstanceComponent } from "./canvas-zone/zone-instance/zone-instance.component"; -import { PaletteAnimationComponent } from "./palette-animation/palette-animation.component" -import { TabModule } from "./tabs/tabs.module"; -import { TooltipModule } from "./tooltip/tooltip.module"; -import { CommonModule } from "@angular/common"; -import { FormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { MultiStepsWizardModule } from "./multi-steps-wizard/multi-steps-wizard.module"; -import { MenuListModule } from "./menu/menu-list.module"; -import { MenuListNg2Module } from "../downgrade-wrappers/menu-list-ng2/menu-list-ng2.module"; -import { ExpandCollapseComponent } from './expand-collapse/expand-collapse.component'; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { TileModule } from "./tile/tile.module"; +import {NgModule} from '@angular/core'; +import {NavbarModule} from "./navbar/navbar.module"; +import {DynamicElementModule} from "./dynamic-element/dynamic-element.module"; +import {FormElementsModule} from "./form-components/form-elements.module"; +import {LoaderComponent} from "./loader/loader.component"; +import {ModalModule} from "./modal/modal.module"; +import {PopoverModule} from "./popover/popover.module"; +import {SearchBarComponent} from "./search-bar/search-bar.component"; +import {SearchWithAutoCompleteComponent} from "./search-with-autocomplete/search-with-autocomplete.component"; +import { PaletteAnimationComponent } from "app/ng2/pages/composition/palette/palette-animation/palette-animation.component"; +import {TabModule} from "./tabs/tabs.module"; +import {TooltipModule} from "./tooltip/tooltip.module"; +import {CommonModule} from "@angular/common"; +import {FormsModule} from "@angular/forms"; +import {BrowserModule} from "@angular/platform-browser"; +import {MultiStepsWizardModule} from "./multi-steps-wizard/multi-steps-wizard.module"; +import {MenuListModule} from "./menu/menu-list.module"; +import {MenuListNg2Module} from "../downgrade-wrappers/menu-list-ng2/menu-list-ng2.module"; +import {ExpandCollapseComponent} from './expand-collapse/expand-collapse.component'; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {SdcTileModule} from "./tile/sdc-tile.module"; +import {PerfectScrollbarDirective} from "./perfect-scroll-bar/perfect-scrollbar.directive"; +import {FileOpenerComponent} from "./file-opener/file-opener.component"; +import {DownloadArtifactComponent} from "app/ng2/components/ui/download-artifact/download-artifact.component"; +import {SdcElementIconComponent} from "./sdc-element-icon/sdc-element-icon.component"; +import {PanelWrapperComponent} from "./panel-wrapper/panel-wrapper.component"; @NgModule({ - declarations: [ - LoaderComponent, - SearchBarComponent, - SearchWithAutoCompleteComponent, - PalettePopupPanelComponent, - ZoneContainerComponent, - ZoneInstanceComponent, - PaletteAnimationComponent, - ExpandCollapseComponent -], - - imports: [ - SdcUiComponentsModule, - BrowserModule, - FormsModule, - CommonModule, - DynamicElementModule, - NavbarModule, - FormElementsModule, - ModalModule, - PopoverModule, - TabModule, - TooltipModule, - MultiStepsWizardModule, - MenuListModule, - MenuListNg2Module, - TileModule - ], - exports: [ - LoaderComponent, - MultiStepsWizardModule, - SearchBarComponent, - SearchWithAutoCompleteComponent, - PalettePopupPanelComponent, - ZoneContainerComponent, - ZoneInstanceComponent, - DynamicElementModule, - NavbarModule, - FormElementsModule, - ModalModule, - PopoverModule, - TabModule, - TooltipModule, - MenuListModule, - MenuListNg2Module, - PaletteAnimationComponent, - ExpandCollapseComponent, - TileModule - ], - entryComponents: [SearchWithAutoCompleteComponent, PalettePopupPanelComponent, ZoneContainerComponent, ZoneInstanceComponent, PaletteAnimationComponent] + declarations: [ + LoaderComponent, + SearchBarComponent, + SearchWithAutoCompleteComponent, + PaletteAnimationComponent, + ExpandCollapseComponent, + PerfectScrollbarDirective, + FileOpenerComponent, + SdcElementIconComponent, + DownloadArtifactComponent, + PanelWrapperComponent + ], + + imports: [ + SdcUiComponentsModule, + BrowserModule, + FormsModule, + CommonModule, + DynamicElementModule, + NavbarModule, + FormElementsModule, + ModalModule, + PopoverModule, + TabModule, + TooltipModule, + MultiStepsWizardModule, + MenuListModule, + MenuListNg2Module, + SdcTileModule + ], + exports: [ + LoaderComponent, + MultiStepsWizardModule, + SearchBarComponent, + SearchWithAutoCompleteComponent, + DynamicElementModule, + NavbarModule, + FormElementsModule, + ModalModule, + PopoverModule, + TabModule, + TooltipModule, + MenuListModule, + MenuListNg2Module, + PaletteAnimationComponent, + ExpandCollapseComponent, + SdcTileModule, + PerfectScrollbarDirective, + SdcElementIconComponent, + FileOpenerComponent, + DownloadArtifactComponent, + PanelWrapperComponent + ], + entryComponents: [SearchWithAutoCompleteComponent, SdcElementIconComponent, PaletteAnimationComponent] }) -export class UiElementsModule {} +export class UiElementsModule { +} @@ -13,13 +13,12 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. -- */ - - import {Provider, OpaqueToken} from "@angular/core"; +import {Provider, InjectionToken} from "@angular/core"; import {getSdcConfig, ISdcConfig} from "./sdc-config.config.factory"; export { ISdcConfig }; -export const SdcConfigToken = new OpaqueToken('SdcConfigToken'); +export const SdcConfigToken = new InjectionToken('SdcConfigToken'); export const SdcConfig:Provider = { provide: SdcConfigToken, @@ -15,13 +15,13 @@ -- */ -import {Provider, OpaqueToken} from "@angular/core"; +import {Provider, InjectionToken} from "@angular/core"; import {getSdcMenu} from "./sdc-menu.config.factory"; import {IAppMenu} from "app/models"; export { IAppMenu }; -export const SdcMenuToken = new OpaqueToken('SdcMenuToken'); +export const SdcMenuToken = new InjectionToken('SdcMenuToken'); export const SdcMenu:Provider = { provide: SdcMenuToken, @@ -0,0 +1,61 @@ +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable, Injector } from '@angular/core'; +import { SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ButtonType } from 'onap-ui-angular/dist/common'; +import { Observable } from 'rxjs/Observable'; +import { ServerErrorResponse } from '../../models/server-error-response'; +import { Cookie2Service } from '../services/cookie.service'; +import { HttpHelperService } from '../services/http-hepler.service'; +import { TranslateService } from '../shared/translator/translate.service'; + +@Injectable() +export class HeadersInterceptor implements HttpInterceptor { + + constructor(private injector: Injector, private cookieService: Cookie2Service, private httpHelperService: HttpHelperService) {} + + intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + let authReq = req.clone({ headers: req.headers.set(this.cookieService.getUserIdSuffix(), this.cookieService.getUserId()) + .set('Content-Type', 'application/json; charset=UTF-8') + .set(this.cookieService.getUserIdSuffix(), this.cookieService.getUserId()) + .set(this.cookieService.getUserIdSuffix(), this.cookieService.getUserId()) + }); + + const uuidValue = this.httpHelperService.getUuidValue(authReq.url); + if (uuidValue !== '') { + authReq = authReq.clone({ headers: authReq.headers.set(this.cookieService.getUserIdSuffix(), this.cookieService.getUserId())}); + } + return next.handle(authReq).do( + + (event: HttpEvent<any>) => { /* Do Nothing */ }, + + (err: any) => { + if (err instanceof HttpErrorResponse) { + const errorResponse: ServerErrorResponse = new ServerErrorResponse(err); + const modalService = this.injector.get(SdcUiServices.ModalService); + const translateService = this.injector.get(TranslateService); + + const errorDetails = { + 'Error Code': errorResponse.messageId, + 'Status Code': errorResponse.status + }; + + if (errorResponse.ecompRequestId) { + errorDetails['Transaction ID'] = errorResponse.ecompRequestId; + } + + if (errorResponse.messageId === 'POL5005') { + // Session and Role expiration special handling + modalService.openWarningModal( + 'Warning', + translateService.translate('ERROR_MODAL_TEXT', errorResponse), + 'warn-modal', + [ ] ); + } else { + modalService.openErrorDetailModal('Error', errorResponse.message, 'error-modal', errorDetails); + } + + return Observable.throwError(err); + } + }); + } +} @@ -0,0 +1,7 @@ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { HeadersInterceptor } from './headers-interceptor'; +import IStateService = angular.ui.IStateService; + +export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: HeadersInterceptor, multi: true }, +]; @@ -21,7 +21,7 @@ export class ServiceContainerToUpgradeUiObject extends UiBaseObject { this.icon = componentToUpgrade.icon; this.version = componentToUpgrade.version; this.isAlreadyUpgrade = true; - this.isLock = componentToUpgrade.state === ComponentState.CERTIFICATION_IN_PROGRESS || componentToUpgrade.state === ComponentState.NOT_CERTIFIED_CHECKOUT; + this.isLock = componentToUpgrade.state === ComponentState.NOT_CERTIFIED_CHECKOUT; this.vspInstances = []; } @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="components-to-upgrade-list-item"> <div class="component-to-upgrade-data"> <div class="component-to-upgrade-icon small sprite-services-icons {{upgradedComponent.icon}}"></div> @@ -13,8 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <div class="components-to-upgrade-list-item "> <div class="component-to-upgrade-data"> <sdc-checkbox class="component-to-upgrade-checkbox" @@ -29,8 +29,7 @@ import {AutomatedUpgradeService} from "./automated-upgrade.service"; @Component({ selector: 'upgrade-vsp', templateUrl: './automated-upgrade.component.html', - styleUrls: ['./automated-upgrade.component.less'], - providers: [TranslateService] + styleUrls: ['./automated-upgrade.component.less'] }) export class AutomatedUpgradeComponent { @@ -2,16 +2,15 @@ * Created by ob0695 on 4/18/2018. */ import { NgModule } from "@angular/core"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; -import {CommonModule} from "@angular/common"; -import {AutomatedUpgradeStatusComponent} from "./automated-upgrade-status/automated-upgrade-status.component"; -import {AutomatedUpgradeComponent} from "./automated-upgrade.component"; -import {UpgradeListItemComponent} from "./automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component"; -import {UpgradeListItemStatusComponent} from "./automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component"; -import {TranslateService} from "../../shared/translator/translate.service"; -import {UpgradeListItemInnerContent} from "./automated-upgrade-ui-components/list-item-inner-content/list-item-inner-content.component"; -import {UpgradeLineItemComponent} from "./automated-upgrade-ui-components/upgrade-line-item/upgrade-line-item.component"; -import {UpgradeListItemOrderPipe} from "./automated-upgrade-ui-components/list-item-order-pipe/list-item-order-pipe"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { CommonModule } from "@angular/common"; +import { AutomatedUpgradeStatusComponent } from "./automated-upgrade-status/automated-upgrade-status.component"; +import { AutomatedUpgradeComponent } from "./automated-upgrade.component"; +import { UpgradeListItemComponent } from "./automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component"; +import { UpgradeListItemStatusComponent } from "./automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component"; +import { UpgradeListItemInnerContent } from "./automated-upgrade-ui-components/list-item-inner-content/list-item-inner-content.component"; +import { UpgradeLineItemComponent } from "./automated-upgrade-ui-components/upgrade-line-item/upgrade-line-item.component"; +import { UpgradeListItemOrderPipe } from "./automated-upgrade-ui-components/list-item-order-pipe/list-item-order-pipe"; @NgModule({ declarations: [ @@ -27,8 +26,7 @@ import {UpgradeListItemOrderPipe} from "./automated-upgrade-ui-components/list-i exports: [], entryComponents: [ AutomatedUpgradeComponent, AutomatedUpgradeStatusComponent - ], - providers: [TranslateService] + ] }) export class AutomatedUpgradeModule { }
\ No newline at end of file @@ -1,19 +1,15 @@ -import {SdcUiComponents} from "sdc-ui/lib/angular"; -import {Injectable, Inject} from "@angular/core"; -import {IModalConfig} from "sdc-ui/lib/angular/modals/models/modal-config"; -import {AutomatedUpgradeComponent} from "./automated-upgrade.component"; -import {Component} from "../../../models/components/component"; -import {ComponentServiceNg2} from "../../services/component-services/component.service"; -import {GeneralStatus, ComponentType} from "../../../utils/constants"; -import {IDependenciesServerResponse} from "../../services/responses/dependencies-server-response"; -import {AutomatedUpgradeStatusComponent} from "./automated-upgrade-status/automated-upgrade-status.component"; -import {AutomatedUpgradeStatusResponse} from "../../services/responses/automated-upgrade-response"; +import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular"; +import { Injectable, ComponentRef } from "@angular/core"; +import { AutomatedUpgradeComponent } from "./automated-upgrade.component"; +import { Component } from "../../../models/components/component"; +import { ComponentServiceNg2 } from "../../services/component-services/component.service"; +import { GeneralStatus, ComponentType } from "../../../utils/constants"; +import { IDependenciesServerResponse } from "../../services/responses/dependencies-server-response"; +import { AutomatedUpgradeStatusComponent } from "./automated-upgrade-status/automated-upgrade-status.component"; +import { AutomatedUpgradeStatusResponse } from "../../services/responses/automated-upgrade-response"; +import { TranslateService, ITranslateArgs } from "../../shared/translator/translate.service"; +import { ServiceContainerToUpgradeUiObject, AllottedResourceInstanceUiObject, VspInstanceUiObject } from "./automated-upgrade-models/ui-component-to-upgrade"; import Dictionary = _.Dictionary; -import {TranslateService, ITranslateArgs} from "../../shared/translator/translate.service"; -import { - ServiceContainerToUpgradeUiObject, - AllottedResourceInstanceUiObject, VspInstanceUiObject -} from "./automated-upgrade-models/ui-component-to-upgrade"; export interface IAutomatedUpgradeRequestObj { serviceId:string; @@ -30,8 +26,9 @@ export class AutomatedUpgradeService { private vspComponent:Component; private uiComponentsToUpgrade:Array<ServiceContainerToUpgradeUiObject>; private componentType:string; + private modalInstance: ComponentRef<SdcUiComponents.ModalComponent>; - constructor(private modalService:SdcUiComponents.ModalService, + constructor(private modalService:SdcUiServices.ModalService, private componentService:ComponentServiceNg2, private translateService:TranslateService) { } @@ -69,21 +66,21 @@ export class AutomatedUpgradeService { } private disabledAllModalButtons = ():void => { - this.modalService.getCurrentInstance().innerModalContent.instance.disabled = true; - this.modalService.getCurrentInstance().buttons[0].show_spinner = true; - this.modalService.getCurrentInstance().buttons[1].disabled = true; + this.modalInstance.instance.innerModalContent.instance.disabled = true; + this.modalInstance.instance.buttons[0].show_spinner = true; + this.modalInstance.instance.buttons[1].disabled = true; } public changeUpgradeButtonState = (isDisabled:boolean):void => { - if (this.modalService.getCurrentInstance().buttons[0].disabled !== isDisabled) { - this.modalService.getCurrentInstance().buttons[0].disabled = isDisabled; + if (this.modalInstance.instance.buttons[0].disabled !== isDisabled) { + this.modalInstance.instance.buttons[0].disabled = isDisabled; } } //TODO We will need to replace this function after sdc-ui modal new design, this is just a workaround public automatedUpgrade = ():void => { - let selectedServices = this.modalService.getCurrentInstance().innerModalContent.instance.selectedComponentsToUpgrade; + let selectedServices = this.modalInstance.instance.innerModalContent.instance.selectedComponentsToUpgrade; this.disabledAllModalButtons(); this.componentService.automatedUpgrade(this.vspComponent.componentType, this.vspComponent.uniqueId, this.convertToServerRequest(selectedServices)).subscribe((automatedUpgradeStatus:any) => { @@ -105,11 +102,11 @@ export class AutomatedUpgradeService { }); let statusModalTitle = this.getTextByComponentType("_UPGRADE_STATUS_TITLE"); - this.modalService.getCurrentInstance().setTitle(statusModalTitle); - this.modalService.getCurrentInstance().getButtons().splice(0, 1); // Remove the upgrade button - this.modalService.getCurrentInstance().buttons[0].disabled = false; // enable close again - this.modalService.getCurrentInstance().innerModalContent.destroy(); - this.modalService.createInnnerComponent(AutomatedUpgradeStatusComponent, { + this.modalInstance.instance.setTitle(statusModalTitle); + this.modalInstance.instance.getButtons().splice(0, 1); // Remove the upgrade button + this.modalInstance.instance.buttons[0].disabled = false; // enable close again + this.modalInstance.instance.innerModalContent.destroy(); + this.modalService.createInnnerComponent(this.modalInstance, AutomatedUpgradeStatusComponent, { upgradedComponentsList: upgradedComponent, upgradeStatusMap: statusMap, statusText: this.getStatusText(statusMap) @@ -250,10 +247,10 @@ export class AutomatedUpgradeService { let modalTitle = this.getTextByComponentType("_UPGRADE_TITLE"); let certificationText = isAfterCertification ? this.getTextByComponentType("_CERTIFICATION_STATUS_TEXT", {resourceName: this.vspComponent.name}) : undefined; - let upgradeVspModalConfig:IModalConfig = { + let upgradeVspModalConfig = { title: modalTitle, size: "md", - type: "custom", + type: SdcUiCommon.ModalType.custom, testId: "upgradeVspModal", buttons: [ { @@ -266,10 +263,11 @@ export class AutomatedUpgradeService { }, {text: 'CLOSE', size: 'sm', closeModal: true, type: 'secondary'} - ] - }; + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig; - this.modalService.openCustomModal(upgradeVspModalConfig, AutomatedUpgradeComponent, { + this.modalInstance = this.modalService.openModal(upgradeVspModalConfig); + this.modalService.createInnnerComponent(this.modalInstance, AutomatedUpgradeComponent, { componentsToUpgrade: this.uiComponentsToUpgrade, informationText: informationalText, certificationStatusText: certificationText @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`catalog component should match current snapshot of catalog component 1`] = ` +<catalog + $state={[Function Object]} + cacheService={[Function Object]} + catalogService={[Function Object]} + componentShouldReload={[Function Function]} + defaultFilterParams={[Function Object]} + getTestIdForCheckboxByText={[Function Function]} + initCatalogData={[Function Function]} + initLeftSwitch={[Function Function]} + initScopeMembers={[Function Function]} + isDefaultFilter={[Function Function]} + loaderService={[Function Object]} + resourceNamePipe={[Function Object]} + sdcConfig={[Function Object]} + sdcMenu={[Function Object]} + updateCatalogItems={[Function Function]} +> + <div + class="sdc-catalog-container" + > + <div + class="w-sdc-main-container" + > + <div + class="i-sdc-designer-leftbar-section-left-switch-header" + > + <div + class="i-sdc-designer-leftbar-section-left-switch-header-text" + > + + </div> + <div + class="i-sdc-designer-leftbar-section-left-switch-header-icon sprite-new arrow-up-small" + > + Â + </div> + + </div> + <div + class="sdc-catalog-body-container w-sdc-left-sidebar i-sdc-designer-left-sidebar" + perfectscrollbar="" + > + <div + class="sdc-catalog-leftbar-container" + > + <div + class="sdc-catalog-type-filter-container" + > + <div + class="i-sdc-designer-leftbar-section-title pointer" + > + <span + class="i-sdc-designer-leftbar-section-title-icon" + /> + <span + class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="typeFilterTitle" + > + Type + </span> + </div> + <div + class="i-sdc-designer-leftbar-section-content" + > + <sdc-checklist /> + </div> + </div> + <div + class="sdc-catalog-categories-filter-container" + > + <div + class="i-sdc-designer-leftbar-section-title pointer" + > + <span + class="i-sdc-designer-leftbar-section-title-icon" + /> + <span + class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="categoriesFilterTitle" + > + Categories + </span> + </div> + <div + class="i-sdc-designer-leftbar-section-content" + > + <sdc-checklist /> + </div> + </div> + <div + class="sdc-catalog-status-filter-container" + > + <div + class="i-sdc-designer-leftbar-section-title pointer" + > + <span + class="i-sdc-designer-leftbar-section-title-icon" + /> + <span + class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="statusFilterTitle" + > + Status + </span> + </div> + <div + class="i-sdc-designer-leftbar-section-content" + > + <sdc-checklist /> + </div> + </div> + </div> + </div> + <div + class="w-sdc-main-right-container w-sdc-catalog-main" + infinitescroll="" + > + <div + class="catalog-top-bar" + > + <div + class="w-sdc-dashboard-catalog-items-header" + /> + <div + class="catalog-top-right-bar" + > + <span + class="w-sdc-dashboard-catalog-header-order1" + > + + </span> + Â Â + <a + class="w-sdc-dashboard-catalog-sort" + data-tests-id="sort-by-last-update" + > + + </a> + Â + + <a + class="w-sdc-dashboard-catalog-sort" + data-tests-id="sort-by-alphabetical" + > + + </a> + Â + + </div> + </div> + <div + class="catalog-elements-list" + > + + </div> + </div> + </div> + <top-nav /> + </div> +</catalog> +`; @@ -0,0 +1,101 @@ +<div class="sdc-catalog-container"> + <div class="w-sdc-main-container"> + <div class="i-sdc-designer-leftbar-section-left-switch-header" + (click)="showCatalogSelector=!showCatalogSelector"> + <div class="i-sdc-designer-leftbar-section-left-switch-header-text"> + {{selectedCatalogItem.title}} + </div> + <div class="i-sdc-designer-leftbar-section-left-switch-header-icon sprite-new arrow-up-small"> </div> + + <div class="sdc-catalog-selector-wrapper" *ngIf="showCatalogSelector"> + <div class="sdc-catalog-selector-item" + *ngFor="let leftSwitchItem of catalogSelectorItems" + (click)="selectLeftSwitchItem(leftSwitchItem)"> + <span>{{leftSwitchItem.title}}</span> + </div> + </div> + </div> + + <!-- LEFT SIDE --> + <div perfectScrollbar class="sdc-catalog-body-container w-sdc-left-sidebar i-sdc-designer-left-sidebar"> + <div class="sdc-catalog-leftbar-container"> + <div class="sdc-catalog-type-filter-container"> + <div class="i-sdc-designer-leftbar-section-title pointer" + (click)="sectionClick('type')" + [ngClass]="{'expanded': expandedSection.indexOf('type') !== -1}"> + <span class="i-sdc-designer-leftbar-section-title-icon"></span> + <span class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="typeFilterTitle">Type</span> + </div> + <div class="i-sdc-designer-leftbar-section-content"> + <sdc-checklist [checklistModel]="typesChecklistModel" [testId]="'checklist-type'" + (checkedChange)="gui.onComponentTypeClick()"></sdc-checklist> + </div> + </div> + + <div class="sdc-catalog-categories-filter-container"> + <div class="i-sdc-designer-leftbar-section-title pointer" (click)="sectionClick('category')" + [ngClass]="{'expanded': expandedSection.indexOf('category') !== -1}"> + <span class="i-sdc-designer-leftbar-section-title-icon"></span> + <span class="i-sdc-designer-leftbar-section-title-text" data-tests-id="categoriesFilterTitle">Categories</span> + </div> + <div class="i-sdc-designer-leftbar-section-content"> + <sdc-checklist [checklistModel]="categoriesChecklistModel" [testId]="'checklist-category'" + (checkedChange)="gui.onCategoryClick()"></sdc-checklist> + </div> + </div> + + <!-- STATUS --> + <div class="sdc-catalog-status-filter-container"> + <div class="i-sdc-designer-leftbar-section-title pointer" (click)="sectionClick('status')" + [ngClass]="{'expanded': expandedSection.indexOf('status') !== -1}"> + <span class="i-sdc-designer-leftbar-section-title-icon"></span> + <span class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="statusFilterTitle">Status</span> + </div> + + <div class="i-sdc-designer-leftbar-section-content"> + <sdc-checklist [checklistModel]="statusChecklistModel" [testId]="'checklist-status'" + (checkedChange)="gui.onStatusClick()"></sdc-checklist> + </div> + </div> + + </div> + </div> + + <!-- RIGHT SIDE --> + <div class="w-sdc-main-right-container w-sdc-catalog-main" infiniteScroll + (infiniteScroll)="raiseNumberOfElementToDisplay()" [infiniteScrollDistance]="100"> + <!-- HEADER --> + <div class="catalog-top-bar"> + <div class="w-sdc-dashboard-catalog-items-header" + [innerHTML]="getNumOfElements(catalogFilteredItems.length)"> + + </div> + <div class="catalog-top-right-bar"> + <span class="w-sdc-dashboard-catalog-header-order1">{{'SORT_CAPTION'|translate}}</span> + <a class="w-sdc-dashboard-catalog-sort" data-tests-id="sort-by-last-update" + [ngClass]="{'blue' : sortBy==='lastUpdateDate'}" + (click)="order('lastUpdateDate')">{{'SORT_BY_UPDATE_DATE'|translate}}</a> + <span *ngIf="sortBy === 'lastUpdateDate'" class="w-sdc-catalog-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"></span> + <a class="w-sdc-dashboard-catalog-sort" data-tests-id="sort-by-alphabetical" + [ngClass]="{'blue' : sortBy!=='lastUpdateDate'}" + (click)="order('resourceName')">{{'SORT_ALPHABETICAL'|translate}}</a> + <span *ngIf="sortBy !== 'lastUpdateDate'" class="w-sdc-catalog-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"></span> + </div> + </div> + + <div class='catalog-elements-list'> + <!-- Tile new --> + <ui-tile *ngFor="let component of catalogFilteredSlicedItems" + [component]="component" (onTileClick)="goToComponent(component)"></ui-tile> + <!-- Tile new --> + </div> + </div> + </div> + + <top-nav [topLvlSelectedIndex]="1" [searchTerm]="search.filterTerm" + (searchTermChange)="gui.changeFilterTerm($event)" [version]="version"></top-nav> +</div> @@ -0,0 +1,113 @@ +@import '../../../../assets/styles/variables'; +@import '../../../../assets/styles/mixins'; + +.w-sdc-catalog-main { + height: 100%; + overflow-y: scroll; + padding: 10px 50px; + + .catalog-top-bar { + display: flex; + justify-content:space-between; + padding: 0px 10px; + } + + .catalog-elements-list { + display: flex; + flex-wrap: wrap; + flex-direction: row; + } + +} + + +.i-sdc-designer-left-sidebar { + margin-top: 43px; +} + +.sdc-catalog-selector-item { + text-transform: none; + line-height: 40px; + font-family: OpenSans-Bold, sans-serif; + font-size: 14px; + color: #000000; + background-color: #ffffff; + padding-left: 20px; +} + +.sdc-catalog-selector-wrapper { + position: absolute; + left: 0px; + top: 42px; + width: 241px; + height: auto; + cursor: pointer; + opacity: 1; + z-index: 1000; + box-shadow: 1px 2px 3px #b1b1b1; +} + +.sdc-catalog-selector-item { + text-transform: none; + line-height: 40px; + font-family: OpenSans-Bold, sans-serif; + font-size: 14px; + color: @main_color_l; + background-color: @main_color_p; + padding-left: 20px; +} + +.sdc-catalog-selector-item:hover { + color: @main_color_a; + background-color: @tlv_color_v; +} + +.i-sdc-designer-leftbar-section-left-switch-header { + text-transform: uppercase; + .l_14_m; + line-height: 40px; + width: 243px; + + font-family: OpenSans-Bold, sans-serif; + font-size: 14px; + + color: @main_color_a; + background-color: @tlv_color_t; + border: solid 1px fade(@main_color_t, 40%); + cursor: pointer; + opacity: 1; + z-index: 9999; + position: relative; +} + +.i-sdc-designer-leftbar-section-left-switch-header-text { + display: inline-block; + width: 180px; + margin-left: 20px; +} + +.i-sdc-designer-leftbar-section-left-switch-header-icon { + display: inline-block; + vertical-align: middle; +} + +.w-sdc-dashboard-catalog-items-header { + color: @main_color_m; + font-family: OpenSans-Regular, sans-serif; + font-size: 12px; + display: inline-block; + font-style: normal; size: 12px; + margin-left: 11px; + font-weight: normal; + b { + font-family: OpenSans-Bold, sans-serif; + color: @main_color_l; + font-weight: bold; + } +} + +.w-sdc-dashboard-catalog-header-order1 { + font-style: normal; + font-size: 12px; + font-weight: 800; +} @@ -0,0 +1,649 @@ + +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import {ConfigureFn, configureTests} from "../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import { CacheService} from "../../../../app/services-ng2"; +import {CatalogComponent} from "./catalog.component"; +import { SdcUiServices } from "onap-ui-angular"; +import { SdcConfigToken } from "../../config/sdc-config.config"; +import { SdcMenuToken} from "../../config/sdc-menu.config"; +import { ResourceNamePipe } from "../../pipes/resource-name.pipe"; +import { CatalogService } from "../../services/catalog.service"; +import {TranslatePipe} from "../../shared/translator/translate.pipe"; +import {TranslateService} from "../../shared/translator/translate.service"; +import {Observable} from "rxjs"; +import {LoaderService} from "onap-ui-angular/dist/loader/loader.service"; +import {categoriesElements} from "../../../../jest/mocks/categories.mock"; +import {sdcMenu} from "../../../../jest/mocks/sdc-menu.mock"; +import {IEntityFilterObject} from "../../pipes/entity-filter.pipe"; + + + + + +describe('catalog component', () => { + + let fixture: ComponentFixture<CatalogComponent>; + + //Data variables + let catalogSelectorItemsMock; + let checkListModelMock; + let filterParamsMock; + let checkboxesFilterMock; + let checkboxesFilterKeysMock; + + + //Service variables + let stateServiceMock; + let cacheServiceMock: Partial<CacheService>; + let loaderServiceMock: Partial<LoaderService>; + let catalogServiceMock: Partial<CatalogService>; + + + beforeEach( + + async(() => { + console.info = jest.fn(); + catalogSelectorItemsMock = [ + { + value: 0, + title: 'Active Items', + header: 'Active' + }, + { + value: 1, + title: 'Archive', + header: 'Archived' + } + ]; + checkListModelMock = { + checkboxes: [ + {label: "VF", disabled: false, isChecked: false, testId: "checkbox-vf", value: "Resource.VF"}, + {label: "VFC", disabled: false, isChecked: false, testId: "checkbox-vfc", value: "Resource.VFC", + subLevelChecklist: {checkboxes:[{label: "VFD", disabled: false, isChecked: false, testId: "checkbox-vfd", value: "Resource.VFD"}], + selectedValues: ["Resource.VFD"]} + }, + {label: "CR", disabled: false, isChecked: false, testId: "checkbox-cr", value: "Resource.CR", + subLevelChecklist: { checkboxes:[{label: "VF", disabled: false, isChecked: false, testId: "checkbox-vf", value: "Resource.VF"}], + selectedValues: []} + }], + selectedValues: ["Resource.VF"] + } + filterParamsMock = { + active: true, + categories: ["resourceNewCategory.allotted resource.allotted resource", "resourceNewCategory.allotted resource.contrail route", "resourceNewCategory.application l4+.application server"], + components: ["Resource.VF", "Resource.VFC"], + order: ["lastUpdateDate", true], + statuses: ["inDesign"], + term: "Vf" + } + checkboxesFilterMock = { + selectedCategoriesModel: ["serviceNewCategory.network l4+", "resourceNewCategory.allotted resource.allotted resource"], + selectedComponentTypes: ["Resource.VF", "Resource.VFC"], + selectedResourceSubTypes: ["VF", "VFC"], + selectedStatuses: ["NOT_CERTIFIED_CHECKOUT", "NOT_CERTIFIED_CHECKIN"] + }; + checkboxesFilterKeysMock = { + categories:{_main: ["serviceNewCategory.network l4+"]}, + componentTypes: { Resource: ["Resource.VF", "Resource.VFC"], _main: ["Resource.VFC"]}, + statuses: {_main: ["inDesign"]} + } + + stateServiceMock = { + go: jest.fn(), + current: jest.fn() + }; + cacheServiceMock = { + get: jest.fn().mockImplementation(()=> categoriesElements), + set: jest.fn(), + contains: jest.fn().mockImplementation(()=> true) + }; + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + catalogServiceMock = { + //TODO create mock function of archive + getCatalog: jest.fn().mockImplementation(()=> Observable.of(categoriesElements)), + getArchiveCatalog: jest.fn().mockImplementation(()=> Observable.of(categoriesElements)) + }; + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [CatalogComponent, TranslatePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: SdcConfigToken, useValue: {}}, + {provide: SdcMenuToken, useValue: sdcMenu}, + {provide: "$state", useValue: stateServiceMock }, + {provide: CacheService, useValue: cacheServiceMock }, + {provide: CatalogService, useValue: catalogServiceMock }, + {provide: ResourceNamePipe, useValue: {}}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }, + {provide: TranslateService, useValue: {}} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(CatalogComponent); + }); + }) + ); + + + it('should match current snapshot of catalog component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it ('should call on catalog component onInit' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initGui = jest.fn(); + component.componentInstance.initLeftSwitch = jest.fn(); + component.componentInstance.initScopeMembers = jest.fn(); + component.componentInstance.loadFilterParams = jest.fn(); + component.componentInstance.initCatalogData = jest.fn(); + component.componentInstance.ngOnInit(); + expect(component.componentInstance.initGui).toHaveBeenCalled(); + expect(component.componentInstance.initLeftSwitch).toHaveBeenCalled(); + expect(component.componentInstance.initScopeMembers).toHaveBeenCalled(); + expect(component.componentInstance.loadFilterParams).toHaveBeenCalled(); + expect(component.componentInstance.initCatalogData).toHaveBeenCalled(); + }); + + it ('should call on catalog component initLeftSwitch' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initLeftSwitch(); + expect(component.componentInstance.showCatalogSelector).toEqual(false); + expect(component.componentInstance.catalogSelectorItems).toEqual(catalogSelectorItemsMock); + expect(component.componentInstance.selectedCatalogItem).toEqual(catalogSelectorItemsMock[0]); + }); + + it ('should call on catalog component initCatalogData and selectedCatalogItem is archive ' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getArchiveCatalogItems = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[1]; + component.componentInstance.initCatalogData(); + expect(component.componentInstance.getArchiveCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component initCatalogData and selectedCatalogItem is active ' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getActiveCatalogItems = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0]; + component.componentInstance.initCatalogData(); + expect(component.componentInstance.getActiveCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component initScopeMembers' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.makeSortedCategories = jest.fn().mockImplementation(()=> categoriesElements); + component.componentInstance.initCategoriesMap = jest.fn(); + component.componentInstance.initCheckboxesFilter = jest.fn(); + component.componentInstance.initCheckboxesFilterKeys = jest.fn(); + component.componentInstance.buildCheckboxLists = jest.fn(); + component.componentInstance.initScopeMembers(); + expect(component.componentInstance.numberOfItemToDisplay).toEqual(0); + expect(component.componentInstance.categories).toEqual(categoriesElements); + expect(component.componentInstance.confStatus).toEqual(component.componentInstance.sdcMenu.statuses); + expect(component.componentInstance.expandedSection).toEqual( ["type", "category", "status"]); + expect(component.componentInstance.catalogItems).toEqual([]); + expect(component.componentInstance.search).toEqual({FilterTerm: ""}); + expect(component.componentInstance.initCategoriesMap).toHaveBeenCalled(); + expect(component.componentInstance.initCheckboxesFilter).toHaveBeenCalled(); + expect(component.componentInstance.initCheckboxesFilterKeys).toHaveBeenCalled(); + expect(component.componentInstance.buildCheckboxLists).toHaveBeenCalled(); + expect(component.componentInstance.version).toEqual(categoriesElements); + expect(component.componentInstance.sortBy).toEqual('lastUpdateDate'); + expect(component.componentInstance.reverse).toEqual(true); + }); + + it ('should call on catalog component buildCheckboxLists ' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.buildChecklistModelForTypes = jest.fn(); + component.componentInstance.buildChecklistModelForCategories = jest.fn(); + component.componentInstance.buildChecklistModelForStatuses = jest.fn(); + component.componentInstance.buildCheckboxLists(); + expect(component.componentInstance.buildChecklistModelForTypes).toHaveBeenCalled(); + expect(component.componentInstance.buildChecklistModelForCategories).toHaveBeenCalled(); + expect(component.componentInstance.buildChecklistModelForStatuses).toHaveBeenCalled(); + }); + + it ('should call on catalog component getTestIdForCheckboxByText ' , () => { + const component = TestBed.createComponent(CatalogComponent); + let testId = component.componentInstance.getTestIdForCheckboxByText("catalog filter"); + expect(testId).toEqual("checkbox-catalogfilter"); + }); + + it ('should call on catalog component selectLeftSwitchItem with active catalog' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[1]; + component.componentInstance.getActiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[0]); + expect(component.componentInstance.getActiveCatalogItems).toBeCalledWith(true); + expect(component.componentInstance.changeFilterParams).toBeCalledWith({"active": true}); + }); + + it ('should call on catalog component selectLeftSwitchItem with archive catalog' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0]; + component.componentInstance.getArchiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[1]); + expect(component.componentInstance.getArchiveCatalogItems).toBeCalledWith(true); + expect(component.componentInstance.changeFilterParams).toBeCalledWith({"active": false}); + }); + + it ('should call on catalog component buildChecklistModelForTypes' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.buildChecklistModelForTypes(); + expect(component.componentInstance.componentTypes).toEqual({ Resource: ['VF', 'VFC', 'CR', 'PNF', 'CP', 'VL'], + Service: null}) + expect(component.componentInstance.typesChecklistModel.checkboxes.length).toEqual(2); + }); + + it ('should call on catalog component buildChecklistModelForCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.categories = categoriesElements; + component.componentInstance.buildChecklistModelForCategories(); + expect(component.componentInstance.categoriesChecklistModel.checkboxes).not.toEqual(null); + }); + + it ('should call on catalog component buildChecklistModelForStatuses' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.categories = categoriesElements; + component.componentInstance.confStatus = sdcMenu.statuses; + component.componentInstance.buildChecklistModelForStatuses(); + expect(component.componentInstance.statusChecklistModel.checkboxes.length).toEqual(3); + }); + + it ('should call on catalog component initCheckboxesFilter' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initCheckboxesFilter(); + expect(component.componentInstance.checkboxesFilter.selectedComponentTypes).toEqual([]); + expect(component.componentInstance.checkboxesFilter.selectedResourceSubTypes).toEqual([]); + expect(component.componentInstance.checkboxesFilter.selectedCategoriesModel).toEqual([]); + expect(component.componentInstance.checkboxesFilter.selectedStatuses).toEqual([]); + }); + + it ('should call on catalog component initCheckboxesFilterKeys' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initCheckboxesFilterKeys(); + expect(component.componentInstance.checkboxesFilterKeys.componentTypes).toEqual({ _main: [] }); + expect(component.componentInstance.checkboxesFilterKeys.categories).toEqual({ _main: [] }); + expect(component.componentInstance.checkboxesFilterKeys.statuses).toEqual({ _main: [] }); + }); + + it ('should call on catalog component initCategoriesMap' , () => { + const component = TestBed.createComponent(CatalogComponent); + const categoriesMap = component.componentInstance.initCategoriesMap(categoriesElements); + expect(categoriesMap["resourceNewCategory.allotted resource.allotted resource"].parent.name).toEqual("Allotted Resource"); + expect(categoriesMap["resourceNewCategory.generic"].category.uniqueId).toEqual("resourceNewCategory.generic"); + expect(categoriesMap["serviceNewCategory.voip call control"].category.name).toEqual("VoIP Call Control"); + + }); + + + it ('should call on catalog component selectLeftSwitchItem with active and selectedCatalogItem equal to archived' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getActiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[1] + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[0]); + expect(component.componentInstance.getActiveCatalogItems).toHaveBeenCalledWith(true); + expect(component.componentInstance.changeFilterParams).toHaveBeenCalledWith({active: true}) + }); + + it ('should call on catalog component selectLeftSwitchItem with archived and selectedCatalogItem equal to active' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getArchiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0] + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[1]); + expect(component.componentInstance.getArchiveCatalogItems).toBeCalledWith(true); + expect(component.componentInstance.changeFilterParams).toHaveBeenCalledWith({active: false}) + }); + + it ('should call on catalog component sectionClick with section contains in expandedSection' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.expandedSection = ["type", "category", "status"]; + component.componentInstance.sectionClick("type"); + expect(component.componentInstance.expandedSection).toEqual(["category", "status"]) + }); + + it ('should call on catalog component sectionClick with section not contains in expandedSection' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.expandedSection = ["type", "category", "status"]; + component.componentInstance.sectionClick("newItem"); + expect(component.componentInstance.expandedSection).toEqual(["type", "category", "status", "newItem"]) + }); + + it ('should call on catalog component makeFilterParamsFromCheckboxes with selected values' , () => { + const component = TestBed.createComponent(CatalogComponent); + expect(component.componentInstance.makeFilterParamsFromCheckboxes(checkListModelMock)).toEqual(["Resource.VF", "Resource.VFD"]) + }); + + it ('should call on catalog component order with resourceName value' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.filterParams = filterParamsMock + component.componentInstance.order("resourceName"); + expect(component.componentInstance.changeFilterParams).toHaveBeenCalledWith( {"order": ["resourceName", false]}) + }); + + it ('should call on catalog component goToComponent' , () => { + const component = TestBed.createComponent(CatalogComponent); + const componentMock = { uniqueId: "d3e80fed-12f6-4f29-aeb1-771050e5db72", componentType: "RESOURCE"} + component.componentInstance.goToComponent(componentMock); + expect(stateServiceMock.go).toHaveBeenCalledWith('workspace.general', {id: componentMock.uniqueId, type: componentMock.componentType.toLowerCase()}) + + }); + + it ('should call on catalog component getNumOfElements for active catalog' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0] + expect(component.componentInstance.getNumOfElements(3)).toEqual("3 <b>Active</b> Elements found") + + }); + + it ('should call on catalog component raiseNumberOfElementToDisplay with empty catalogFilteredItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.catalogFilteredItems = [] + component.componentInstance.raiseNumberOfElementToDisplay(true); + expect(component.componentInstance.numberOfItemToDisplay).toEqual(NaN); + expect(component.componentInstance.catalogFilteredSlicedItems).toEqual([]); + }); + + it ('should call on catalog component raiseNumberOfElementToDisplay with full catalogFilteredItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.catalogFilteredItems = [1 , 2 , 3, 4, 5, 6] + component.componentInstance.numberOfItemToDisplay = 2; + component.componentInstance.raiseNumberOfElementToDisplay(false); + expect(component.componentInstance.numberOfItemToDisplay).toEqual(6); + expect(component.componentInstance.catalogFilteredSlicedItems).toEqual([1 , 2 , 3, 4, 5, 6]); + }); + + it ('should call on catalog component componentShouldReload return false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.isDefaultFilter = jest.fn().mockImplementation(() => false); + cacheServiceMock.get.mockImplementation(()=> "mockConstructor"); + let componentShouldReload = component.componentInstance.componentShouldReload(); + expect(component.componentInstance.cacheService.get()).toEqual(component.componentInstance.$state.current.name); + expect(component.componentInstance.cacheService.contains()).toEqual(true); + expect(component.componentInstance.isDefaultFilter).toHaveBeenCalled(); + expect(componentShouldReload).toEqual(false); + }); + + it ('should call on catalog component componentShouldReload return true' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.isDefaultFilter = jest.fn(); + let componentShouldReload = component.componentInstance.componentShouldReload(); + expect(component.componentInstance.cacheService.get()).not.toEqual(component.componentInstance.$state.current.name); + expect(component.componentInstance.cacheService.contains()).toEqual(true); + expect(componentShouldReload).toEqual(true); + }); + + it ('should call on catalog component getActiveCatalogItems with true' , () => { + const component = TestBed.createComponent(CatalogComponent); + let resp = component.componentInstance.cacheService.get(); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => {}); + component.componentInstance.getActiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(component.componentInstance.cacheService.set).toHaveBeenCalledWith('breadcrumbsComponentsState', "mockConstructor"); + expect(component.componentInstance.cacheService.set).toHaveBeenCalledWith('breadcrumbsComponents', categoriesElements); + expect(component.componentInstance.catalogService.getCatalog).toHaveBeenCalled(); + }); + + it ('should call on catalog component getActiveCatalogItems with false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.componentShouldReload = jest.fn(); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => {}); + component.componentInstance.getActiveCatalogItems(false); + expect(component.componentInstance.componentShouldReload).toHaveBeenCalled(); + let resp = component.componentInstance.cacheService.get(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp); + }); + + it ('should call on catalog component getActiveCatalogItems with true observable return error' , () => { + const component = TestBed.createComponent(CatalogComponent); + catalogServiceMock.getCatalog.mockImplementation(()=> Observable.throwError('error')); + component.componentInstance.getActiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledWith('Failed to load catalog CatalogViewModel::getActiveCatalogItems'); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(component.componentInstance.catalogService.getCatalog).toHaveBeenCalled(); + }); + + it ('should call on catalog component getArchiveCatalogItems with true' , () => { + const component = TestBed.createComponent(CatalogComponent); + const resp = component.componentInstance.cacheService.get(); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => {}); + component.componentInstance.getArchiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(component.componentInstance.catalogService.getArchiveCatalog).toHaveBeenCalled(); + expect(component.componentInstance.cacheService.set).toHaveBeenCalledWith('archiveComponents', categoriesElements); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp) + }); + + it ('should call on catalog component getArchiveCatalogItems with false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => Observable.of()); + component.componentInstance.getArchiveCatalogItems(false); + expect(component.componentInstance.cacheService.contains).toHaveBeenCalled(); + expect(component.componentInstance.cacheService.get).toHaveBeenCalled(); + let resp = component.componentInstance.cacheService.get(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp); + }); + + it ('should call on catalog component getArchiveCatalogItems with true observable return error' , () => { + const component = TestBed.createComponent(CatalogComponent); + catalogServiceMock.getArchiveCatalog.mockImplementation(()=> Observable.throwError('error')); + component.componentInstance.getArchiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(component.componentInstance.catalogService.getArchiveCatalog).toHaveBeenCalled(); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledWith('Failed to load catalog CatalogViewModel::getArchiveCatalogItems'); + }); + + it ('should call on catalog component updateCatalogItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.filterCatalogItems = jest.fn(); + component.componentInstance.addFilterTermToComponent = jest.fn(); + component.componentInstance.updateCatalogItems([1, 2, 3]); + expect(component.componentInstance.catalogItems).toEqual([1, 2, 3]); + expect(component.componentInstance.addFilterTermToComponent).toHaveBeenCalled(); + expect(component.componentInstance.filterCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component applyFilterParamsToView' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initCheckboxesFilter = jest.fn(); + component.componentInstance.filterCatalogCategories = jest.fn(); + component.componentInstance.applyFilterParamsComponents = jest.fn(); + component.componentInstance.applyFilterParamsCategories = jest.fn(); + component.componentInstance.applyFilterParamsStatuses = jest.fn(); + component.componentInstance.applyFilterParamsOrder = jest.fn(); + component.componentInstance.applyFilterParamsTerm = jest.fn(); + component.componentInstance.filterCatalogItems = jest.fn(); + component.componentInstance.applyFilterParamsToView(filterParamsMock); + expect(component.componentInstance.initCheckboxesFilter).toHaveBeenCalled(); + expect(component.componentInstance.filterCatalogCategories).toHaveBeenCalled(); + expect(component.componentInstance.applyFilterParamsComponents).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsCategories).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsStatuses).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsOrder).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsTerm).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.filterCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component filterCatalogCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.makeFilteredCategories = jest.fn(); + component.componentInstance.buildChecklistModelForCategories = jest.fn(); + component.componentInstance.categories = categoriesElements; + component.componentInstance.checkboxesFilter = {selectedComponentTypes: ["firstType", "secondType"]}; + component.componentInstance.filterCatalogCategories(); + expect(component.componentInstance.makeFilteredCategories).toHaveBeenCalledWith(categoriesElements, ["firstType", "secondType"]); + expect(component.componentInstance.buildChecklistModelForCategories).toHaveBeenCalled(); + }); + + it ('should call on catalog component filterCatalogItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.makeFilteredItems = jest.fn().mockImplementation(() => [1,2,3]); + component.componentInstance.raiseNumberOfElementToDisplay = jest.fn(); + component.componentInstance.catalogItems = ["firstComponent", "secondComponent"]; + component.componentInstance.checkboxesFilter = {}; + component.componentInstance.search = {}; + component.componentInstance.sortBy = ""; + component.componentInstance.reverse = true; + component.componentInstance.numberOfItemToDisplay = 2; + // component.componentInstance.catalogFilteredItems = component.componentInstance.makeFilteredItems(); + component.componentInstance.filterCatalogItems(); + expect(component.componentInstance.makeFilteredItems).toHaveBeenCalledWith(["firstComponent", "secondComponent"], {}, {}, "",true); + expect(component.componentInstance.raiseNumberOfElementToDisplay).toHaveBeenCalledWith(true); + expect(component.componentInstance.catalogFilteredSlicedItems).toEqual([1,2]); + }); + + it ('should call on catalog component applyFilterParamsToCheckboxes' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes(checkListModelMock, ["Resource.CR", "Resource.VFD", "Resource.VF"]); + expect(checkListModelMock.selectedValues).toEqual(["Resource.VF","Resource.CR"]); + expect(checkListModelMock.checkboxes[1].subLevelChecklist.selectedValues).toEqual(["Resource.VFD"]); + expect(checkListModelMock.checkboxes[2].subLevelChecklist.selectedValues).toEqual(["Resource.VF"]) + }); + + it ('should call on catalog component applyFilterParamsComponents and filterParams.active equal true' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.catalogSelectorItems = catalogSelectorItemsMock; + component.componentInstance.typesChecklistModel = checkListModelMock; + component.componentInstance.applyFilterParamsComponents(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.components); + expect(component.componentInstance.checkboxesFilter.selectedComponentTypes).toEqual(["Resource.VFC"]); + expect(component.componentInstance.checkboxesFilter.selectedResourceSubTypes).toEqual(["VF", "VFC"]); + expect(component.componentInstance.selectedCatalogItem).toEqual(catalogSelectorItemsMock[0]); + }); + + it ('should call on catalog component applyFilterParamsComponents and filterParams.active equal false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + filterParamsMock.active = false; + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.catalogSelectorItems = catalogSelectorItemsMock; + component.componentInstance.typesChecklistModel = checkListModelMock; + component.componentInstance.applyFilterParamsComponents(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.components); + expect(component.componentInstance.checkboxesFilter.selectedComponentTypes).toEqual(["Resource.VFC"]); + expect(component.componentInstance.checkboxesFilter.selectedResourceSubTypes).toEqual(["VF", "VFC"]); + expect(component.componentInstance.selectedCatalogItem).toEqual(catalogSelectorItemsMock[1]); + }); + + it ('should call on catalog component applyFilterParamsCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + component.componentInstance.categoriesChecklistModel = checkListModelMock; + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.applyFilterParamsCategories(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.categories); + expect(component.componentInstance.checkboxesFilter.selectedCategoriesModel).toEqual(["serviceNewCategory.network l4+"]); + }); + + it ('should call on catalog component applyFilterParamsStatuses' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + + component.componentInstance.statusChecklistModel = checkListModelMock; + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.confStatus = sdcMenu.statuses; + component.componentInstance.applyFilterParamsStatuses(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.statuses); + expect(component.componentInstance.checkboxesFilter.selectedStatuses).toEqual(["NOT_CERTIFIED_CHECKOUT", "NOT_CERTIFIED_CHECKIN"]); + }); + + it ('should call on catalog component applyFilterParamsOrder' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsOrder(filterParamsMock); + expect(component.componentInstance.sortBy).toEqual("lastUpdateDate"); + expect(component.componentInstance.reverse).toEqual( true); + }); + + it ('should call on catalog component applyFilterParamsTerm' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsTerm(filterParamsMock); + expect(component.componentInstance.search.filterTerm).toEqual("Vf"); + }); + + // it ('should call on catalog component loadFilterParams' , () => { + // const component = TestBed.createComponent(CatalogComponent); + // component.componentInstance.$state = {params: {}}; + // component.componentInstance.loadFilterParams(); + // }); + + it ('should call on catalog component changeFilterParams' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToView = jest.fn(); + component.componentInstance.filterParams = { active: true, categories: [], components: [], order: ["lastUpdateDate", true], statuses: [], term: ""}; + component.componentInstance.$state.go = jest.fn().mockImplementation(() => Promise.resolve({ json: () => [] })); + const newParams = {"filter.active": true, "filter.categories": "resourceNewCategory.allotted resource.allotted resource,resourceNewCategory.allotted resource.contrail route,resourceNewCategory.application l4+.application server", "filter.components": "Resource.VF,Resource.VFC", "filter.order": "-lastUpdateDate", "filter.statuses": "inDesign", "filter.term": "Vf"} + component.componentInstance.changeFilterParams(filterParamsMock); + expect(component.componentInstance.filterParams).toEqual(filterParamsMock); + expect(component.componentInstance.$state.go).toHaveBeenCalledWith('.',newParams, {location: 'replace', notify: false}); + expect(component.componentInstance.applyFilterParamsToView).toHaveBeenCalledWith(filterParamsMock); + }); + + it ('should call on catalog component changeFilterParams and rebuild equal true' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToView = jest.fn(); + component.componentInstance.makeFilterParamsFromCheckboxes = jest.fn(); + component.componentInstance.buildCheckboxLists = jest.fn(); + component.componentInstance.filterParams = { active: true, categories: [], components: [], order: ["lastUpdateDate", true], statuses: [], term: ""}; + component.componentInstance.$state.go = jest.fn().mockImplementation(() => Promise.resolve({ json: () => [] })); + const newParams = {"filter.active": true, "filter.categories": "resourceNewCategory.allotted resource.allotted resource,resourceNewCategory.allotted resource.contrail route,resourceNewCategory.application l4+.application server", "filter.components": "Resource.VF,Resource.VFC", "filter.order": "-lastUpdateDate", "filter.statuses": "inDesign", "filter.term": "Vf"} + component.componentInstance.typesChecklistModel = checkListModelMock; + component.componentInstance.categoriesChecklistModel = checkListModelMock; + component.componentInstance.statusChecklistModel = checkListModelMock; + component.componentInstance.changeFilterParams(filterParamsMock, true); + expect(component.componentInstance.filterParams).toEqual(filterParamsMock); + expect(component.componentInstance.$state.go).toHaveBeenCalledWith('.',newParams, {location: 'replace', notify: false}); + //expect(component.componentInstance.makeFilterParamsFromCheckboxes).toHaveBeenCalledWith(component.componentInstance.typesChecklistModel); + //expect(component.componentInstance.buildCheckboxLists).toHaveBeenCalled(); + expect(component.componentInstance.applyFilterParamsToView).toHaveBeenCalledWith(filterParamsMock); + }); + + it ('should call on catalog component makeFilteredCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + const categoryMock = [{"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}]; + cacheServiceMock.get.mockImplementation(()=> categoryMock); + const resp = component.componentInstance.makeFilteredCategories(categoriesElements, checkboxesFilterMock.selectedComponentTypes); + expect(component.componentInstance.cacheService.get).toHaveBeenCalledWith("resourceCategories"); + expect(resp).toEqual(categoryMock); + }); + + it ('should call on catalog component makeFilteredCategories return unique elements' , () => { + const component = TestBed.createComponent(CatalogComponent); + const categoryMock = [{"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}, + {"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}, + {"name":"Network Service","normalizedName":"network service","uniqueId":"serviceNewCategory.network service","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}]; + const categoryUniqueMock = [{"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}, + {"name":"Network Service","normalizedName":"network service","uniqueId":"serviceNewCategory.network service","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}]; + cacheServiceMock.get.mockImplementation(()=> categoryMock); + checkboxesFilterMock.selectedComponentTypes = ["SERVICE", "Resource.VF"]; + const resp = component.componentInstance.makeFilteredCategories(categoriesElements, checkboxesFilterMock.selectedComponentTypes); + expect(component.componentInstance.cacheService.get).toHaveBeenCalledWith("resourceCategories"); + expect(resp).toEqual(categoryUniqueMock); + }); + + +}); @@ -0,0 +1,666 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import { Component as NgComponent, Inject } from '@angular/core'; +import { SdcUiCommon, SdcUiServices } from "onap-ui-angular"; +import { CacheService, CatalogService } from "app/services-ng2"; +import { SdcConfigToken, ISdcConfig } from "../../config/sdc-config.config"; +import { SdcMenuToken, IAppMenu } from "../../config/sdc-menu.config"; +import { Component, ICategoryBase, IMainCategory, ISubCategory, IConfigStatuses, ICatalogSelector, CatalogSelectorTypes } from "app/models"; +import { ResourceNamePipe } from "../../pipes/resource-name.pipe"; +import { EntityFilterPipe, IEntityFilterObject, ISearchFilter} from "../../pipes/entity-filter.pipe"; + +interface Gui { + onComponentSubTypesClick:Function; + onComponentTypeClick:Function; + onCategoryClick:Function; + onStatusClick:Function; + changeFilterTerm:Function; +} + +interface IFilterParams { + components: string[]; + categories: string[]; + statuses: (string)[]; + order: [string, boolean]; + term: string; + active: boolean; +} + +interface ICheckboxesFilterMap { + [key: string]: Array<string>; + _main: Array<string>; +} + +interface ICheckboxesFilterKeys { + componentTypes: ICheckboxesFilterMap; + categories: ICheckboxesFilterMap; + statuses: ICheckboxesFilterMap; +} + +interface ICategoriesMap { + [key: string]: { + category: ICategoryBase, + parent: ICategoryBase + } +} + +@NgComponent({ + selector: 'catalog', + templateUrl: './catalog.component.html', + styleUrls:['./catalog.component.less'] +}) +export class CatalogComponent { + public checkboxesFilter:IEntityFilterObject; + public checkboxesFilterKeys:ICheckboxesFilterKeys; + public gui:Gui; + public categories:Array<IMainCategory>; + public filteredCategories:Array<IMainCategory>; + public confStatus:IConfigStatuses; + public componentTypes:{[key:string]: Array<string>}; + public catalogItems:Array<Component>; + public catalogFilteredItems:Array<Component>; + public catalogFilteredSlicedItems:Array<Component>; + public expandedSection:Array<string>; + public version:string; + public sortBy:string; + public reverse:boolean; + public filterParams:IFilterParams; + public search:ISearchFilter; + + //this is for UI paging + public numberOfItemToDisplay:number; + + public selectedCatalogItem: ICatalogSelector; + public catalogSelectorItems: Array<ICatalogSelector>; + public showCatalogSelector: boolean; + + public typesChecklistModel: SdcUiCommon.ChecklistModel; + public categoriesChecklistModel: SdcUiCommon.ChecklistModel; + public statusChecklistModel: SdcUiCommon.ChecklistModel; + + private defaultFilterParams:IFilterParams = { + components: [], + categories: [], + statuses: [], + order: ['lastUpdateDate', true], + term: '', + active: true + }; + private categoriesMap:ICategoriesMap; + + constructor( + @Inject(SdcConfigToken) private sdcConfig:ISdcConfig, + @Inject(SdcMenuToken) public sdcMenu:IAppMenu, + @Inject("$state") private $state:ng.ui.IStateService, + private cacheService:CacheService, + private catalogService:CatalogService, + private resourceNamePipe:ResourceNamePipe, + private loaderService: SdcUiServices.LoaderService + ) {} + + ngOnInit(): void { + this.initGui(); + this.initLeftSwitch(); + this.initScopeMembers(); + this.loadFilterParams(); + this.initCatalogData(); // Async task to get catalog from server. + } + + private initLeftSwitch = ():void => { + this.showCatalogSelector = false; + + this.catalogSelectorItems = [ + {value: CatalogSelectorTypes.Active, title: "Active Items", header: "Active"}, + {value: CatalogSelectorTypes.Archive, title: "Archive", header: "Archived"} + ]; + // set active items is default + this.selectedCatalogItem = this.catalogSelectorItems[0]; + }; + + private initCatalogData = ():void => { + if(this.selectedCatalogItem.value === CatalogSelectorTypes.Archive){ + this.getArchiveCatalogItems(); + } else { + this.getActiveCatalogItems(); + } + }; + + + private initScopeMembers = ():void => { + this.numberOfItemToDisplay = 0; + this.categories = this.makeSortedCategories(this.cacheService.get('serviceCategories').concat(this.cacheService.get('resourceCategories'))) + .map((cat) => <IMainCategory>cat); + this.confStatus = this.sdcMenu.statuses; + this.expandedSection = ["type", "category", "status"]; + this.catalogItems = []; + this.search = {FilterTerm: ""}; + this.categoriesMap = this.initCategoriesMap(); + this.initCheckboxesFilter(); + this.initCheckboxesFilterKeys(); + this.buildCheckboxLists(); + + this.version = this.cacheService.get('version'); + this.sortBy = 'lastUpdateDate'; + this.reverse = true; + }; + + private buildCheckboxLists() { + this.buildChecklistModelForTypes(); + this.buildChecklistModelForCategories(); + this.buildChecklistModelForStatuses(); + } + + private getTestIdForCheckboxByText = ( text: string ):string => { + return 'checkbox-' + text.toLowerCase().replace(/ /g, ''); + } + + private buildChecklistModelForTypes() { + this.componentTypes = { + Resource: ['VF', 'VFC', 'CR', 'PNF', 'CP', 'VL'], + Service: null + }; + this.typesChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.componentTypes._main, + Object.keys(this.componentTypes).map((ct) => { + let subChecklist = null; + if (this.componentTypes[ct]) { + this.checkboxesFilterKeys.componentTypes[ct] = this.checkboxesFilterKeys.componentTypes[ct] || []; + subChecklist = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.componentTypes[ct], + this.componentTypes[ct].map((st) => { + const stKey = [ct, st].join('.'); + const testId = this.getTestIdForCheckboxByText(st); + return new SdcUiCommon.ChecklistItemModel(st, false, this.checkboxesFilterKeys.componentTypes[ct].indexOf(stKey) !== -1, null, testId, stKey); + }) + ); + } + const testId = this.getTestIdForCheckboxByText(ct); + return new SdcUiCommon.ChecklistItemModel(ct, false, this.checkboxesFilterKeys.componentTypes._main.indexOf(ct) !== -1, subChecklist, testId, ct); + }) + ); + } + + private buildChecklistModelForCategories() { + this.categoriesChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.categories._main, + (this.filteredCategories || this.categories).map((cat) => { + this.checkboxesFilterKeys.categories[cat.uniqueId] = this.checkboxesFilterKeys.categories[cat.uniqueId] || []; + const subCategoriesChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.categories[cat.uniqueId], + (cat.subcategories || []).map((scat) => { + this.checkboxesFilterKeys.categories[scat.uniqueId] = this.checkboxesFilterKeys.categories[scat.uniqueId] || []; + const groupingsChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.categories[scat.uniqueId], + (scat.groupings || []).map(gcat => + new SdcUiCommon.ChecklistItemModel(gcat.name, false, this.checkboxesFilterKeys.categories[scat.uniqueId].indexOf(gcat.uniqueId) !== -1, null, this.getTestIdForCheckboxByText(gcat.uniqueId), gcat.uniqueId)) + ); + return new SdcUiCommon.ChecklistItemModel(scat.name, false, this.checkboxesFilterKeys.categories[cat.uniqueId].indexOf(scat.uniqueId) !== -1, groupingsChecklistModel, this.getTestIdForCheckboxByText(scat.uniqueId), scat.uniqueId); + }) + ); + return new SdcUiCommon.ChecklistItemModel(cat.name, false, this.checkboxesFilterKeys.categories._main.indexOf(cat.uniqueId) !== -1, subCategoriesChecklistModel, this.getTestIdForCheckboxByText(cat.uniqueId), cat.uniqueId); + }) + ); + } + + private buildChecklistModelForStatuses() { + // For statuses checklist model, use the statuses keys as values. On applying filtering map the statuses keys to statuses values. + this.statusChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.statuses._main, + Object.keys(this.confStatus).map((sKey) => new SdcUiCommon.ChecklistItemModel( + this.confStatus[sKey].name, + false, + this.checkboxesFilterKeys.statuses._main.indexOf(sKey) !== -1, + null, + this.getTestIdForCheckboxByText(sKey), + sKey)) + ); + } + + private initCheckboxesFilter() { + // Checkboxes filter init + this.checkboxesFilter = <IEntityFilterObject>{}; + this.checkboxesFilter.selectedComponentTypes = []; + this.checkboxesFilter.selectedResourceSubTypes = []; + this.checkboxesFilter.selectedCategoriesModel = []; + this.checkboxesFilter.selectedStatuses = []; + } + + private initCheckboxesFilterKeys() { + // init checkboxes filter keys (for checklists values): + this.checkboxesFilterKeys = <ICheckboxesFilterKeys>{}; + this.checkboxesFilterKeys.componentTypes = { _main: [] }; + this.checkboxesFilterKeys.categories = { _main: [] }; + this.checkboxesFilterKeys.statuses = { _main: [] }; + } + + private initCategoriesMap(categoriesList?:(ICategoryBase)[], parentCategory:ICategoryBase=null): ICategoriesMap { + categoriesList = (categoriesList) ? categoriesList : this.categories; + + // Init categories map + return categoriesList.reduce((acc, cat) => { + acc[cat.uniqueId] = { + category: cat, + parent: parentCategory + }; + const catChildren = ((<IMainCategory>cat).subcategories) + ? (<IMainCategory>cat).subcategories + : (((<ISubCategory>cat).groupings) + ? (<ISubCategory>cat).groupings + : null); + if (catChildren) { + Object.assign(acc, this.initCategoriesMap(catChildren, cat)); + } + return acc; + }, <ICategoriesMap>{}); + } + + public selectLeftSwitchItem(item: ICatalogSelector): void { + if (this.selectedCatalogItem.value !== item.value) { + this.selectedCatalogItem = item; + switch (item.value) { + case CatalogSelectorTypes.Active: + this.getActiveCatalogItems(true); + break; + + case CatalogSelectorTypes.Archive: + this.getArchiveCatalogItems(true); + break; + } + this.changeFilterParams({active: (item.value === CatalogSelectorTypes.Active)}); + } + } + + public sectionClick(section: string): void { + let index: number = this.expandedSection.indexOf(section); + if (index !== -1) { + this.expandedSection.splice(index, 1); + } else { + this.expandedSection.push(section); + } + } + + + private makeFilterParamsFromCheckboxes(checklistModel:SdcUiCommon.ChecklistModel): Array<string> { + return checklistModel.checkboxes.reduce((acc, chbox) => { + if (checklistModel.selectedValues.indexOf(chbox.value) !== -1) { + acc.push(chbox.value); + } else if (chbox.subLevelChecklist) { // else, if checkbox is not checked, then try to get values from sub checklists + acc.push(...this.makeFilterParamsFromCheckboxes(chbox.subLevelChecklist)); + } + return acc; + }, []); + } + + //default sort by descending last update. default for alphabetical = ascending + public order(sortBy: string): void { + this.changeFilterParams({ + order: (this.filterParams.order[0] === sortBy) + ? [sortBy, !this.filterParams.order[1]] + : [sortBy, sortBy === 'lastUpdateDate'] + }); + } + + + public goToComponent(component: Component): void { + this.$state.go('workspace.general', {id: component.uniqueId, type: component.componentType.toLowerCase()}); + } + + + // Will print the number of elements found in catalog + public getNumOfElements(num:number):string { + if (!num || num === 0) { + return `No <b>${this.selectedCatalogItem.header}</b> Elements found`; + } else if (num === 1) { + return `1 <b>${this.selectedCatalogItem.header}</b> Element found`; + } else { + return num + ` <b>${this.selectedCatalogItem.header}</b> Elements found`; + } + } + + public initGui(): void { + this.gui = <Gui>{}; + + /** + * Select | unselect sub resource when resource is clicked | unclicked. + * @param type + */ + this.gui.onComponentTypeClick = (): void => { + this.changeFilterParams({ + components: this.makeFilterParamsFromCheckboxes(this.typesChecklistModel) + }); + }; + + this.gui.onCategoryClick = (): void => { + this.changeFilterParams({ + categories: this.makeFilterParamsFromCheckboxes(this.categoriesChecklistModel) + }); + }; + + this.gui.onStatusClick = (statusChecklistItem: SdcUiCommon.ChecklistItemModel) => { + this.changeFilterParams({ + statuses: this.makeFilterParamsFromCheckboxes(this.statusChecklistModel) + }); + }; + + this.gui.changeFilterTerm = (filterTerm: string) => { + this.changeFilterParams({ + term: filterTerm + }); + }; + } + + public raiseNumberOfElementToDisplay(recalculate:boolean = false): void { + const scrollPageAmount = 35; + if (!this.catalogFilteredItems) { + this.numberOfItemToDisplay = 0; + } else if (this.catalogFilteredItems.length > this.numberOfItemToDisplay || recalculate) { + let fullPagesAmount = Math.ceil(this.numberOfItemToDisplay / scrollPageAmount) * scrollPageAmount; + if (!recalculate || fullPagesAmount === 0) { //TODO trigger infiniteScroll to check bottom and fire onBottomHit by itself (sdc-ui) + fullPagesAmount += scrollPageAmount; + } + this.numberOfItemToDisplay = Math.min(this.catalogFilteredItems.length, fullPagesAmount); + this.catalogFilteredSlicedItems = this.catalogFilteredItems.slice(0, this.numberOfItemToDisplay); + } + } + + private isDefaultFilter = (): boolean => { + return angular.equals(this.defaultFilterParams, this.filterParams); + } + + private componentShouldReload = ():boolean => { + let breadcrumbsValid: boolean = (this.$state.current.name === this.cacheService.get('breadcrumbsComponentsState') && this.cacheService.contains('breadcrumbsComponents')); + return !breadcrumbsValid || this.isDefaultFilter(); + } + + private getActiveCatalogItems(forceReload?: boolean): void { + if (forceReload || this.componentShouldReload()) { + this.loaderService.activate(); + + let onSuccess = (followedResponse:Array<Component>):void => { + this.updateCatalogItems(followedResponse); + this.loaderService.deactivate(); + this.cacheService.set('breadcrumbsComponentsState', this.$state.current.name); //catalog + this.cacheService.set('breadcrumbsComponents', followedResponse); + + }; + + let onError = ():void => { + console.info('Failed to load catalog CatalogViewModel::getActiveCatalogItems'); + this.loaderService.deactivate(); + }; + this.catalogService.getCatalog().subscribe(onSuccess, onError); + } else { + let cachedComponents = this.cacheService.get('breadcrumbsComponents'); + this.updateCatalogItems(cachedComponents); + } + } + + private getArchiveCatalogItems(forceReload?: boolean): void { + if(forceReload || !this.cacheService.contains("archiveComponents")) { + this.loaderService.activate(); + let onSuccess = (followedResponse:Array<Component>):void => { + this.cacheService.set("archiveComponents", followedResponse); + this.loaderService.deactivate(); + this.updateCatalogItems(followedResponse); + }; + + let onError = ():void => { + console.info('Failed to load catalog CatalogViewModel::getArchiveCatalogItems'); + this.loaderService.deactivate(); + }; + + this.catalogService.getArchiveCatalog().subscribe(onSuccess, onError); + } else { + let archiveCache = this.cacheService.get("archiveComponents"); + this.updateCatalogItems(archiveCache); + } + } + + private updateCatalogItems = (items:Array<Component>):void => { + this.catalogItems = items; + this.catalogItems.forEach(this.addFilterTermToComponent); + this.filterCatalogItems(); + } + + private applyFilterParamsToView(filterParams:IFilterParams) { + // reset checkboxes filter + this.initCheckboxesFilter(); + + this.filterCatalogCategories(); + + this.applyFilterParamsComponents(filterParams); + this.applyFilterParamsCategories(filterParams); + this.applyFilterParamsStatuses(filterParams); + this.applyFilterParamsOrder(filterParams); + this.applyFilterParamsTerm(filterParams); + + // do filters when filter params are changed: + this.filterCatalogItems(); + } + + private filterCatalogCategories() { + this.filteredCategories = this.makeFilteredCategories(this.categories, this.checkboxesFilter.selectedComponentTypes); + this.buildChecklistModelForCategories(); + } + + private filterCatalogItems() { + this.catalogFilteredItems = this.makeFilteredItems(this.catalogItems, this.checkboxesFilter, this.search, this.sortBy, this.reverse); + this.raiseNumberOfElementToDisplay(true); + this.catalogFilteredSlicedItems = this.catalogFilteredItems.slice(0, this.numberOfItemToDisplay); + } + + private applyFilterParamsToCheckboxes(checklistModel:SdcUiCommon.ChecklistModel, filterParamsList:Array<string>) { + checklistModel.checkboxes.forEach((chbox) => { + // if checkbox is checked, then add it to selected values if not there, and select all sub checkboxes + if (filterParamsList.indexOf(chbox.value) !== -1 && checklistModel.selectedValues.indexOf(chbox.value) === -1) { + checklistModel.selectedValues.push(chbox.value); + if (chbox.subLevelChecklist) { + this.applyFilterParamsToCheckboxes(chbox.subLevelChecklist, chbox.subLevelChecklist.checkboxes.map((subchbox) => subchbox.value)); + } + } else if ( chbox.subLevelChecklist ) { + this.applyFilterParamsToCheckboxes(chbox.subLevelChecklist, filterParamsList); + } + }); + } + + private applyFilterParamsComponents(filterParams:IFilterParams) { + this.applyFilterParamsToCheckboxes(this.typesChecklistModel, filterParams.components); + this.checkboxesFilter.selectedComponentTypes = this.checkboxesFilterKeys.componentTypes._main; + Object.keys(this.checkboxesFilterKeys.componentTypes).forEach((chKey) => { + if (chKey !== '_main') { + this.checkboxesFilter['selected' + chKey + 'SubTypes'] = this.checkboxesFilterKeys.componentTypes[chKey].map((st) => st.substr(chKey.length + 1)); + } + }); + + let selectedCatalogIndex = filterParams.active ? CatalogSelectorTypes.Active : CatalogSelectorTypes.Archive; + this.selectedCatalogItem = this.catalogSelectorItems[selectedCatalogIndex]; + } + + private applyFilterParamsCategories(filterParams:IFilterParams) { + this.applyFilterParamsToCheckboxes(this.categoriesChecklistModel, filterParams.categories); + this.checkboxesFilter.selectedCategoriesModel = <Array<string>>_.flatMap(this.checkboxesFilterKeys.categories); + } + + private applyFilterParamsStatuses(filterParams: IFilterParams) { + this.applyFilterParamsToCheckboxes(this.statusChecklistModel, filterParams.statuses); + this.checkboxesFilter.selectedStatuses = _.reduce(_.flatMap(this.checkboxesFilterKeys.statuses), (stats, st:string) => [...stats, ...this.confStatus[st].values], []); + } + + private applyFilterParamsOrder(filterParams: IFilterParams) { + this.sortBy = filterParams.order[0]; + this.reverse = filterParams.order[1]; + } + + private applyFilterParamsTerm(filterParams: IFilterParams) { + this.search = { + filterTerm: filterParams.term + }; + } + + private loadFilterParams() { + const params = this.$state.params; + this.filterParams = angular.copy(this.defaultFilterParams); + Object.keys(params).forEach((k) => { + if (!angular.isUndefined(params[k])) { + let newVal; + let paramsChecklist: SdcUiCommon.ChecklistModel = null; + let filterKey = k.substr('filter.'.length); + switch (k) { + case 'filter.components': + paramsChecklist = paramsChecklist || this.typesChecklistModel; + case 'filter.categories': + paramsChecklist = paramsChecklist || this.categoriesChecklistModel; + case 'filter.statuses': + paramsChecklist = paramsChecklist || this.statusChecklistModel; + + // for those cases above - split param by comma and make reduced checklist values for filter params (url) + newVal = _.uniq(params[k].split(',')); + break; + case 'filter.order': + newVal = params[k].startsWith('-') ? [params[k].substr(1), true] : [params[k], false]; + break; + case 'filter.term': + newVal = params[k]; + break; + case 'filter.active': + newVal = (params[k] === "true" || params[k] === true)? true : false; + break; + default: + // unknown filter key + filterKey = null; + } + if (filterKey) { + this.filterParams[filterKey] = newVal; + } + } + }); + // re-set filter params with valid values, and then re-build checklists + this.changeFilterParams(this.filterParams, true); + } + + private changeFilterParams(changedFilterParams, rebuild:boolean = false) { + const newParams = {}; + Object.keys(changedFilterParams).forEach((k) => { + let newVal; + switch (k) { + case 'components': + case 'categories': + case 'statuses': + newVal = changedFilterParams[k] && changedFilterParams[k].length ? changedFilterParams[k].join(',') : null; + break; + case 'order': + newVal = (changedFilterParams[k][1] ? '-' : '') + changedFilterParams[k][0]; + break; + case 'term': + newVal = changedFilterParams[k] ? changedFilterParams[k] : null; + break; + case 'active': + newVal = (changedFilterParams[k] === "true" || changedFilterParams[k] === true); + break; + default: + return; + } + this.filterParams[k] = changedFilterParams[k]; + newParams['filter.' + k] = newVal; + }); + this.$state.go('.', newParams, {location: 'replace', notify: false}).then(() => { + if (rebuild) { + // fix the filter params to only valid values for checkboxes + this.changeFilterParams({ + components: this.makeFilterParamsFromCheckboxes(this.typesChecklistModel), + categories: this.makeFilterParamsFromCheckboxes(this.categoriesChecklistModel), + statuses: this.makeFilterParamsFromCheckboxes(this.statusChecklistModel) + }); + // rebuild the checkboxes to show selected + this.buildCheckboxLists(); + } + }); + this.applyFilterParamsToView(this.filterParams); + } + + private makeFilteredCategories(categories:Array<IMainCategory>, selectedTypes:Array<string>=[]): Array<IMainCategory> { + let filteredCategories = categories.slice(); + + const filteredMainTypes = selectedTypes.reduce((acc, st) => { + const mainType = st.split('.')[0]; + if (acc.indexOf(mainType) === -1) { + acc.push(mainType); + } + return acc; + }, []); + + // filter by selected types + if (filteredMainTypes.length) { + const filteredTypesCategories = filteredMainTypes.reduce((acc, mainType: string) => { + acc.push(...this.cacheService.get(mainType.toLowerCase() + 'Categories')); + return acc; + }, []); + + filteredCategories = _.intersectionBy(filteredCategories, filteredTypesCategories, c => c.uniqueId); + } + + return filteredCategories; + } + + private makeSortedCategories(categories:Array<IMainCategory|ISubCategory|ICategoryBase>, sortBy?:any): Array<IMainCategory|ISubCategory|ICategoryBase> { + sortBy = (sortBy !== undefined) ? sortBy : ['name']; + let sortedCategories = categories.map(cat => Object.assign({}, cat)); // copy each object in the array + sortedCategories = _.sortBy(sortedCategories, sortBy); + + // inner sort of subcategories and groupings + sortedCategories.forEach((cat) => { + if ('subcategories' in cat && cat['subcategories'] && cat['subcategories'].length > 0) { + cat['subcategories'] = this.makeSortedCategories(cat['subcategories'], sortBy); + } + if ('groupings' in cat && cat['groupings'] && cat['groupings'].length > 0) { + cat['groupings'] = this.makeSortedCategories(cat['groupings'], sortBy); + } + }); + + return sortedCategories; + } + + private addFilterTermToComponent(component:Component) { + component.filterTerm = component.name + ' ' + component.description + ' ' + component.tags.toString() + ' ' + component.version; + component.filterTerm = component.filterTerm.toLowerCase(); + } + + private makeFilteredItems(catalogItems:Array<Component>, filter:IEntityFilterObject, search:ISearchFilter, sortBy:string, reverse:boolean) { + let filteredComponents:Array<Component> = catalogItems; + + // common entity filter + // -------------------------------------------------------------------------- + filter = Object.assign({ search }, filter); // add search to entity filter object + filteredComponents = EntityFilterPipe.transform(filteredComponents, filter); + + // sort + // -------------------------------------------------------------------------- + if (sortBy) { + switch (sortBy) { + case 'resourceName': + filteredComponents = _.sortBy(filteredComponents, cat => this.resourceNamePipe.transform(cat.name)); + break; + default: + filteredComponents = _.sortBy(filteredComponents, [sortBy]); + } + if (reverse) { + _.reverse(filteredComponents); + } + } + + return filteredComponents; + } +} @@ -0,0 +1,33 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { CatalogComponent } from "./catalog.component"; +import { LayoutModule } from "../../components/layout/layout.module"; +import { UiElementsModule } from "../../components/ui/ui-elements.module"; +import { GlobalPipesModule } from "../../pipes/global-pipes.module"; +import { TranslateModule } from "../../shared/translator/translate.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import {SdcTileModule} from "../../components/ui/tile/sdc-tile.module"; + +@NgModule({ + declarations: [ + CatalogComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + LayoutModule, + UiElementsModule, + GlobalPipesModule, + TranslateModule, + SdcTileModule + ], + exports: [ + CatalogComponent + ], + entryComponents: [ + CatalogComponent + ], + providers: [] +}) +export class CatalogModule { +} @@ -0,0 +1,64 @@ +import {Injectable} from "@angular/core"; +import 'rxjs/add/observable/forkJoin'; +import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance"; +import {SelectedComponentType} from "./store/graph.actions"; +import {RelationshipModel} from "../../../../models/graph/relationship"; + +@Injectable() +export class CommonGraphDataService { + + public componentInstances: Array<ComponentInstance>; + public componentInstancesRelations: RelationshipModel[]; + public selectedComponentType: SelectedComponentType; + + constructor() { + } + + //------------------------ RELATIONS ---------------------------------// + public setRelations = (componentInstancesRelations: RelationshipModel[]) => { + this.componentInstancesRelations = this.componentInstancesRelations; + } + + public getRelations = (): RelationshipModel[] => { + return this.componentInstancesRelations; + } + + public addRelation = (componentInstancesRelations: RelationshipModel) => { + this.componentInstancesRelations.push(componentInstancesRelations); + } + + public deleteRelation(relationToDelete: RelationshipModel) { + this.componentInstancesRelations = _.filter(this.componentInstancesRelations, (relationship: RelationshipModel) => { + return relationship.relationships[0].relation.id !== relationToDelete.relationships[0].relation.id; + }); + } + + //---------------------------- COMPONENT INSTANCES ------------------------------------// + public getComponentInstances = (): Array<ComponentInstance> => { + return this.componentInstances; + } + + public addComponentInstance = (instance: ComponentInstance) => { + return this.componentInstances.push(instance); + } + + public updateComponentInstances = (componentInstances: ComponentInstance[]) => { + _.unionBy(this.componentInstances, componentInstances, 'uniqueId'); + } + + public updateInstance = (instance: ComponentInstance) => { + this.componentInstances = this.componentInstances.map(componentInstance => instance.uniqueId === componentInstance.uniqueId? instance : componentInstance); + } + + public deleteComponentInstance(instanceToDelete: string) { + this.componentInstances = _.filter(this.componentInstances, (instance: ComponentInstance) => { + return instance.uniqueId !== instanceToDelete; + }); + } + + //----------------------------SELECTED COMPONENT -----------------------// + + public setSelectedComponentType = (selectedType: SelectedComponentType) => { + this.selectedComponentType = selectedType; + } +} @@ -0,0 +1,33 @@ +export enum SelectedComponentType { + COMPONENT_INSTANCE = "COMPONENT_INSTANCE", + GROUP = "GROUP", + POLICY = "POLICY", + TOPOLOGY_TEMPLATE = "TOPOLOGY_TEMPLATE" +} + +export class UpdateSelectedComponentAction { + static readonly type = '[COMPOSITION] UpdateSelectedComponent'; + + constructor(public payload: {uniqueId?: string, type?: string}) { + } +} + +export class SetSelectedComponentAction { + static readonly type = '[COMPOSITION] SetSelectedComponent'; + + constructor(public payload: {component?: any, type?: SelectedComponentType}) { + } +} + +export class OnSidebarOpenOrCloseAction { + static readonly type = '[COMPOSITION] OnSidebarOpenOrCloseAction'; + + constructor() { + } +} + +export class TogglePanelLoadingAction { + static readonly type = '[COMPOSITION] TogglePanelLoading'; + constructor(public payload: { isLoading: boolean}) { + } +} @@ -0,0 +1,170 @@ +import { Action, Selector, State, StateContext} from '@ngxs/store'; +import { + OnSidebarOpenOrCloseAction, + SelectedComponentType, + SetSelectedComponentAction, + TogglePanelLoadingAction +} from "./graph.actions"; +import { PolicyInstance, GroupInstance, Component as TopologyTemplate, ComponentInstance, LeftPaletteComponent, FullComponentInstance} from "app/models"; +import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service"; +import { tap } from "rxjs/operators"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import {GroupsService} from "../../../../services/groups.service"; +import {PoliciesService} from "../../../../services/policies.service"; +import {WorkspaceService} from "../../../workspace/workspace.service"; + +export class CompositionStateModel { + + isViewOnly?: boolean; + panelLoading?: boolean; + selectedComponentType?: SelectedComponentType; + selectedComponent?: PolicyInstance | GroupInstance | TopologyTemplate | ComponentInstance; + withSidebar?: boolean; +} + +@State<CompositionStateModel>({ + name: 'composition', + defaults: { + withSidebar: true + } +}) +export class GraphState { + + constructor(private topologyTemplateService: TopologyTemplateService, + private compositionService: CompositionService, + private policiesService:PoliciesService, private groupsService:GroupsService, + private workspaceService: WorkspaceService) {} + + @Action(SetSelectedComponentAction) + setSelectedComponent({dispatch, getState, patchState}:StateContext<CompositionStateModel>, action: SetSelectedComponentAction) { + + const state:CompositionStateModel = getState(); + + patchState({ panelLoading: true }); + + if(action.payload.component instanceof ComponentInstance){ + let originComponent = this.compositionService.getOriginComponentById(action.payload.component.getComponentUid()); + if(!originComponent) { + return this.topologyTemplateService.getFullComponent(action.payload.component.originType, action.payload.component.getComponentUid()) + .pipe(tap(resp => { + this.compositionService.addOriginComponent(resp); + this.compositionService.setSelectedComponentType(SelectedComponentType.COMPONENT_INSTANCE); + patchState({ + selectedComponent: new FullComponentInstance(action.payload.component, resp), + selectedComponentType: action.payload.type, + panelLoading: false + }); + }, err => { + patchState({ + panelLoading: false + }) + } + )); + } else { + patchState({ + selectedComponent: new FullComponentInstance(action.payload.component, originComponent), + selectedComponentType: action.payload.type, + panelLoading: false + }); + } + } else if (action.payload.component instanceof PolicyInstance) { + let topologyTemplate = this.workspaceService.metadata; + return this.policiesService.getSpecificPolicy(topologyTemplate.componentType, topologyTemplate.uniqueId, action.payload.component.uniqueId).pipe(tap(resp => + { + this.compositionService.updatePolicy(resp); + patchState({ + selectedComponent: resp, + selectedComponentType: action.payload.type, + panelLoading: false + }) + }, err => { + patchState({ + panelLoading: false + }) + } + )); + + } else if (action.payload.component instanceof GroupInstance) { + let topologyTemplate = this.workspaceService.metadata; + return this.groupsService.getSpecificGroup(topologyTemplate.componentType, topologyTemplate.uniqueId, action.payload.component.uniqueId).pipe(tap(resp => { + this.compositionService.updateGroup(resp); + patchState({ + selectedComponent: resp, + selectedComponentType: action.payload.type, + panelLoading: false + }); + }, err => { + patchState({ + panelLoading: false + }) + } + )); + } else { //TopologyTemplate + patchState({ + selectedComponent: action.payload.component, + selectedComponentType: action.payload.type, + panelLoading: false + }) + } + } + + + // @Action(UpdateSelectedComponentNameAction) + // UpdateSelectedComponentNameAction({patchState}:StateContext<CompositionStateModel>, action: UpdateSelectedComponentNameAction) { + + // switch(action.payload.type){ + // case SelectedComponentType.COMPONENT_INSTANCE: + // this.store.dispatch(new UpdateComponentInstancesAction([action.payload.component])); + // break; + // case SelectedComponentType.POLICY: + // this.store.dispatch(new UpdatePolicyNameAction(action.payload.uniqueId, action.payload.newName)); + // break; + // case SelectedComponentType.GROUP: + // this.store.dispatch(new UpdateGroupInstancesAction) + + // } + // if(action.payload.type === SelectedComponentType.COMPONENT_INSTANCE){ + + // } + + // } + + @Selector() + static getSelectedComponent(state:CompositionStateModel) { + return state.selectedComponent; + } + + @Selector() + static getSelectedComponentId(state:CompositionStateModel) { + return state.selectedComponent.uniqueId; + } + + @Selector() + static getSelectedComponentType(state:CompositionStateModel) { + return state.selectedComponentType; + } + + + @Action(OnSidebarOpenOrCloseAction) + onSidebarOpenOrCloseAction({getState, setState}:StateContext<CompositionStateModel>) { + const state:CompositionStateModel = getState(); + + setState({ + ...state, + withSidebar: !state.withSidebar + }); + } + + @Action(TogglePanelLoadingAction) + TogglePanelLoading({patchState}:StateContext<CompositionStateModel>, action: TogglePanelLoadingAction) { + + patchState({ + panelLoading: action.payload.isLoading + }); + } + + @Selector() static withSidebar(state):boolean { + return state.withSidebar; + } + +}
\ No newline at end of file @@ -0,0 +1,8 @@ +<div class="workspace-composition-page"> + <div class="composition-graph"> + <composition-palette></composition-palette> + <app-palette-popup-panel></app-palette-popup-panel> + <composition-graph dndDropzone [dndAllowExternal]=true [topologyTemplate]="topologyTemplate" [testId]="'canvas'"></composition-graph> + <ng2-composition-panel [topologyTemplate]="topologyTemplate"></ng2-composition-panel> + </div> +</div> @@ -0,0 +1,26 @@ +@import "./../../../../assets/styles/override"; +.workspace-composition-page { + height:100%; + display: block; + text-align: left; + align-items: left; + padding: 0; + + .composition-graph { + height:100%; + background-color: @sdcui_color_white; + bottom: 0; + display:flex; + flex-direction: row; + + .view-mode{ + background-color: #f8f8f8; + border:0; + } + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} @@ -0,0 +1,47 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Component as TopologyTemplate } from 'app/models'; +import * as Constants from 'constants'; +import { EventListenerService } from '../../../services/event-listener-service'; +import { EVENTS } from '../../../utils'; + +@Component({ + templateUrl: './composition-page.component.html', + styleUrls: ['composition-page.component.less'] +}) +export class CompositionPageComponent implements OnInit, OnDestroy { + + private topologyTemplate: TopologyTemplate; + + constructor(@Inject('$stateParams') private stateParams, private eventListenerService: EventListenerService) { + this.topologyTemplate = stateParams.component; + } + + ngOnInit(): void { + this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, (comp) => { + this.topologyTemplate = comp; + }); + } + + ngOnDestroy(): void { + this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT); + } +} @@ -0,0 +1,30 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {CompositionGraphModule} from "./graph/composition-graph.module"; +import {CompositionPageComponent} from "./composition-page.component"; +import {NgxsModule} from "@ngxs/store"; +import {PaletteModule} from "./palette/palette.module"; +import {PalettePopupPanelComponent} from "./palette/palette-popup-panel/palette-popup-panel.component"; +import { CompositionPanelModule } from "app/ng2/pages/composition/panel/composition-panel.module"; +import {CompositionService} from "./composition.service"; +import {DndModule} from "ngx-drag-drop"; +import {GraphState} from "./common/store/graph.state"; + +@NgModule({ + declarations: [CompositionPageComponent, PalettePopupPanelComponent], + imports: [CommonModule, + CompositionGraphModule, + CompositionPanelModule, + PaletteModule, + DndModule, + NgxsModule.forFeature([ + GraphState])], + exports: [CompositionPageComponent], + entryComponents: [CompositionPageComponent], + providers: [CompositionService] +}) +export class CompositionPageModule { +}
\ No newline at end of file @@ -0,0 +1,59 @@ +import {Injectable} from "@angular/core"; +import 'rxjs/add/observable/forkJoin'; +import {Component, PropertiesGroup, AttributesGroup, PolicyInstance} from "app/models"; +import {GroupInstance} from "app/models/graph/zones/group-instance"; +import {CommonGraphDataService} from "./common/common-graph-data.service"; +import {ForwardingPath} from "../../../models/forwarding-path"; +import {SelectedComponentType} from "./common/store/graph.actions"; + +@Injectable() +export class CompositionService extends CommonGraphDataService{ + + public originComponents: Array<Component>; //This contains the full data set after specifically requesting it. The uniqueId matches the 'componentUid' in the componentInstances array + public componentInstancesProperties:PropertiesGroup; + public componentInstancesAttributes:AttributesGroup; + public groupInstances: GroupInstance[]; + public policies: PolicyInstance[]; + public forwardingPaths: { [key:string]:ForwardingPath }; + public selectedComponentType: SelectedComponentType; + + //---------------------------- COMPONENT INSTANCES ------------------------------------// + + public getOriginComponentById = (uniqueId:string):Component => { + return this.originComponents && this.originComponents.find(instance => instance.uniqueId === uniqueId); + } + + public addOriginComponent = (originComponent:Component) => { + if(!this.originComponents) this.originComponents = []; + if(!this.getOriginComponentById(originComponent.uniqueId)){ + this.originComponents.push(originComponent); + } + } + + + public updateGroup = (instance: GroupInstance) => { + this.groupInstances = this.groupInstances.map(group => instance.uniqueId === group.uniqueId? instance : group); + } + + public updatePolicy = (instance: PolicyInstance) => { + this.policies = this.policies.map(policy => instance.uniqueId === policy.uniqueId? instance : policy); + } + + //---------------------------- POLICIES---------------------------------// + public addPolicyInstance = (instance: PolicyInstance) => { + return this.policies.push(instance); + } + + + //---------------------------- POLICIES---------------------------------// + public addGroupInstance = (instance: GroupInstance) => { + return this.groupInstances.push(instance); + } + + + //----------------------------SELECTED COMPONENT -----------------------// + + public setSelectedComponentType = (selectedType: SelectedComponentType) => { + this.selectedComponentType = selectedType; + } +} @@ -0,0 +1 @@ +<div class="sdc-deployment-graph-wrapper"></div> @@ -0,0 +1,13 @@ +.sdc-deployment-graph-wrapper { + height: 100%; + width: 100%; + + ::ng-deep canvas { + /*canvas z-index is initialized to 999 while top-nav z-Index is 10, which makes top-nav disappear, so z-Index must be overwritten here*/ + z-index: 10 !important; + } + } + +::ng-deep .sdc-workspace-container .w-sdc-main-right-container .w-sdc-main-container-body-content.deploy-body-content{ + padding: 0px; +}
\ No newline at end of file @@ -0,0 +1,92 @@ +import {async, ComponentFixture} from '@angular/core/testing'; +import 'jest-dom/extend-expect'; +import {DeploymentGraphComponent} from "./deployment-graph.component"; +import {DeploymentGraphService} from "./deployment-graph.service"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import * as cytoscape from "cytoscape/dist/cytoscape" +import {AngularJSBridge} from "../../../../services/angular-js-bridge-service"; +import {NodesFactory} from "../../../../models/graph/nodes/nodes-factory"; +import {CommonGraphUtils} from "../graph/common/common-graph-utils"; +import {groupsMock} from "../../../../../jest/mocks/groups.mock"; +import {Module} from "../../../../models/modules/base-module"; +import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance"; +import {componentInstancesMock} from "../../../../../jest/mocks/component-instance.mock"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../../workspace/workspace.service"; +import {SdcConfigToken} from "../../../config/sdc-config.config"; +import {CompositionGraphLinkUtils} from "../graph/utils"; + +describe('DeploymentGraphComponent', () => { + + let fixture: ComponentFixture<DeploymentGraphComponent>; + let deploymentGraphServiceMock: Partial<DeploymentGraphService>; + let nodeFactoryServiceMock: Partial<NodesFactory>; + let commonGraphUtilsServiceMock: Partial<CommonGraphUtils>; + let angularJsBridgeServiceMock: Partial<AngularJSBridge>; + let sdcConfigTokenMock: Partial<AngularJSBridge>; + + beforeEach( + async(() => { + + deploymentGraphServiceMock = { + modules: <Array<Module>>groupsMock, + componentInstances: <Array<ComponentInstance>>componentInstancesMock + } + + nodeFactoryServiceMock = { + createModuleNode: jest.fn().mockResolvedValue(() => { + }), + createNode: jest.fn().mockResolvedValue(() => { + }) + } + + commonGraphUtilsServiceMock = { + addNodeToGraph: jest.fn(), + addComponentInstanceNodeToGraph: jest.fn() + } + + sdcConfigTokenMock = { + imagePath: '' + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [DeploymentGraphComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DeploymentGraphService, useValue: deploymentGraphServiceMock}, + {provide: NodesFactory, useValue: nodeFactoryServiceMock}, + {provide: TopologyTemplateService, useValue: {}}, + {provide: WorkspaceService, useValue: {}}, + {provide: CommonGraphUtils, useValue: commonGraphUtilsServiceMock}, + {provide: CompositionGraphLinkUtils, useValue: {}}, + {provide: AngularJSBridge, useValue: angularJsBridgeServiceMock}, + {provide: SdcConfigToken, useValue: SdcConfigToken} + ] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(DeploymentGraphComponent); + }); + }) + ); + + it('expected deployment graph component to be defined', () => { + expect(fixture).toBeDefined(); + }); + + + it('expected to addNodeToGraph to haveBeenCalled 6 times out of 7 cause one of the instances have no parent module', () => { + fixture.componentInstance._cy = cytoscape({ + zoomingEnabled: false, + selectionType: 'single', + }); + jest.spyOn(fixture.componentInstance, 'findInstanceModule'); + fixture.componentInstance.initGraphComponentInstances(); + expect(fixture.componentInstance.findInstanceModule).toHaveBeenCalledTimes(7); + expect(commonGraphUtilsServiceMock.addComponentInstanceNodeToGraph).toHaveBeenCalledTimes(6); + }); + +});
\ No newline at end of file @@ -0,0 +1,127 @@ +import {Component, ElementRef, Inject, OnInit} from "@angular/core"; +import {DeploymentGraphService} from "./deployment-graph.service"; +import '@bardit/cytoscape-expand-collapse'; +import * as _ from "lodash"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../../workspace/workspace.service"; +import {NodesFactory} from "../../../../models/graph/nodes/nodes-factory"; +import {CommonGraphUtils} from "../graph/common/common-graph-utils"; +import {ISdcConfig, SdcConfigToken} from "../../../config/sdc-config.config"; +import {Module} from "../../../../models/modules/base-module"; +import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance"; +import {ComponentGenericResponse} from "../../../services/responses/component-generic-response"; +import {ComponentInstanceFactory} from "../../../../utils/component-instance-factory"; +import {ModulesNodesStyle} from "../graph/common/style/module-node-style"; +import {ComponentInstanceNodesStyle} from "../graph/common/style/component-instances-nodes-style"; +import {CompositionGraphLinkUtils} from "../graph/utils/composition-graph-links-utils"; + +@Component({ + selector: 'deployment-graph', + templateUrl: './deployment-graph.component.html', + styleUrls: ['./deployment-graph.component.less'] +}) + +export class DeploymentGraphComponent implements OnInit { + constructor(private elRef: ElementRef, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private deploymentService: DeploymentGraphService, + private commonGraphUtils: CommonGraphUtils, + private nodeFactory: NodesFactory, + private commonGraphLinkUtils: CompositionGraphLinkUtils, + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig) { + + } + + public _cy: Cy.Instance; + + ngOnInit(): void { + this.topologyTemplateService.getDeploymentGraphData(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response: ComponentGenericResponse) => { + this.deploymentService.componentInstances = response.componentInstances; + this.deploymentService.componentInstancesRelations = response.componentInstancesRelations; + this.deploymentService.modules = response.modules; + this.loadGraph(); + }); + } + + public findInstanceModule = (groupsArray: Array<Module>, componentInstanceId: string): string => { + let parentGroup: Module = _.find(groupsArray, (group: Module) => { + return _.find(_.values(group.members), (member: string) => { + return member === componentInstanceId; + }); + }); + return parentGroup ? parentGroup.uniqueId : ""; + }; + + public initGraphModules = () => { + if (this.deploymentService.modules) { // Init module nodes + _.each(this.deploymentService.modules, (groupModule: Module) => { + let moduleNode = this.nodeFactory.createModuleNode(groupModule); + this.commonGraphUtils.addNodeToGraph(this._cy, moduleNode); + }); + } + } + + public initGraphComponentInstances = () => { + _.each(this.deploymentService.componentInstances, (instance: ComponentInstance) => { // Init component instance nodes + let componentInstanceNode = this.nodeFactory.createNode(instance); + componentInstanceNode.parent = this.findInstanceModule(this.deploymentService.modules, instance.uniqueId); + if (componentInstanceNode.parent) { // we are not drawing instances that are not a part of a module + this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, componentInstanceNode); + } + }); + } + + public handleEmptyModule = () => { + // This is a special functionality to pass the cytoscape default behavior - we can't create Parent module node without children's + // so we must add an empty dummy child node + _.each(this._cy.nodes('[?isGroup]'), (moduleNode: Cy.CollectionFirstNode) => { + if (!moduleNode.isParent()) { + let dummyInstance = ComponentInstanceFactory.createEmptyComponentInstance(); + let componentInstanceNode = this.nodeFactory.createNode(dummyInstance); + componentInstanceNode.parent = moduleNode.id(); + let dummyNode = this.commonGraphUtils.addNodeToGraph(this._cy, componentInstanceNode, moduleNode.position()); + dummyNode.addClass('dummy-node'); + } + }) + } + + public initGraphNodes = (): void => { + this.initGraphModules(); + this.initGraphComponentInstances(); + this.handleEmptyModule(); + }; + + private loadGraph = () => { + + let graphEl = this.elRef.nativeElement.querySelector('.sdc-deployment-graph-wrapper'); + this._cy = cytoscape({ + container: graphEl, + style: ComponentInstanceNodesStyle.getCompositionGraphStyle().concat(ModulesNodesStyle.getModuleGraphStyle()), + zoomingEnabled: false, + selectionType: 'single', + + }); + + //adding expand collapse extension + this._cy.expandCollapse({ + layoutBy: { + name: "grid", + animate: true, + randomize: false, + fit: true + }, + fisheye: false, + undoable: false, + expandCollapseCueSize: 18, + expandCueImage: this.sdcConfig.imagesPath + '/assets/styles/images/resource-icons/' + 'closeModule.png', + collapseCueImage: this.sdcConfig.imagesPath + '/assets/styles/images/resource-icons/' + 'openModule.png', + expandCollapseCueSensitivity: 2, + cueOffset: -20 + }); + + this.initGraphNodes(); //creating instances nodes + this.commonGraphLinkUtils.initGraphLinks(this._cy, this.deploymentService.componentInstancesRelations); + this._cy.collapseAll(); + }; +}
\ No newline at end of file @@ -0,0 +1,15 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {DeploymentGraphComponent} from "./deployment-graph.component"; + +@NgModule({ + declarations: [DeploymentGraphComponent], + imports: [CommonModule], + exports: [DeploymentGraphComponent], + entryComponents: [DeploymentGraphComponent], + providers: [ + + ] +}) +export class DeploymentGraphModule { +}
\ No newline at end of file @@ -0,0 +1,8 @@ +import {Injectable} from "@angular/core"; +import 'rxjs/add/observable/forkJoin'; +import {CommonGraphDataService} from "../common/common-graph-data.service"; +import {Module} from "../../../../models/modules/base-module"; +@Injectable() +export class DeploymentGraphService extends CommonGraphDataService { + public modules:Array<Module>; +} @@ -0,0 +1,23 @@ +<div class="canvas-search-component" [ngClass]="{'results-shown': autoCompleteResults.length}" + [class.canvas-search-visible]="autoCompleteValues && autoCompleteValues.length" [attr.data-tests-id]="testId"> + <div class="canvas-search-bar-container" [attr.data-tests-id]="testId" + [class.active]="searchQuery && searchQuery.length"> + <sdc-search-bar class="canvas-search-bar" + [placeHolder]="placeholder" + (onSearchClicked)="onSearchClicked($event)" + [size]="'medium'" + [value]="searchQuery" + (valueChange)="onSearchQueryChanged($event)"> + </sdc-search-bar> + <svg-icon class="canvas-clear-search" + [name]="'close'" + [clickable]="true" + [mode]="'secondary'" + [size]="'small'" + (click)="onClearSearch()"> + </svg-icon> + </div> + <dropdown-results *ngIf="autoCompleteResults && autoCompleteResults.length" [options]="autoCompleteResults" + (onItemSelected)="onItemSelected($event)"></dropdown-results> +</div> + @@ -0,0 +1,42 @@ +.canvas-search-component { + + .canvas-search-bar-container { + display:flex; + border-radius: 4px; + align-items: center; + box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29); + + /deep/.sdc-search-bar .search-bar-container .search-button { + border: solid 1px #d2d2d2; + } + + /deep/.sdc-input__input { + width: 250px; + transition: all 0.4s; + } + + .canvas-clear-search { + position: absolute; + right: 45px; + } + } + + &:not(:hover):not(.canvas-search-visible):not(.active) { + border-radius: 0; + box-shadow: none; + + /deep/.sdc-input__input:not(:focus) { + border: none; + padding: 0px; + width: 0px; + } + .canvas-clear-search { + display: none; + } + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +}
\ No newline at end of file @@ -0,0 +1,25 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {AutoCompleteComponent} from "onap-ui-angular/dist/autocomplete/autocomplete.component"; + +@Component({ + selector: 'canvas-search', + templateUrl: './canvas-search.component.html', + styleUrls: ['./canvas-search.component.less'] +}) +export class CanvasSearchComponent extends AutoCompleteComponent { + + @Output() public searchButtonClicked: EventEmitter<string> = new EventEmitter<string>(); + @Output() public onSelectedItem: EventEmitter<string> = new EventEmitter<string>(); + + public onSearchClicked = (searchText:string)=> { + this.searchButtonClicked.emit(searchText); + } + + public onItemSelected = (selectedItem) => { + this.searchQuery = selectedItem.value; + this.autoCompleteResults = []; + this.searchButtonClicked.emit(this.searchQuery); + this.onSelectedItem.emit(selectedItem); + } + +} @@ -0,0 +1,30 @@ +import {SdcUiComponentsModule} from "onap-ui-angular"; +import { NgModule } from "@angular/core"; +import {CanvasSearchComponent} from "./canvas-search.component"; +import {CommonModule} from "@angular/common"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {HttpClientModule} from "@angular/common/http"; +import {BrowserModule} from "@angular/platform-browser"; +import {AutocompletePipe} from "onap-ui-angular/dist/autocomplete/autocomplete.pipe"; + +@NgModule({ + declarations: [ + CanvasSearchComponent + ], + imports: [ + CommonModule, + BrowserModule, + HttpClientModule, + BrowserAnimationsModule, + SdcUiComponentsModule, + ], + exports: [ + CanvasSearchComponent + ], + entryComponents: [ + CanvasSearchComponent + ], + providers: [AutocompletePipe] +}) +export class CanvasSearchModule { +} @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ZoneContainerComponent should match current snapshot of palette element component 1`] = ` +<zone-container + backgroundClick={[Function EventEmitter]} + backgroundClicked={[Function Function]} + minimize={[Function EventEmitter]} + unminifyZone={[Function Function]} +> + <div> + <div + class="sdc-canvas-zone__header" + > + <div + class="sdc-canvas-zone__title" + > + + <span + class="sdc-canvas-zone__counter" + > + + </span> + </div> + <span + class="sdc-canvas-zone__state-button" + > + – + </span> + </div> + <div + class="sdc-canvas-zone__container" + /> + </div> +</zone-container> +`; @@ -15,14 +15,16 @@ --> -<div class="sdc-canvas-zone {{class}}-zone" [class.minimized]="minimized" [class.hidden]="!visible" (click)="backgroundClicked()"> - <div class="sdc-canvas-zone__header" (click)="unminifyZone(); $event.stopPropagation();" > - <div class="sdc-canvas-zone__title">{{title}} +<div class="sdc-canvas-zone {{class}}-zone" [class.minimized]="minimized" [class.hidden]="!visible" + (click)="backgroundClicked()"> + <div class="sdc-canvas-zone__header" (click)="unminifyZone(); $event.stopPropagation();"> + <div class="sdc-canvas-zone__title">{{title}} <span class="sdc-canvas-zone__counter">{{count}}</span> </div> <span class="sdc-canvas-zone__state-button">–</span> </div> - <div class="sdc-canvas-zone__container" #scrollDiv > + <div class="sdc-canvas-zone__container" #scrollDiv> <ng-content></ng-content> </div> -</div>
\ No newline at end of file +</div> + @@ -3,12 +3,12 @@ max-height:186px; display:flex; flex-direction:column; - align-self: flex-end; color:white; font-family:OpenSans-Regular, sans-serif; transition: width .2s ease-in-out, max-height .2s ease-in-out .1s; position:relative; bottom:0px; + margin-right: 5px; .sdc-canvas-zone__header { background: #5A5A5A; @@ -0,0 +1,46 @@ +import {async, ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {ConfigureFn, configureTests} from '../../../../../../jest/test-config.helper'; +import 'jest-dom/extend-expect'; +import {ZoneInstanceType} from '../../../../../../app/models/graph/zones/zone-instance'; +import {ZoneContainerComponent} from './zone-container.component'; + + +describe('ZoneContainerComponent', () => { + let fixture: ComponentFixture<ZoneContainerComponent>; + + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ZoneContainerComponent] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ZoneContainerComponent); + }); + }) + ); + + + it('should match current snapshot of palette element component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should have a group-zone class when the ZoneInstanceType is GROUP', + () => { + fixture.componentInstance.type = ZoneInstanceType.GROUP; + fixture.detectChanges(); + const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone')); + expect(compiled.nativeElement).toHaveClass('group-zone'); + }); + + it('should have a policy-zone class when the ZoneInstanceType is POLICY', + () => { + fixture.componentInstance.type = ZoneInstanceType.POLICY; + fixture.detectChanges(); + const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone')); + expect(compiled.nativeElement).toHaveClass('policy-zone'); + }); +});
\ No newline at end of file @@ -1,5 +1,5 @@ import { Component, Input, Output, ViewEncapsulation, EventEmitter, OnInit } from '@angular/core'; -import { ZoneInstanceType } from '../../../../models/graph/zones/zone-instance'; +import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance'; @Component({ selector: 'zone-container', @@ -12,15 +12,16 @@ * See the License for the specific language governing permissions and * limitations under the License. --> - - -<div tooltip="{{zoneInstance.instanceData.name}}" #currentComponent - class="zone-instance mode-{{zoneInstance.mode}}" [class.locked]="activeInstanceMode > MODE.HOVER" [class.hiding]="hidden" + + +<div #currentComponent class="zone-instance mode-{{zoneInstance.mode}}" [class.locked]="activeInstanceMode > MODE.HOVER" + [class.hiding]="hidden" (mouseenter)="setMode(MODE.HOVER)" (mouseleave)="setMode(MODE.NONE)" (click)="setMode(MODE.SELECTED, $event)"> - <div *ngIf="zoneInstance.handle" class="target-handle {{zoneInstance.handle}}" (click)="tagHandleClicked($event)"></div> + <div class="zone-instance__body" sdc-tooltip tooltip-text="{{zoneInstance.instanceData.name}}" [attr.data-tests-id]="zoneInstance.instanceData.name"> + <div *ngIf="zoneInstance.handle" class="target-handle {{zoneInstance.handle}}" + (click)="tagHandleClicked($event)"></div> <div *ngIf="!isViewOnly" class="zone-instance__handle" (click)="setMode(MODE.TAG, $event)">+</div> - <div class="zone-instance__body"> - <div class="zone-instance__body-content">{{zoneInstance.assignments.length || defaultIconText}}</div> - </div> - <div class="zone-instance__name">{{zoneInstance.instanceData.name}}</div> -</div>
\ No newline at end of file + <div class="zone-instance__body-content">{{zoneInstance.assignments.length || defaultIconText}}</div> + </div> + <div class="zone-instance__name">{{zoneInstance.instanceData.name}}</div> +</div> @@ -1,7 +1,7 @@ -@import '../../../../../../assets/styles/variables'; +@import '../../../../../../../assets/styles/variables'; .zone-instance { - position:relative; + width:76px; margin:5px; opacity:1; @@ -9,8 +9,8 @@ .zone-instance__handle { display:none; position:absolute; - right:4px; - top:10px; + left: 31px; + top: 8px; width:22px; height:22px; cursor:pointer; @@ -21,6 +21,7 @@ } .zone-instance__body { + position:relative; margin:0 auto; width:43px; height:43px; @@ -55,16 +56,16 @@ height:18px; display:block; top: -4px; - right: 10px; + right: -6px; background-size: 100% 100%; - cursor: url("../../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer; + cursor: url("../../../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer; &.tagged-policy { - background-image: url('../../../../../../assets/styles/images/canvas-tagging-icons/policy_added.svg'); + background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/policy_added.svg'); } &.tag-available { - background-image: url('../../../../../../assets/styles/images/canvas-tagging-icons/indication.svg'); + background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/indication.svg'); } } @@ -99,8 +100,8 @@ &.mode-3 .zone-instance__handle { width:24px; height:24px; - right:3px; - top:9px; + right:-6px; + top:7px; display:block; background-image: linear-gradient(-140deg, #009E98 0%, #97D648 100%); border: 2px solid @main_color_p; @@ -0,0 +1,132 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SimpleChanges } from '@angular/core'; +import { PoliciesService } from 'app/ng2/services/policies.service'; +import { GroupsService } from 'app/ng2/services/groups.service'; +import { EventListenerService } from 'app/services'; +import { Store } from '@ngxs/store'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { ZoneInstanceComponent } from './zone-instance.component'; +import { ZoneInstanceType, ZoneInstance, ZoneInstanceMode, ZoneInstanceAssignmentType, IZoneInstanceAssignment } from "app/models"; +import { PolicyInstance } from "app/models/graph/zones/policy-instance"; +import { Subject, of } from 'rxjs'; +import { _throw } from 'rxjs/observable/throw'; + +describe('ZoneInstanceComponent', () => { + let component: ZoneInstanceComponent; + let fixture: ComponentFixture<ZoneInstanceComponent>; + + let createPolicyInstance = () => { + let policy = new PolicyInstance(); + policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []}; + return new ZoneInstance(policy, '', ''); + } + + beforeEach(() => { + const policiesServiceStub = {updateZoneInstanceAssignments : jest.fn()}; + const groupsServiceStub = {}; + const eventListenerServiceStub = {}; + const storeStub = {}; + const compositionServiceStub = {}; + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [ZoneInstanceComponent], + providers: [ + { provide: PoliciesService, useValue: policiesServiceStub }, + { provide: GroupsService, useValue: groupsServiceStub }, + { provide: EventListenerService, useValue: eventListenerServiceStub }, + { provide: Store, useValue: storeStub }, + { provide: CompositionService, useValue: compositionServiceStub } + ] + }).compileComponents().then(() => { + fixture = TestBed.createComponent(ZoneInstanceComponent); + component = fixture.componentInstance; + }); + }); + + it('can load instance', async((done) => { + component.zoneInstance = <ZoneInstance>{type : ZoneInstanceType.POLICY, instanceData: {name: 'test policy'}, assignments: []}; + component.forceSave = new Subject<Function>(); + fixture.detectChanges(); + expect(component).toBeTruthy(); + })); + + + it('if another instance is already tagging, i cannot change my mode', ()=> { + component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.NONE }; + component.isActive = false; + component.activeInstanceMode = ZoneInstanceMode.TAG; + component.setMode(ZoneInstanceMode.SELECTED); + expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.NONE); + }); + + it('if i am active(selected) and NOT in tag mode, I can set another mode', ()=> { + component.isActive = true; + component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.SELECTED }; + jest.spyOn(component.modeChange, 'emit'); + component.setMode(ZoneInstanceMode.NONE); + expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE }); + }); + + it('if i am active and in tag mode and i try to set mode other than tag, I am not allowed', ()=> { + component.isActive = true; + component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.TAG }; + component.setMode(ZoneInstanceMode.SELECTED); + expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.TAG); + }); + + it('if i am active and in tag mode and click tag again and no changes, does NOT call save, but DOES turn tagging off', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments'); + jest.spyOn(component.modeChange, 'emit'); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneService.updateZoneInstanceAssignments).not.toHaveBeenCalled(); + expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE }); + + }); + it('if i am active and in tag mode and click tag again and HAVE changes, calls save AND turns tagging off', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES}); + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true)); + jest.spyOn(component.modeChange, 'emit'); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneService.updateZoneInstanceAssignments).toHaveBeenCalled(); + expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE }); + }); + + it('on save error, temporary assignment list is reverted to saved assignments', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES}); + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(_throw({status: 404})); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneInstance.assignments.length).toEqual(0); + }); + + it('on save success, all changes are saved to zoneInstance.savedAssignments', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES}); + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true)); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneInstance.instanceData.getSavedAssignments().length).toEqual(1); + }); +}); @@ -3,12 +3,17 @@ import { ZoneInstance, ZoneInstanceMode, ZoneInstanceType, IZoneInstanceAssignment } from 'app/models/graph/zones/zone-instance'; -import { PoliciesService } from '../../../../services/policies.service'; -import { GroupsService } from '../../../../services/groups.service'; -import { IZoneService } from "../../../../../models/graph/zones/zone"; +import { PoliciesService } from 'app/ng2/services/policies.service'; +import { GroupsService } from 'app/ng2/services/groups.service'; +import { IZoneService } from "app/models/graph/zones/zone"; import { EventListenerService } from 'app/services'; -import { GRAPH_EVENTS } from '../../../../../utils'; -import { Subject, Observable } from 'rxjs'; +import { GRAPH_EVENTS } from 'app/utils'; +import { Subject } from 'rxjs'; +import { Store } from "@ngxs/store"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { PolicyInstance } from "app/models"; +import {SelectedComponentType, SetSelectedComponentAction} from "../../../common/store/graph.actions"; + @Component({ selector: 'zone-instance', @@ -33,7 +38,7 @@ export class ZoneInstanceComponent implements OnInit { private MODE = ZoneInstanceMode; private zoneService:IZoneService; - constructor(private policiesService:PoliciesService, private groupsService:GroupsService, private eventListenerService:EventListenerService){} + constructor(private policiesService:PoliciesService, private groupsService:GroupsService, private eventListenerService:EventListenerService, private compositionService:CompositionService, private store:Store){} ngOnInit(){ if(this.zoneInstance.type == ZoneInstanceType.POLICY){ @@ -41,9 +46,11 @@ export class ZoneInstanceComponent implements OnInit { } else { this.zoneService = this.groupsService; } - this.forceSave.subscribe((afterSaveFunction:Function) => { - this.setMode(ZoneInstanceMode.TAG, null, afterSaveFunction); - }) + if(this.forceSave) { + this.forceSave.subscribe((afterSaveFunction:Function) => { + this.setMode(ZoneInstanceMode.TAG, null, afterSaveFunction); + }) + } } ngOnChanges(changes:SimpleChanges) { @@ -53,7 +60,9 @@ export class ZoneInstanceComponent implements OnInit { } ngOnDestroy() { - this.forceSave.unsubscribe(); + if(this.forceSave) { + this.forceSave.unsubscribe(); + } } private setMode = (mode:ZoneInstanceMode, event?:any, afterSaveCallback?:Function):void => { @@ -79,11 +88,17 @@ export class ZoneInstanceComponent implements OnInit { this.zoneService.updateZoneInstanceAssignments(this.zoneInstance.parentComponentType, this.zoneInstance.parentComponentID, this.zoneInstance.instanceData.uniqueId, this.zoneInstance.assignments).subscribe( (success) => { this.zoneInstance.instanceData.setSavedAssignments(this.zoneInstance.assignments); - if(this.zoneInstance.type === ZoneInstanceType.POLICY){ + + if(this.zoneInstance.instanceData instanceof PolicyInstance){ + this.compositionService.updatePolicy(this.zoneInstance.instanceData); this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.zoneInstance.instanceData); + this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.POLICY})); } else { + this.compositionService.updateGroup(this.zoneInstance.instanceData); this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.zoneInstance.instanceData); + this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.GROUP})); } + this.assignmentSaveComplete.emit(true); if(afterSaveCallback) afterSaveCallback(); }, (error) => { @@ -94,12 +109,15 @@ export class ZoneInstanceComponent implements OnInit { if(afterSaveCallback) afterSaveCallback(); } this.modeChange.emit({newMode: ZoneInstanceMode.NONE, instance: this.zoneInstance}); + // this.store.dispatch(new unsavedChangesActions.RemoveUnsavedChange(this.zoneInstance.instanceData.uniqueId)); + } else { this.modeChange.emit({newMode: mode, instance: this.zoneInstance}); + if(mode == ZoneInstanceMode.TAG){ + // this.store.dispatch(new unsavedChangesActions.AddUnsavedChange(this.zoneInstance.instanceData.uniqueId)); + } } - - } private tagHandleClicked = (event:Event) => { @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ZoneContainerComponent } from "./zone-container.component"; +import { ZoneInstanceComponent } from "./zone-instance/zone-instance.component"; +import { SdcUiComponentsModule } from "onap-ui-angular"; + +@NgModule({ + declarations: [ZoneContainerComponent, ZoneInstanceComponent], + imports: [CommonModule, SdcUiComponentsModule], + entryComponents: [ZoneContainerComponent, ZoneInstanceComponent], + exports: [ZoneContainerComponent, ZoneInstanceComponent], + providers: [] +}) +export class ZoneModules { +}
\ No newline at end of file @@ -0,0 +1,304 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import { + CommonNodeBase, + Relationship, + CompositionCiNodeBase +} from "app/models"; +import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link"; +import {Requirement, Capability} from "app/models"; +import {Injectable} from "@angular/core"; + + + +@Injectable() +export class CommonGraphUtils { + + constructor() { + + } + + public safeApply = (scope:ng.IScope, fn:any) => { //todo remove to general utils + let phase = scope.$root.$$phase; + if (phase == '$apply' || phase == '$digest') { + if (fn && (typeof(fn) === 'function')) { + fn(); + } + } else { + scope.$apply(fn); + } + }; + + /** + * Draw node on the graph + * @param cy + * @param compositionGraphNode + * @param position + * @returns {CollectionElements} + */ + public addNodeToGraph(cy:Cy.Instance, compositionGraphNode:CommonNodeBase, position?:Cy.Position):Cy.CollectionElements { + + let node = cy.add(<Cy.ElementDefinition> { + group: 'nodes', + position: position, + data: compositionGraphNode, + classes: compositionGraphNode.classes + }); + + this.initNodeTooltip(node); + return node; + }; + + /** + * The function will create a component instance node by the componentInstance position. + * If the node is UCPE the function will create all cp lan&wan for the ucpe + * @param cy + * @param compositionGraphNode + * @returns {Cy.CollectionElements} + */ + public addComponentInstanceNodeToGraph(cy:Cy.Instance, compositionGraphNode:CompositionCiNodeBase):Cy.CollectionElements { + + let nodePosition = { + x: +compositionGraphNode.componentInstance.posX, + y: +compositionGraphNode.componentInstance.posY + }; + + let node = this.addNodeToGraph(cy, compositionGraphNode, nodePosition); + return node; + }; + + /** + * Add service path link to graph - only draw the link + * @param cy + * @param link + */ + public insertServicePathLinkToGraph = (cy:Cy.Instance, link:CompositionCiServicePathLink) => { + let linkElement = cy.add({ + group: 'edges', + data: link, + classes: link.classes + }); + this.initServicePathTooltip(linkElement, link); + }; + + /** + * Returns function for the link tooltip content + * @param {Relationship} linkRelation + * @param {Requirement} requirement + * @param {Capability} capability + * @returns {() => string} + * @private + */ + private _getLinkTooltipContent(linkRelation:Relationship, requirement?:Requirement, capability?:Capability):string { + return '<div class="line">' + + '<span class="req-cap-label">R: </span>' + + '<span>' + (requirement ? requirement.getTitle() : linkRelation.relation.requirement) + '</span>' + + '</div>' + + '<div class="line">' + + '<div class="sprite-new link-tooltip-arrow"></div>' + + '<span class="req-cap-label">C: </span>' + + '<span>' + (capability ? capability.getTitle() : linkRelation.relation.capability) + '</span>' + + '</div>'; + } + + /** + * This function will init qtip tooltip on the link + * @param linkElement - the link we want the tooltip to apply on, + * @param link + * @param getLinkRequirementCapability + * link - the link obj + */ + public initLinkTooltip(linkElement:Cy.CollectionElements, link:Relationship, getLinkRequirementCapability:Function) { + const content = () => this._getLinkTooltipContent(link); // base tooltip content without owner names + const render = (event, api) => { + // on render (called once at first show), get the link requirement and capability and change to full tooltip content (with owner names) + getLinkRequirementCapability().then((linkReqCap) => { + const fullContent = () => this._getLinkTooltipContent(link, linkReqCap.requirement, linkReqCap.capability); + api.set('content.text', fullContent); + }); + }; + linkElement.qtip(this.prepareInitTooltipData({content, events: {render}})); + }; + + /** + * + * @param linkElement + * @param link + */ + public initServicePathTooltip(linkElement:Cy.CollectionElements, link:CompositionCiServicePathLink) { + let content = function () { + return '<div class="line">' + + '<div>' + link.pathName + '</div>' + + '</div>'; + }; + linkElement.qtip(this.prepareInitTooltipData({content})); + }; + + private prepareInitTooltipData(options?:Object) { + return _.merge({ + position: { + my: 'top center', + at: 'bottom center', + adjust: {x: 0, y: 0}, + effect: false + }, + style: { + classes: 'qtip-dark qtip-rounded qtip-custom link-qtip', + tip: { + width: 16, + height: 8 + } + }, + show: { + event: 'mouseover', + delay: 1000 + }, + hide: {event: 'mouseout mousedown'}, + includeLabels: true, + events: {} + }, options); + } + + public HTMLCoordsToCytoscapeCoords(cytoscapeBoundingBox:Cy.Extent, mousePos:Cy.Position):Cy.Position { + return {x: mousePos.x + cytoscapeBoundingBox.x1, y: mousePos.y + cytoscapeBoundingBox.y1} + }; + + + public getCytoscapeNodePosition = (cy:Cy.Instance, event:DragEvent | MouseEvent):Cy.Position => { + let targetOffset = $(event.target).offset(); + if(event instanceof DragEvent) { + targetOffset = $('canvas').offset(); + } + + let x = (event.pageX - targetOffset.left) / cy.zoom(); + let y = (event.pageY - targetOffset.top) / cy.zoom(); + + return this.HTMLCoordsToCytoscapeCoords(cy.extent(), { + x: x, + y: y + }); + }; + + + public getNodePosition(node:Cy.CollectionFirstNode):Cy.Position { + let nodePosition = node.relativePoint(); + if (node.data().isUcpe) { //UCPEs use bounding box and not relative point. + nodePosition = {x: node.boundingbox().x1, y: node.boundingbox().y1}; + } + + return nodePosition; + } + + /** + * Generic function that can be used for any html elements overlaid on canvas + * Returns the html position of a node on canvas, including left palette and header offsets. Option to pass in additional offset to add to return position. + * @param node + * @param additionalOffset + * @returns {Cy.Position} + + public getNodePositionWithOffset = (node:Cy.CollectionFirstNode, additionalOffset?:Cy.Position): Cy.Position => { + if(!additionalOffset) additionalOffset = {x: 0, y:0}; + + let nodePosition = node.renderedPosition(); + let posWithOffset:Cy.Position = { + x: nodePosition.x + GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET + additionalOffset.x, + y: nodePosition.y + GraphUIObjects.COMPOSITION_HEADER_OFFSET + additionalOffset.y + }; + return posWithOffset; + };*/ + + /** + * return true/false if first node contains in second - this used in order to verify is node is entirely inside ucpe + * @param firstBox + * @param secondBox + * @returns {boolean} + */ + public isFirstBoxContainsInSecondBox(firstBox:Cy.BoundingBox, secondBox:Cy.BoundingBox) { + + return firstBox.x1 > secondBox.x1 && firstBox.x2 < secondBox.x2 && firstBox.y1 > secondBox.y1 && firstBox.y2 < secondBox.y2; + + }; + + /** + * + * @param cy + * @param node + * @returns {Array} + */ + public getLinkableNodes(cy:Cy.Instance, node:Cy.CollectionFirstNode):Array<CompositionCiNodeBase> { + let compatibleNodes = []; + _.each(cy.nodes(), (tempNode)=> { + if (this.nodeLocationsCompatible(node, tempNode)) { + compatibleNodes.push(tempNode.data()); + } + }); + return compatibleNodes; + } + + /** + * Checks whether node locations are compatible in reference to UCPEs. + * Returns true if both nodes are in UCPE or both nodes out, or one node is UCPEpart. + * @param node1 + * @param node2 + */ + public nodeLocationsCompatible(node1:Cy.CollectionFirstNode, node2:Cy.CollectionFirstNode) { + return (this.isFirstBoxContainsInSecondBox(node1.boundingbox(), node2.boundingbox())); + } + + /** + * This function will init qtip tooltip on the node + * @param node - the node we want the tooltip to apply on + */ + public initNodeTooltip(node:Cy.CollectionNodes) { + + let opts = { + content: function () { + return this.data('name'); + }, + position: { + my: 'top center', + at: 'bottom center', + adjust: {x: 0, y: -5} + }, + style: { + classes: 'qtip-dark qtip-rounded qtip-custom', + tip: { + width: 16, + height: 8 + } + }, + show: { + event: 'mouseover', + delay: 1000 + }, + hide: {event: 'mouseout mousedown'}, + includeLabels: true + }; + + if (node.data().isUcpePart) { //fix tooltip positioning for UCPE-cps + opts.position.adjust = {x: 0, y: 20}; + } + + node.qtip(opts); + }; +} + @@ -0,0 +1,92 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +'use strict'; +import {Injectable} from "@angular/core"; + +export interface ICanvasImage { + src: string; + width: number + height: number; + x: number; + y: number; +} + +@Injectable() +export class ImageCreatorService { + + private _canvas:HTMLCanvasElement; + + constructor() { + this._canvas = <HTMLCanvasElement>$('<canvas>')[0]; + this._canvas.setAttribute('style', 'display:none'); + + let body = document.getElementsByTagName('body')[0]; + body.appendChild(this._canvas); + } + + /** + * Create an image composed of different image layers + * @param canvasImages + * @param canvasWidth + * @param canvasHeight + * returns a PROMISE + */ + getMultiLayerBase64Image(canvasImages: ICanvasImage[], canvasWidth?:number, canvasHeight?:number):Promise<string> { + + var promise = new Promise<string>((resolve, reject) => { + if(canvasImages && canvasImages.length === 0){ + return null; + } + + //If only width was set, use it for height, otherwise use first canvasImage height + canvasHeight = canvasHeight || canvasImages[0].height; + canvasWidth = canvasWidth || canvasImages[0].width; + + const images = []; + let imagesLoaded = 0; + const onImageLoaded = () => { + imagesLoaded++; + if(imagesLoaded < canvasImages.length){ + return; + } + this._canvas.setAttribute('width', (canvasWidth * 4).toString()); + this._canvas.setAttribute('height', (canvasHeight * 4).toString()); + const canvasCtx = this._canvas.getContext('2d'); + canvasCtx.scale(4,4); + canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height); + images.forEach((image, index) => { + const canvasImage = canvasImages[index]; + canvasCtx.drawImage(image, canvasImage.x, canvasImage.y, canvasImage.width, canvasImage.height); + }); + + let base64Image = this._canvas.toDataURL(); + resolve(base64Image) + }; + canvasImages.forEach(canvasImage => { + let image = new Image(); + image.onload = onImageLoaded; + image.src = canvasImage.src; + images.push(image); + }); + }); + + return promise; + } +} @@ -0,0 +1,37 @@ +import {async} from "@angular/core/testing"; +import {ComponentInstanceNodesStyle} from "./component-instances-nodes-style"; + + +describe('component instance nodes style component', () => { + + beforeEach( + async(() => { + const createElement = document.createElement.bind(document); + document.createElement = (tagName) => { + if (tagName === 'canvas') { + return { + getContext: () => ({ + font: "", + measureText: (x) => ({width: x.length}) + }), + }; + } + return createElement(tagName); + }; + }) + ); + + it('verify getGraphDisplayName for String.length smaller than 67 chars', () => { + let inputString = 'SomeText'; + let expectedRes = inputString; + let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString); + expect(res).toBe(expectedRes); + }); + + it('verify getGraphDisplayName for String.length greater than 67 chars', () => { + let inputString = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGG12345678'; + let expectedRes = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFF...'; + let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString); + expect(res).toBe(expectedRes); + }); +}
\ No newline at end of file @@ -0,0 +1,362 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { GraphColors, GraphUIObjects} from "app/utils/constants"; +import constant = require("lodash/constant"); +import {ImagesUrl} from "app/utils/constants"; +import {AngularJSBridge} from "app/services/angular-js-bridge-service"; +import { CanvasHandleTypes } from "app/utils"; +/** + * Created by obarda on 12/18/2016. + */ +export class ComponentInstanceNodesStyle { + + public static getCompositionGraphStyle = ():Array<Cy.Stylesheet> => { + return [ + { + selector: 'core', + css: { + 'shape': 'rectangle', + 'active-bg-size': 0, + 'selection-box-color': 'rgb(0, 159, 219)', + 'selection-box-opacity': 0.2, + 'selection-box-border-color': '#009fdb', + 'selection-box-border-width': 1 + + } + }, + { + selector: 'node', + css: { + 'font-family': 'OpenSans-Regular,sans-serif', + + 'font-size': 14, + 'events': 'yes', + 'text-events': 'yes', + 'text-border-width': 15, + 'text-border-color': GraphColors.NODE_UCPE, + 'text-margin-y': 5 + } + }, + { + selector: '.vf-node', + css: { + 'background-color': 'transparent', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'width': GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + 'height': GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + 'background-opacity': 0, + "background-width": GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + "background-height": GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-fit': 'cover', + 'background-clip': 'node', + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + + { + selector: '.service-node', + css: { + 'background-color': 'transparent', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'background-image': 'data(img)', + 'width': 64, + 'height': 64, + "border-width": 0, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.cp-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE, + 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2, + 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2, + 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.vl-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2, + 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2, + 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE, + 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE / 2, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.ucpe-cp', + css: { + 'background-color': GraphColors.NODE_UCPE_CP, + 'background-width': 15, + 'background-height': 15, + 'width': 15, + 'height': 15, + 'text-halign': 'center', + 'overlay-opacity': 0, + 'label': 'data(displayName)', + 'text-valign': 'data(textPosition)', + 'text-margin-y': (ele:Cy.Collection) => { + return (ele.data('textPosition') == 'top') ? -5 : 5; + }, + 'font-size': 12 + } + }, + { + selector: '.ucpe-node', + css: { + 'background-fit': 'cover', + 'padding-bottom': 0, + 'padding-top': 0 + } + }, + { + selector: '.simple-link', + css: { + 'width': 1, + 'line-color': GraphColors.BASE_LINK, + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.vl-link', + css: { + 'width': 3, + 'line-color': GraphColors.VL_LINK, + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.vl-link-1', + css: { + 'width': 3, + 'line-color': GraphColors.ACTIVE_LINK, + 'curve-style': 'unbundled-bezier', + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle', + 'control-point-step-size': 30 + } + }, + { + selector: '.ucpe-host-link', + css: { + 'width': 0 + } + }, + { + selector: '.not-certified-link', + css: { + 'width': 1, + 'line-color': GraphColors.NOT_CERTIFIED_LINK, + 'curve-style': 'bezier', + 'control-point-step-size': 30, + 'line-style': 'dashed', + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle' + + } + }, + + { + selector: '.service-path-link', + css: { + 'width': 2, + 'line-color': GraphColors.SERVICE_PATH_LINK, + 'target-arrow-color': GraphColors.SERVICE_PATH_LINK, + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.not-certified', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + // return ele.data().setUncertifiedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE);//Change name to setUncertifiedImageBgStyle?? + return ele.data().initUncertifiedImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE); + }, + 'border-width': 0 + } + }, + { + selector: '.dependent', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().initDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE) + }, + 'border-width': 0 + } + }, + { + selector: '.dependent.not-certified', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().initUncertifiedDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE) + }, + 'border-width': 0 + } + }, + { + selector: 'node:selected', + css: { + "border-width": 2, + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'shape': 'rectangle' + } + }, + { + selector: 'edge:selected', + css: { + 'line-color': GraphColors.ACTIVE_LINK + + } + }, + { + selector: 'edge:active', + css: { + 'overlay-opacity': 0 + } + }, { + selector: '.configuration-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2, + 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2, + 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE, + 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.archived', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().setArchivedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE); //Change name to setArchivedImageBgStyle ?? + }, + "border-width": 0 + } + } + ] + } + + public static getAddEdgeHandle = () => { + return { + + single: false, + type: CanvasHandleTypes.ADD_EDGE, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_PLUS_ICON, + lineColor: '#27a337', + lineWidth: 2, + lineStyle: 'dashed' + + } + } + + public static getTagHandle = () => { + return { + single: false, + type: CanvasHandleTypes.TAG_AVAILABLE, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_TAG_ICON, + } + } + + public static getTaggedPolicyHandle = () => { + return { + single: false, + type: CanvasHandleTypes.TAGGED_POLICY, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_POLICY_TAGGED_ICON, + } + } + + public static getTaggedGroupHandle = () => { + return { + single: false, + type: CanvasHandleTypes.TAGGED_GROUP, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_GROUP_TAGGED_ICON, + } + } + + public static getGraphDisplayName(name:string):string { + let context = document.createElement("canvas").getContext("2d"); + context.font = "13px Arial"; + + if (67 < context.measureText(name).width) { + let newLen = name.length - 3; + let newName = name.substring(0, newLen); + + while (59 < (context.measureText(newName).width)) { + newName = newName.substring(0, (--newLen)); + } + return newName + '...'; + } + return name; + } + +} @@ -0,0 +1,103 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import {GraphColors} from "app/utils"; +export class ModulesNodesStyle { + + public static getModuleGraphStyle = ():Array<Cy.Stylesheet> => { + + return [ + { + selector: '.cy-expand-collapse-collapsed-node', + css: { + 'background-image': 'data(img)', + 'width': 34, + 'height': 32, + 'background-opacity': 0, + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 5, + 'border-opacity': 0 + } + }, + { + selector: '.module-node', + css: { + 'background-color': 'transparent', + 'background-opacity': 0, + "border-width": 2, + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-style': 'dashed', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 8 + } + }, + { + selector: 'node:selected', + css: { + "border-opacity": 0 + } + }, + { + selector: '.simple-link:selected', + css: { + 'line-color': GraphColors.BASE_LINK, + } + }, + { + selector: '.vl-link:selected', + css: { + 'line-color': GraphColors.VL_LINK, + } + }, + { + selector: '.cy-expand-collapse-collapsed-node:selected', + css: { + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-opacity': 1, + 'border-style': 'solid', + 'border-width': 2 + } + }, + { + selector: '.module-node:selected', + css: { + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-opacity': 1 + } + }, + { + selector: '.dummy-node', + css: { + 'width': 20, + 'height': 20 + } + }, + ] + } +} @@ -0,0 +1,57 @@ +<div class="sdc-composition-graph-wrapper {{zoneTagMode}}" + [ngClass]="{'with-sidebar': withSidebar$ | async, 'view-only':isViewOnly$ | async}"> +</div> + +<div class="sdc-composition-menu" [ngClass]="{'with-sidebar': withSidebar$ | async}"> + + <service-path-selector + *ngIf="topologyTemplate.isService() && compositionService.forwardingPaths" + [drawPath]="drawPathOnCy" + [deletePaths]="deletePathsOnCy" + [selectedPathId]="selectedPathId"> + </service-path-selector> + + <canvas-search *ngIf="componentInstanceNames" class="composition-search" + [placeholder]="'Type to search'" + [data]="componentInstanceNames" + (searchChanged)="getAutoCompleteValues($event)" + (searchButtonClicked)="highlightSearchMatches($event)"> + </canvas-search> + + <!--<service-path class="zoom-icons"--> + <!--*ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()"--> + <!--[service]="topologyTemplate"--> + <!--[onCreate]="createOrUpdateServicePath">--> + <!--</service-path>--> + + <svg-icon *ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()" class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'browse'" [clickable]="true" [testId]="'pathsMenuBtn'" + (click)="openServicePathMenu($event)"></svg-icon> + <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'expand-o'" [clickable]="true" + (click)="zoomAllWithoutSidebar()"></svg-icon> + <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'plus'" [clickable]="true" + (click)="zoom(true)"></svg-icon> + <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'minus'" [clickable]="true" + (click)="zoom(false)"></svg-icon> +</div> + +<div class="sdc-canvas-zones__wrapper {{zoneTagMode}}" [ngClass]="{'with-sidebar': withSidebar$ | async}"> + <zone-container *ngFor="let zone of zones" [title]="zone.title" [type]="zone.type" [count]="zone.instances.length" + [visible]="zone.visible" [minimized]="zone.minimized" (minimize)="zoneMinimizeToggle(zone.type)" + (backgroundClick)="zoneBackgroundClicked()"> + <zone-instance *ngFor="let instance of zone.instances" [hidden]="instance.hidden" + [zoneInstance]="instance" [defaultIconText]="zone.defaultIconText" + [isActive]="activeZoneInstance == instance" + [activeInstanceMode]="activeZoneInstance && activeZoneInstance.mode" + [isViewOnly]="isViewOnly$ | async" + [forceSave]="instance.forceSave" + (modeChange)="zoneInstanceModeChanged($event.newMode, $event.instance, zone.type)" + (tagHandleClick)="zoneInstanceTagged($event)" + (assignmentSaveStart)="zoneAssignmentSaveStart()" + (assignmentSaveComplete)="zoneAssignmentSaveComplete($event)"> + </zone-instance> + </zone-container> +</div>
\ No newline at end of file @@ -0,0 +1,93 @@ +:host(composition-graph) { + flex: 1; + padding-top: 53px; +} + +.composition { + .custom-modal { + /* Hack solution to hide canvas tooltips under modals */ + z-index: 20000 !important; + } +} + +.sdc-composition-graph-wrapper { + height: 100%; + width: 100%; + + &.with-sidebar { + width: calc(~'100% - 300px'); + } +} + +.view-only { + background-color: rgb(248, 248, 248); +} + +.sdc-canvas-zones__wrapper { + position: absolute; + bottom: 10px; + right: 12px; + display: flex; + transition: right 0.2s; + + &.with-sidebar { + right: 310px; + } + + ng2-zone-container { + display: flex; + margin-left: 10px; + } +} + +.group-tagging { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_1.svg"), pointer; +} + +.group-tagging-hover { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_2.svg"), pointer; +} + +.policy-tagging { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_1.svg"), pointer; +} + +.policy-tagging-hover { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer; +} + +//Canvas menu +.sdc-composition-menu { + position: absolute; + right: 18px; + top: 53px; + transition: right 0.2s; + display: flex; + flex-direction: column; + align-items: flex-end; + margin-right: 10px; + pointer-events: none; + + & > * { + pointer-events: all; + } + + &.with-sidebar { + right: 320px; + } + + .composition-search { + margin-top: 12px; + } + + .zoom-icons { + border: solid 1px #d2d2d2; + border-radius: 2px; + box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29); + margin-top: 10px; + + /deep/ .svg-icon { + box-sizing: content-box; + } + } +}
\ No newline at end of file @@ -0,0 +1,354 @@ +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture} from '@angular/core/testing'; +import {SdcUiServices} from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import {ConfigureFn, configureTests} from '../../../../../jest/test-config.helper'; +import {CompositionGraphComponent} from "./composition-graph.component"; +import {WorkspaceService} from "../../workspace/workspace.service"; +import {ComponentInstance, GroupInstance, NodesFactory, ZoneInstance, ZoneInstanceMode} from "../../../../models"; +import {EventListenerService} from "../../../../services"; +import { + CompositionGraphGeneralUtils, + CompositionGraphNodesUtils, + CompositionGraphZoneUtils, + MatchCapabilitiesRequirementsUtils, ServicePathGraphUtils +} from "./utils"; +import {CompositionGraphLinkUtils} from "./utils/composition-graph-links-utils"; +import {ConnectionWizardService} from "./connection-wizard/connection-wizard.service"; +import {CommonGraphUtils} from "./common/common-graph-utils"; +import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {ComponentInstanceServiceNg2} from "../../../services/component-instance-services/component-instance.service"; +import {CompositionService} from "../composition.service"; +import {ModalService} from '../../../services/modal.service'; +import {Store} from '@ngxs/store'; +import {PoliciesService} from '../../../services/policies.service'; +import {GroupsService} from '../../../services/groups.service'; +import {PolicyInstance} from "../../../../models/graph/zones/policy-instance"; +import {ZoneInstanceType} from "../../../../models/graph/zones/zone-instance"; +import {GRAPH_EVENTS} from "../../../../utils/constants"; +import * as cytoscape from "cytoscape"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import {Zone} from "../../../../models/graph/zones/zone"; +import {SelectedComponentType, SetSelectedComponentAction} from "../common/store/graph.actions"; + +describe('composition graph component', () => { + + let fixture: ComponentFixture<CompositionGraphComponent>; + let instance: CompositionGraphComponent; + let eventServiceMock: Partial<EventListenerService>; + let compositionGraphZoneUtils: Partial<CompositionGraphZoneUtils>; + let generalGraphUtils: Partial<CompositionGraphGeneralUtils>; + let workspaceServiceMock: Partial<WorkspaceService>; + let policyService: Partial<PoliciesService>; + let storeStub; + let compositionGraphLinkUtils: Partial<CompositionGraphLinkUtils>; + let nodesGraphUtils: Partial<CompositionGraphNodesUtils>; + + let createPolicyInstance = () => { + let policy = new PolicyInstance(); + policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []}; + return new ZoneInstance(policy, '', ''); + } + + beforeEach( + async(() => { + + eventServiceMock = { + notifyObservers: jest.fn(), + unRegisterObserver: jest.fn() + } + + compositionGraphZoneUtils = { + endCyTagMode: jest.fn(), + showZoneTagIndications: jest.fn(), + hideZoneTagIndications: jest.fn(), + hideGroupZoneIndications: jest.fn(), + showGroupZoneIndications: jest.fn(), + startCyTagMode: jest.fn() + } + + workspaceServiceMock = { + metadata: <ComponentMetadata>{ + uniqueId: 'service_unique_id', + componentType: 'SERVICE' + } + } + + compositionGraphLinkUtils = { + handleLinkClick: jest.fn(), + getModifyLinkMenu: jest.fn() + } + + storeStub = { + dispatch: jest.fn() + } + policyService = { + getSpecificPolicy: jest.fn() + } + + generalGraphUtils = { + zoomGraphTo: jest.fn() + } + + nodesGraphUtils = { + onNodesPositionChanged: jest.fn() + } + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [CompositionGraphComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: NodesFactory, useValue: {}}, + {provide: EventListenerService, useValue: eventServiceMock}, + {provide: CompositionGraphZoneUtils, useValue: compositionGraphZoneUtils}, + {provide: CompositionGraphGeneralUtils, useValue: generalGraphUtils}, + {provide: CompositionGraphLinkUtils, useValue: compositionGraphLinkUtils}, + {provide: CompositionGraphNodesUtils, useValue: nodesGraphUtils}, + {provide: ConnectionWizardService, useValue: {}}, + {provide: CommonGraphUtils, useValue: {}}, + {provide: CompositionGraphPaletteUtils, useValue: {}}, + {provide: TopologyTemplateService, useValue: {}}, + {provide: ComponentInstanceServiceNg2, useValue: {}}, + {provide: MatchCapabilitiesRequirementsUtils, useValue: {}}, + {provide: CompositionService, useValue: {}}, + {provide: SdcUiServices.LoaderService, useValue: {}}, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.NotificationsService, useValue: {}}, + {provide: SdcUiServices.simplePopupMenuService, useValue: {}}, + {provide: ServicePathGraphUtils, useValue: {}}, + {provide: ModalService, useValue: {}}, + {provide: PoliciesService, useValue: policyService}, + {provide: GroupsService, useValue: {}}, + {provide: Store, useValue: storeStub}, + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(CompositionGraphComponent); + instance = fixture.componentInstance; + instance._cy = cytoscape({}); + }); + }) + ); + + it('composition graph component should be defined', () => { + expect(fixture).toBeDefined(); + }); + + describe('on zone instance mode changed', () => { + let newZoneInstance: ZoneInstance; + + beforeEach( + async(() => { + newZoneInstance = createPolicyInstance(); + instance.zoneTagMode = null; + instance.zones = []; + instance.zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY); + instance.zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP); + instance.activeZoneInstance = createPolicyInstance(); + })) + + it('zone instance in tag mode and we want to turn tag mode off', () => { + instance.zoneTagMode = 'some_zone_id'; + instance.activeZoneInstance = newZoneInstance; + instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_END, newZoneInstance); + expect(instance.activeZoneInstance.mode).toBe(ZoneInstanceMode.SELECTED) + }) + + it('we are not in tag mode and policy instance mode changed to NONE - group and zone tag indication need to be removed', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.hideZoneTagIndications).toHaveBeenCalledWith(instance._cy); + expect(instance.compositionGraphZoneUtils.hideGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances); + }) + + it('we are not in tag mode and active zone instance gets hover/none - we dont actually change mode', () => { + let newMode = ZoneInstanceMode.SELECTED; + instance.zoneInstanceModeChanged(newMode, newZoneInstance, ZoneInstanceType.POLICY); + expect(newZoneInstance.mode).toBe(newMode); + }) + + it('we are not in tag mode and zone instance mode changed to HOVER mode', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.HOVER, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance); + expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance); + expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalled(); + }) + + it('we are not in tag mode and mode changed to SELECTED', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.SELECTED, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance); + expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance); + expect(instance.activeZoneInstance).toBe(newZoneInstance); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance); + expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({ + component: newZoneInstance.instanceData, + type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]] + })); + expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY); + }) + + + it('we are not in tag mode and and zone instance mode changed to TAG', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.TAG, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance); + expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance); + expect(instance.activeZoneInstance).toBe(newZoneInstance); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance); + expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({ + component: newZoneInstance.instanceData, + type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]] + })); + expect(instance.compositionGraphZoneUtils.startCyTagMode).toHaveBeenCalledWith(instance._cy); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY); + }) + }) + + it('unset active zone instance', () => { + instance.activeZoneInstance = createPolicyInstance(); + instance.unsetActiveZoneInstance(); + expect(instance.activeZoneInstance).toBeNull(); + expect(instance.zoneTagMode).toBeNull(); + }) + + it('zone background clicked - we are not in tag mode and active zone instance exist', () => { + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + jest.spyOn(instance, 'selectTopologyTemplate'); + instance.zoneBackgroundClicked(); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + }) + + it('zone background clicked - we are not in tag mode and no active zone instance exist', () => { + jest.spyOn(instance, 'unsetActiveZoneInstance'); + jest.spyOn(instance, 'selectTopologyTemplate'); + instance.zoneBackgroundClicked(); + expect(instance.unsetActiveZoneInstance).not.toHaveBeenCalled(); + expect(instance.selectTopologyTemplate).not.toHaveBeenCalled(); + }) + + it('on zoom in', () => { + jest.spyOn(instance, 'zoom'); + instance.zoom(true); + expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() + .1); + }) + + it('on zoom out', () => { + jest.spyOn(instance, 'zoom'); + instance.zoom(false); + expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() - .1); + }) + + describe('cytoscape tap end event have been called', () => { + + it('canvas background was clicked while zone instance in tag mode, zone instance still selected in tag mode)', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + instance.zoneTagMode = 'instance_in_tag' + instance.onTapEnd(event); + expect(instance.zoneTagMode).toBe('instance_in_tag'); + }) + + it('canvas background was clicked and no zone instance selected, topology template is now selected', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + jest.spyOn(instance, 'selectTopologyTemplate'); + instance.onTapEnd(event); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED); + }) + + it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'selectTopologyTemplate'); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + instance.onTapEnd(event); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + }) + + + it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'selectTopologyTemplate'); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + instance.onTapEnd(event); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + }) + + it('on simple edge clicked, open link menu and handle link click', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(true), + data: jest.fn().mockReturnValue({type: 'simple'}) + } + }]; + instance.openModifyLinkMenu = jest.fn(); + instance.onTapEnd(event); + expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event); + expect(instance.openModifyLinkMenu).toHaveBeenCalled(); + }) + + it('on service path edge clicked, no menu is opened', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(true), + data: jest.fn().mockReturnValue({type: 'service-path-link'}) + }] + }; + instance.openModifyLinkMenu = jest.fn(); + instance.onTapEnd(event); + expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event); + expect(instance.openModifyLinkMenu).not.toHaveBeenCalled(); + }) + + it('on drop after drag event (position has changed), call onNodesPositionChanged to update node position', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(false), + position: jest.fn().mockReturnValue({x:2.11, y:2.44}) + }] + }; + instance.currentlyClickedNodePosition = <Cy.Position>{x:2.33, y:2.44}; + instance.onTapEnd(event); + let nodesMoved: Cy.CollectionNodes = instance._cy.$(':grabbed'); + expect(instance.nodesGraphUtils.onNodesPositionChanged).toHaveBeenCalledWith(instance._cy, instance.topologyTemplate, nodesMoved); + + }) + + it('on node clicked (position not changed) while zone instance selected, unset active zone and call set selected instance', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(false), + position: jest.fn().mockReturnValue({x:2.11, y:2.44}), + data: jest.fn().mockReturnValue({componentInstance: new ComponentInstance()}) + }], + }; + instance.currentlyClickedNodePosition = <Cy.Position>{x:2.11, y:2.44}; + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + jest.spyOn(instance, 'selectComponentInstance'); + instance.onTapEnd(event); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + expect(instance.selectComponentInstance).toHaveBeenCalledWith(event.cyTarget[0].data().componentInstance); + }) + }) + + it('initial view mode will turn off all cytoscape events', () => { + jest.spyOn(instance, 'isViewOnly').mockReturnValue(true); + jest.spyOn(instance._cy, 'off'); + instance.initViewMode(); + expect(instance._cy.off).toHaveBeenCalledWith('drag'); + expect(instance._cy.off).toHaveBeenCalledWith('handlemouseout'); + expect(instance._cy.off).toHaveBeenCalledWith('handlemouseover'); + expect(instance._cy.off).toHaveBeenCalledWith('canvasredraw'); + expect(instance._cy.off).toHaveBeenCalledWith('handletagclick'); + + }) +}); @@ -0,0 +1,768 @@ +/** + * Created by ob0695 on 4/24/2018. + */ +import { AfterViewInit, Component, ElementRef, HostBinding, Input } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { + ButtonModel, + Component as TopologyTemplate, + ComponentInstance, + CompositionCiNodeBase, + ConnectRelationModel, + GroupInstance, + LeftPaletteComponent, + LinkMenu, + Match, + ModalModel, + NodesFactory, + Point, + PolicyInstance, + PropertyBEModel, + Relationship, + StepModel, + Zone, + ZoneInstance, + ZoneInstanceAssignmentType, + ZoneInstanceMode, + ZoneInstanceType +} from 'app/models'; +import { ForwardingPath } from 'app/models/forwarding-path'; +import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link'; +import { UIZoneInstanceObject } from 'app/models/ui-models/ui-zone-instance-object'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { CommonGraphUtils } from 'app/ng2/pages/composition/graph/common/common-graph-utils'; +import { ComponentInstanceNodesStyle } from 'app/ng2/pages/composition/graph/common/style/component-instances-nodes-style'; +import { ConnectionPropertiesViewComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component'; +import { ConnectionWizardHeaderComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component'; +import { ConnectionWizardService } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service'; +import { FromNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component'; +import { PropertiesStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component'; +import { ToNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ModalService } from 'app/ng2/services/modal.service'; +import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response'; +import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response'; +import { WorkspaceState } from 'app/ng2/store/states/workspace.state'; +import { EventListenerService } from 'app/services'; +import { ComponentInstanceFactory, EVENTS, SdcElementType } from 'app/utils'; +import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS } from 'app/utils/constants'; +import * as _ from 'lodash'; +import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop'; +import { SdcUiServices } from 'onap-ui-angular'; +import { NotificationSettings } from 'onap-ui-angular/dist/notifications/utilities/notification.config'; +import { menuItem } from 'onap-ui-angular/dist/simple-popup-menu/menu-data.interface'; +import { CytoscapeEdgeEditation } from '../../../../../third-party/cytoscape.js-edge-editation/CytoscapeEdgeEditation.js'; +import { SelectedComponentType, SetSelectedComponentAction } from '../common/store/graph.actions'; +import { GraphState } from '../common/store/graph.state'; +import { + CompositionGraphGeneralUtils, + CompositionGraphNodesUtils, + CompositionGraphZoneUtils, + MatchCapabilitiesRequirementsUtils +} from './utils'; +import { CompositionGraphLinkUtils } from './utils/composition-graph-links-utils'; +import { CompositionGraphPaletteUtils } from './utils/composition-graph-palette-utils'; +import { ServicePathGraphUtils } from './utils/composition-graph-service-path-utils'; + +declare const window: any; + +@Component({ + selector: 'composition-graph', + templateUrl: './composition-graph.component.html', + styleUrls: ['./composition-graph.component.less'] +}) + +export class CompositionGraphComponent implements AfterViewInit { + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + @Select(GraphState.withSidebar) withSidebar$: boolean; + @Input() topologyTemplate: TopologyTemplate; + @HostBinding('attr.data-tests-id') dataTestId: string; + @Input() testId: string; + + // tslint:disable:variable-name + private _cy: Cy.Instance; + private zoneTagMode: string; + private activeZoneInstance: ZoneInstance; + private zones: Zone[]; + private currentlyClickedNodePosition: Cy.Position; + private dragElement: JQuery; + private dragComponent: ComponentInstance; + private componentInstanceNames: string[]; + private topologyTemplateId: string; + private topologyTemplateType: string; + + constructor(private elRef: ElementRef, + private nodesFactory: NodesFactory, + private eventListenerService: EventListenerService, + private compositionGraphZoneUtils: CompositionGraphZoneUtils, + private generalGraphUtils: CompositionGraphGeneralUtils, + private compositionGraphLinkUtils: CompositionGraphLinkUtils, + private nodesGraphUtils: CompositionGraphNodesUtils, + private connectionWizardService: ConnectionWizardService, + private commonGraphUtils: CommonGraphUtils, + private modalService: ModalService, + private compositionGraphPaletteUtils: CompositionGraphPaletteUtils, + private topologyTemplateService: TopologyTemplateService, + private componentInstanceService: ComponentInstanceServiceNg2, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private store: Store, + private compositionService: CompositionService, + private loaderService: SdcUiServices.LoaderService, + private workspaceService: WorkspaceService, + private notificationService: SdcUiServices.NotificationsService, + private simplePopupMenuService: SdcUiServices.simplePopupMenuService, + private servicePathGraphUtils: ServicePathGraphUtils) { + } + + ngOnInit() { + this.dataTestId = this.testId; + this.topologyTemplateId = this.workspaceService.metadata.uniqueId; + this.topologyTemplateType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new SetSelectedComponentAction({ + component: this.topologyTemplate, + type: SelectedComponentType.TOPOLOGY_TEMPLATE + })); + this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, () => { + this.loadGraphData(); + }); + this.loadCompositionData(); + } + + ngAfterViewInit() { + this.loadGraph(); + } + + ngOnDestroy() { + this._cy.destroy(); + _.forEach(GRAPH_EVENTS, (event) => { + this.eventListenerService.unRegisterObserver(event); + }); + this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT); + this.eventListenerService.unRegisterObserver(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE); + } + + public isViewOnly = (): boolean => { + return this.store.selectSnapshot((state) => state.workspace.isViewOnly); + } + + public zoom = (zoomIn: boolean): void => { + const currentZoom: number = this._cy.zoom(); + if (zoomIn) { + this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom + .1); + } else { + this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom - .1); + } + } + + public zoomAllWithoutSidebar = () => { + setTimeout(() => { // wait for sidebar changes to take effect before zooming + this.generalGraphUtils.zoomAll(this._cy); + }); + } + + public getAutoCompleteValues = (searchTerm: string) => { + if (searchTerm.length > 1) { // US requirement: only display search results after 2nd letter typed. + const nodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm); + this.componentInstanceNames = _.map(nodes, (node) => node.data('name')); + } else { + this.componentInstanceNames = []; + } + } + + public highlightSearchMatches = (searchTerm: string) => { + this.nodesGraphUtils.highlightMatchingNodesByName(this._cy, searchTerm); + const matchingNodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm); + this.generalGraphUtils.zoomAll(this._cy, matchingNodes); + } + + public onDrop = (dndEvent: DndDropEvent) => { + this.compositionGraphPaletteUtils.addNodeFromPalette(this._cy, dndEvent); + } + + public openServicePathMenu = ($event): void => { + + const menuConfig: menuItem[] = []; + if (!this.isViewOnly()) { + menuConfig.push({ + text: 'Create Service Flow', + action: () => this.servicePathGraphUtils.onCreateServicePath() + }); + } + menuConfig.push({ + text: 'Service Flows List', + type: '', + action: () => this.servicePathGraphUtils.onListServicePath() + }); + const popup = this.simplePopupMenuService.openBaseMenu(menuConfig, { + x: $event.x, + y: $event.y + }); + + } + + public deletePathsOnCy = () => { + this.servicePathGraphUtils.deletePathsFromGraph(this._cy); + } + + public drawPathOnCy = (data: ForwardingPath) => { + this.servicePathGraphUtils.drawPath(this._cy, data); + } + + public onTapEnd = (event: Cy.EventObject) => { + if (this.zoneTagMode) { + return; + } + if (event.cyTarget === this._cy) { // On Background clicked + if (this._cy.$('node:selected').length === 0) { // if the background click but not dragged + if (this.activeZoneInstance) { + this.unsetActiveZoneInstance(); + this.selectTopologyTemplate(); + } else { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED); + this.selectTopologyTemplate(); + } + + } + } else if (event.cyTarget[0].isEdge()) { // and Edge clicked + this.compositionGraphLinkUtils.handleLinkClick(this._cy, event); + if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) { + return; + } + this.openModifyLinkMenu(this.compositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), event); + } else { // On Node clicked + + this._cy.nodes(':grabbed').style({'overlay-opacity': 0}); + + const newPosition = event.cyTarget[0].position(); + // node position changed (drop after drag event) - we need to update position + if (this.currentlyClickedNodePosition.x !== newPosition.x || this.currentlyClickedNodePosition.y !== newPosition.y) { + const nodesMoved: Cy.CollectionNodes = this._cy.$(':grabbed'); + this.nodesGraphUtils.onNodesPositionChanged(this._cy, this.topologyTemplate, nodesMoved); + } else { + if (this.activeZoneInstance) { + this.unsetActiveZoneInstance(); + } + this.selectComponentInstance(event.cyTarget[0].data().componentInstance); + } + } + } + + private registerCytoscapeGraphEvents() { + + this._cy.on('addedgemouseup', (event, data) => { + const connectRelationModel: ConnectRelationModel = this.compositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target); + if (connectRelationModel != null) { + this.connectionWizardService.setRelationMenuDirectiveObj(connectRelationModel); + this.connectionWizardService.selectedMatch = null; + + const steps: StepModel[] = []; + const fromNodeName: string = connectRelationModel.fromNode.componentInstance.name; + const toNodeName: string = connectRelationModel.toNode.componentInstance.name; + steps.push(new StepModel(fromNodeName, FromNodeStepComponent)); + steps.push(new StepModel(toNodeName, ToNodeStepComponent)); + steps.push(new StepModel('Properties', PropertiesStepComponent)); + const wizardTitle = 'Connect: ' + fromNodeName + ' to ' + toNodeName; + const modalInstance = this.modalService.createMultiStepsWizard(wizardTitle, steps, this.createLinkFromMenu, ConnectionWizardHeaderComponent); + modalInstance.instance.open(); + } + }); + + this._cy.on('tapstart', 'node', (event: Cy.EventObject) => { + this.currentlyClickedNodePosition = angular.copy(event.cyTarget[0].position()); // update node position on drag + }); + + this._cy.on('drag', 'node', (event: Cy.EventObject) => { + if (event.cyTarget.data().componentSubType !== SdcElementType.POLICY && event.cyTarget.data().componentSubType !== SdcElementType.GROUP) { + event.cyTarget.style({'overlay-opacity': 0.24}); + if (this.generalGraphUtils.isValidDrop(this._cy, event.cyTarget)) { + event.cyTarget.style({'overlay-color': GraphColors.NODE_BACKGROUND_COLOR}); + } else { + event.cyTarget.style({'overlay-color': GraphColors.NODE_OVERLAPPING_BACKGROUND_COLOR}); + } + } + }); + + this._cy.on('handlemouseover', (event, payload) => { + // no need to add opacity while we are dragging and hovering othe nodes- or if opacity was already calculated for these nodes + if (payload.node.grabbed() || this._cy.scratch('_edge_editation_highlights') === true) { + return; + } + + if (this.zoneTagMode) { + this.zoneTagMode = this.zones[this.activeZoneInstance.type].getHoverTagModeId(); + return; + } + + const nodesData = this.nodesGraphUtils.getAllNodesData(this._cy.nodes()); + const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(this._cy); + const instance = payload.node.data().componentInstance; + const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(instance, nodesData, nodesLinks); + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data()); + + this._cy.scratch()._edge_editation_highlights = true; + }); + + this._cy.on('handlemouseout', () => { + if (this.zoneTagMode) { + this.zoneTagMode = this.zones[this.activeZoneInstance.type].getTagModeId(); + return; + } + if (this._cy.scratch('_edge_editation_highlights') === true) { + this._cy.removeScratch('_edge_editation_highlights'); + this._cy.emit('hidehandles'); + this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy); + } + }); + + this._cy.on('tapend', (event: Cy.EventObject) => { + this.onTapEnd(event); + }); + + this._cy.on('boxselect', 'node', (event: Cy.EventObject) => { + this.unsetActiveZoneInstance(); + this.selectComponentInstance(event.cyTarget.data().componentInstance); + }); + + this._cy.on('canvasredraw', (event: Cy.EventObject) => { + if (this.zoneTagMode) { + this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, this.activeZoneInstance); + } + }); + + this._cy.on('handletagclick', (event: Cy.EventObject, eventData: any) => { + this.compositionGraphZoneUtils.handleTagClick(this._cy, this.activeZoneInstance, eventData.nodeId); + }); + } + + private initViewMode() { + + if (this.isViewOnly()) { + // remove event listeners + this._cy.off('drag'); + this._cy.off('handlemouseout'); + this._cy.off('handlemouseover'); + this._cy.off('canvasredraw'); + this._cy.off('handletagclick'); + this._cy.edges().unselectify(); + } + } + + private saveChangedCapabilityProperties = (): Promise<PropertyBEModel[]> => { + return new Promise<PropertyBEModel[]>((resolve) => { + const capabilityPropertiesBE: PropertyBEModel[] = this.connectionWizardService.changedCapabilityProperties.map((prop) => { + prop.value = prop.getJSONValue(); + const propBE = new PropertyBEModel(prop); + propBE.parentUniqueId = this.connectionWizardService.selectedMatch.relationship.relation.capabilityOwnerId; + return propBE; + }); + if (capabilityPropertiesBE.length > 0) { + // if there are capability properties to update, then first update capability properties and then resolve promise + this.componentInstanceService + .updateInstanceCapabilityProperties( + this.topologyTemplate, + this.connectionWizardService.selectedMatch.toNode, + this.connectionWizardService.selectedMatch.capability, + capabilityPropertiesBE + ) + .subscribe((response) => { + console.log('Update resource instance capability properties response: ', response); + this.connectionWizardService.changedCapabilityProperties = []; + resolve(capabilityPropertiesBE); + }); + } else { + // no capability properties to update, immediately resolve promise + resolve(capabilityPropertiesBE); + } + }); + } + + private loadCompositionData = () => { + this.loaderService.activate(); + this.topologyTemplateService.getComponentCompositionData(this.topologyTemplateId, this.topologyTemplateType).subscribe((response: ComponentGenericResponse) => { + if (this.topologyTemplateType === ComponentType.SERVICE) { + this.compositionService.forwardingPaths = (response as ServiceGenericResponse).forwardingPaths; + } + this.compositionService.componentInstances = response.componentInstances; + this.compositionService.componentInstancesRelations = response.componentInstancesRelations; + this.compositionService.groupInstances = response.groupInstances; + this.compositionService.policies = response.policies; + this.loadGraphData(); + this.loaderService.deactivate(); + }, (error) => { this.loaderService.deactivate(); }); + } + + private loadGraph = () => { + const graphEl = this.elRef.nativeElement.querySelector('.sdc-composition-graph-wrapper'); + this.initGraph(graphEl); + this.zones = this.compositionGraphZoneUtils.createCompositionZones(); + this.registerCytoscapeGraphEvents(); + this.registerCustomEvents(); + this.initViewMode(); + } + + private initGraphNodes() { + + setTimeout(() => { + const handles = new CytoscapeEdgeEditation(); + handles.init(this._cy); + if (!this.isViewOnly()) { // Init nodes handle extension - enable dynamic links + handles.initNodeEvents(); + handles.registerHandle(ComponentInstanceNodesStyle.getAddEdgeHandle()); + } + handles.registerHandle(ComponentInstanceNodesStyle.getTagHandle()); + handles.registerHandle(ComponentInstanceNodesStyle.getTaggedPolicyHandle()); + handles.registerHandle(ComponentInstanceNodesStyle.getTaggedGroupHandle()); + }, 0); + + _.each(this.compositionService.componentInstances, (instance) => { + const compositionGraphNode: CompositionCiNodeBase = this.nodesFactory.createNode(instance); + this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode); + }); + + } + + private loadGraphData = () => { + this.initGraphNodes(); + this.compositionGraphLinkUtils.initGraphLinks(this._cy, this.compositionService.componentInstancesRelations); + this.compositionGraphZoneUtils.initZoneInstances(this.zones); + setTimeout(() => { // Need setTimeout so that angular canvas changes will take effect before resize & center + this.generalGraphUtils.zoomAllWithMax(this._cy, 1); + }); + this.componentInstanceNames = _.map(this._cy.nodes(), (node) => node.data('name')); + } + + private initGraph(graphEl: JQuery) { + + this._cy = cytoscape({ + container: graphEl, + style: ComponentInstanceNodesStyle.getCompositionGraphStyle(), + zoomingEnabled: true, + maxZoom: 1.2, + minZoom: .1, + userZoomingEnabled: false, + userPanningEnabled: true, + selectionType: 'single', + boxSelectionEnabled: true, + autolock: this.isViewOnly(), + autoungrabify: this.isViewOnly() + }); + + // Testing Bridge that allows Cypress tests to select a component on canvas not via DOM + if (window.Cypress) { + window.testBridge = this.createCanvasTestBridge(); + } + } + + private createCanvasTestBridge(): any { + return { + selectComponentInstance: (componentName: string) => { + const matchingNodesByName = this.nodesGraphUtils.getMatchingNodesByName(this._cy, componentName); + const component = new ComponentInstance(matchingNodesByName.first().data().componentInstance); + this.selectComponentInstance(component); + } + }; + } + + // -------------------------------------------- ZONES---------------------------------------------------------// + private zoneMinimizeToggle = (zoneType: ZoneInstanceType): void => { + this.zones[zoneType].minimized = !this.zones[zoneType].minimized; + } + + private zoneInstanceModeChanged = (newMode: ZoneInstanceMode, instance: ZoneInstance, zoneId: ZoneInstanceType): void => { + if (this.zoneTagMode) { // we're in tag mode. + if (instance === this.activeZoneInstance && newMode === ZoneInstanceMode.NONE) { // we want to turn tag mode off. + this.zoneTagMode = null; + this.activeZoneInstance.mode = ZoneInstanceMode.SELECTED; + this.compositionGraphZoneUtils.endCyTagMode(this._cy); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_END, instance); + + } + } else { + // when active zone instance gets hover/none, don't actually change mode, just show/hide indications + if (instance !== this.activeZoneInstance || (instance === this.activeZoneInstance && newMode > ZoneInstanceMode.HOVER)) { + instance.mode = newMode; + } + + if (newMode === ZoneInstanceMode.NONE) { + this.compositionGraphZoneUtils.hideZoneTagIndications(this._cy); + if (this.zones[ZoneInstanceType.GROUP]) { + this.compositionGraphZoneUtils.hideGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances); + } + } + if (newMode >= ZoneInstanceMode.HOVER) { + this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, instance); + if (instance.type === ZoneInstanceType.POLICY && this.zones[ZoneInstanceType.GROUP]) { + this.compositionGraphZoneUtils.showGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances, instance); + } + } + if (newMode >= ZoneInstanceMode.SELECTED) { + this._cy.$('node:selected').unselect(); + if (this.activeZoneInstance && this.activeZoneInstance !== instance && newMode >= ZoneInstanceMode.SELECTED) { + this.activeZoneInstance.mode = ZoneInstanceMode.NONE; + } + this.activeZoneInstance = instance; + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, instance); + this.store.dispatch(new SetSelectedComponentAction({ + component: instance.instanceData, + type: SelectedComponentType[ZoneInstanceType[instance.type]] + })); + } + if (newMode === ZoneInstanceMode.TAG) { + this.compositionGraphZoneUtils.startCyTagMode(this._cy); + this.zoneTagMode = this.zones[zoneId].getTagModeId(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_START, zoneId); + } + } + } + + private zoneInstanceTagged = (taggedInstance: ZoneInstance) => { + this.activeZoneInstance.addOrRemoveAssignment(taggedInstance.instanceData.uniqueId, ZoneInstanceAssignmentType.GROUPS); + const newHandle: string = this.compositionGraphZoneUtils.getCorrectHandleForNode(taggedInstance.instanceData.uniqueId, this.activeZoneInstance); + taggedInstance.showHandle(newHandle); + } + + private unsetActiveZoneInstance = (): void => { + if (this.activeZoneInstance) { + this.activeZoneInstance.mode = ZoneInstanceMode.NONE; + this.activeZoneInstance = null; + this.zoneTagMode = null; + } + } + + private selectComponentInstance = (componentInstance: ComponentInstance) => { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_NODE_SELECTED, componentInstance); + this.store.dispatch(new SetSelectedComponentAction({ + component: componentInstance, + type: SelectedComponentType.COMPONENT_INSTANCE + })); + } + + private selectTopologyTemplate = () => { + this.store.dispatch(new SetSelectedComponentAction({ + component: this.topologyTemplate, + type: SelectedComponentType.TOPOLOGY_TEMPLATE + })); + } + + private zoneBackgroundClicked = (): void => { + if (!this.zoneTagMode && this.activeZoneInstance) { + this.unsetActiveZoneInstance(); + this.selectTopologyTemplate(); + } + } + + private zoneAssignmentSaveStart = () => { + this.loaderService.activate(); + } + + private zoneAssignmentSaveComplete = (success: boolean) => { + this.loaderService.deactivate(); + if (!success) { + this.notificationService.push(new NotificationSettings('error', 'Update Failed', 'Error')); + } + } + + private deleteZoneInstance = (deletedInstance: UIZoneInstanceObject) => { + if (deletedInstance.type === ZoneInstanceType.POLICY) { + this.compositionService.policies = this.compositionService.policies.filter((policy) => policy.uniqueId !== deletedInstance.uniqueId); + } else if (deletedInstance.type === ZoneInstanceType.GROUP) { + this.compositionService.groupInstances = this.compositionService.groupInstances.filter((group) => group.uniqueId !== deletedInstance.uniqueId); + } + // remove it from zones + this.zones[deletedInstance.type].removeInstance(deletedInstance.uniqueId); + if (deletedInstance.type === ZoneInstanceType.GROUP && !_.isEmpty(this.zones[ZoneInstanceType.POLICY])) { + this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(deletedInstance.uniqueId, [this.zones[ZoneInstanceType.POLICY]], ZoneInstanceAssignmentType.GROUPS); + } + this.selectTopologyTemplate(); + } + // -------------------------------------------------------------------------------------------------------------// + + private registerCustomEvents() { + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, (groupInstance: GroupInstance) => { + this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, groupInstance); + this.notificationService.push(new NotificationSettings('success', 'Group Updated', 'Success')); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, (policyInstance: PolicyInstance) => { + this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, policyInstance); + this.notificationService.push(new NotificationSettings('success', 'Policy Updated', 'Success')); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (leftPaletteComponent: LeftPaletteComponent) => { + this.compositionGraphPaletteUtils.onComponentHoverIn(leftPaletteComponent, this._cy); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE, + (component: TopologyTemplate, paletteComponent: LeftPaletteComponent, startPosition: Point) => { + + const zoneType: ZoneInstanceType = this.compositionGraphZoneUtils.getZoneTypeForPaletteComponent(paletteComponent.categoryType); + this.compositionGraphZoneUtils.showZone(this.zones[zoneType]); + + this.loaderService.activate(); + this.compositionGraphZoneUtils.createZoneInstanceFromLeftPalette(zoneType, paletteComponent.type).subscribe((zoneInstance: ZoneInstance) => { + this.loaderService.deactivate(); + this.compositionGraphZoneUtils.addInstanceToZone(this.zones[zoneInstance.type], zoneInstance, true); + this.compositionGraphZoneUtils.createPaletteToZoneAnimation(startPosition, zoneType, zoneInstance); + }, (error) => { + this.loaderService.deactivate(); + }); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT, () => { + this._cy.emit('hidehandles'); + this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, (dragElement, dragComponent) => { + this.dragElement = dragElement; + this.dragComponent = ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (position: Point) => { + const draggedElement = document.getElementById('draggable_element'); + draggedElement.className = this.compositionGraphPaletteUtils.isDragValid(this._cy, position) ? 'valid-drag' : 'invalid-drag'; + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, (event: DndDropEvent) => { + this.onDrop(event); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component: ComponentInstance) => { + const selectedNode = this._cy.getElementById(component.uniqueId); + selectedNode.data().componentInstance.name = component.name; + selectedNode.data('name', component.name); // used for tooltip + selectedNode.data('displayName', selectedNode.data().getDisplayName()); // abbreviated + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstanceId: string) => { + const nodeToDelete = this._cy.getElementById(componentInstanceId); + this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, nodeToDelete); + this.selectTopologyTemplate(); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, (deletedInstance: UIZoneInstanceObject) => { + this.deleteZoneInstance(deletedInstance); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, (componentInstanceId: string) => { + if (!_.isEmpty(this.zones)) { + this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(componentInstanceId, this.zones, ZoneInstanceAssignmentType.COMPONENT_INSTANCES); + } + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading: boolean, linksToDelete: Cy.CollectionEdges) => { + this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, releaseLoading, linksToDelete); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component: ComponentInstance) => { + // Remove everything from graph and reload it all + this._cy.elements().remove(); + this.loadCompositionData(); + setTimeout(() => { this._cy.getElementById(component.uniqueId).select(); }, 1000); + this.selectComponentInstance(component); + }); + this.eventListenerService.registerObserverCallback(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, (ischecked: boolean) => { + if (ischecked) { + this._cy.$('node:selected').addClass('dependent'); + } else { + // due to defect in cytoscape, just changing the class does not replace the icon, and i need to revert to original icon with no markings. + this._cy.$('node:selected').removeClass('dependent'); + this._cy.$('node:selected').style({'background-image': this._cy.$('node:selected').data('originalImg')}); + } + }); + } + private createLinkFromMenu = (): void => { + this.saveChangedCapabilityProperties().then(() => { + this.compositionGraphLinkUtils.createLinkFromMenu(this._cy, this.connectionWizardService.selectedMatch); + }); + } + + private deleteRelation = (link: Cy.CollectionEdges) => { + // if multiple edges selected, delete the VL itself so edges get deleted automatically + if (this._cy.$('edge:selected').length > 1) { + this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, this._cy.$('node:selected')); + } else { + this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, true, link); + } + } + + private viewRelation = (link: Cy.CollectionEdges) => { + + const linkData = link.data(); + const sourceNode: CompositionCiNodeBase = link.source().data(); + const targetNode: CompositionCiNodeBase = link.target().data(); + const relationship: Relationship = linkData.relation.relationships[0]; + + this.compositionGraphLinkUtils.getRelationRequirementCapability(relationship, sourceNode.componentInstance, targetNode.componentInstance).then((objReqCap) => { + const capability = objReqCap.capability; + const requirement = objReqCap.requirement; + + this.connectionWizardService.connectRelationModel = new ConnectRelationModel(sourceNode, targetNode, []); + this.connectionWizardService.selectedMatch = new Match(requirement, capability, true, linkData.source, linkData.target); + this.connectionWizardService.selectedMatch.relationship = relationship; + + const title = `Connection Properties`; + const saveButton: ButtonModel = new ButtonModel('Save', 'blue', () => { + this.saveChangedCapabilityProperties().then(() => { + this.modalService.closeCurrentModal(); + }); + }); + const cancelButton: ButtonModel = new ButtonModel('Cancel', 'white', () => { + this.modalService.closeCurrentModal(); + }); + const modal = new ModalModel('xl', title, '', [saveButton, cancelButton]); + const modalInstance = this.modalService.createCustomModal(modal); + this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent); + modalInstance.instance.open(); + + new Promise((resolve) => { + if (!this.connectionWizardService.selectedMatch.capability.properties) { + this.componentInstanceService.getInstanceCapabilityProperties(this.topologyTemplateType, this.topologyTemplateId, linkData.target, capability) + .subscribe(() => { + resolve(); + }, () => { /* do nothing */ }); + } else { + resolve(); + } + }).then(() => { + this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent); + }); + }, () => { /* do nothing */ }); + } + + private openModifyLinkMenu = (linkMenuObject: LinkMenu, $event) => { + + const menuConfig: menuItem[] = [{ + text: 'View', + iconName: 'eye-o', + iconType: 'common', + iconMode: 'secondary', + iconSize: 'small', + type: '', + action: () => this.viewRelation(linkMenuObject.link as Cy.CollectionEdges) + }]; + + if (!this.isViewOnly()) { + menuConfig.push({ + text: 'Delete', + iconName: 'trash-o', + iconType: 'common', + iconMode: 'secondary', + iconSize: 'small', + type: '', + action: () => this.deleteRelation(linkMenuObject.link as Cy.CollectionEdges) + }); + } + this.simplePopupMenuService.openBaseMenu(menuConfig, { + x: $event.originalEvent.x, + y: $event.originalEvent.y + }); + } + +} @@ -0,0 +1,55 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {CompositionGraphComponent} from "./composition-graph.component"; +import {ZoneModules} from "./canvas-zone/zones-module"; +import {CompositionGraphZoneUtils} from "./utils/composition-graph-zone-utils"; +import {CompositionGraphGeneralUtils} from "./utils/composition-graph-general-utils"; +import {CommonGraphUtils} from "./common/common-graph-utils"; +import {LinksFactory} from "app/models/graph/graph-links/links-factory"; +import {NodesFactory} from "app/models/graph/nodes/nodes-factory"; +import {ImageCreatorService} from "./common/image-creator.service"; +import {MatchCapabilitiesRequirementsUtils} from "./utils/match-capability-requierment-utils"; +import {CompositionGraphNodesUtils} from "./utils/composition-graph-nodes-utils"; +import {ConnectionWizardService} from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service"; +import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {DndModule} from "ngx-drag-drop"; +import { MenuListNg2Module } from "app/ng2/components/downgrade-wrappers/menu-list-ng2/menu-list-ng2.module"; +import { UiElementsModule } from "app/ng2/components/ui/ui-elements.module"; +import {ServicePathSelectorModule} from "./service-path-selector/service-path-selector.module"; +import {SdcUiComponentsModule, SdcUiServices} from "onap-ui-angular"; +import {CanvasSearchModule} from "./canvas-search/canvas-search.module"; +import {CompositionGraphLinkUtils, ServicePathGraphUtils} from "./utils"; + + +@NgModule({ + declarations: [CompositionGraphComponent], + imports: [CommonModule, + ServicePathSelectorModule, + SdcUiComponentsModule, + MenuListNg2Module, + UiElementsModule, + ZoneModules, + CanvasSearchModule, + DndModule], + exports: [CompositionGraphComponent], + entryComponents: [CompositionGraphComponent], + providers: [ + CompositionGraphZoneUtils, + CompositionGraphGeneralUtils, + MatchCapabilitiesRequirementsUtils, + CompositionGraphNodesUtils, + CompositionGraphLinkUtils, + CommonGraphUtils, + NodesFactory, + LinksFactory, + ImageCreatorService, + ConnectionWizardService, + CompositionGraphPaletteUtils, + QueueServiceUtils, + SdcUiServices.simplePopupMenuService, + ServicePathGraphUtils + ] +}) +export class CompositionGraphModule { +}
\ No newline at end of file @@ -1,5 +1,5 @@ -@import '../../../../../assets/styles/sprite-proxy-services-icons'; -@import '../../../../../assets/styles/variables'; +@import '../../../../../../../assets/styles/sprite-proxy-services-icons'; +@import '../../../../../../../assets/styles/variables'; .header-main-container{ background-color: #f8f8f8; width: 100%; @@ -3,9 +3,9 @@ import {NgModule} from "@angular/core"; import {FromNodeStepComponent} from "./from-node-step/from-node-step.component"; import {PropertiesStepComponent} from "./properties-step/properties-step.component"; import {ConnectionWizardService} from "./connection-wizard.service"; -import {SelectRequirementOrCapabilityModule} from "../../components/logic/select-requirement-or-capability/select-requirement-or-capability.module"; -import {PropertyTableModule} from "../../components/logic/properties-table/property-table.module"; -import {FormElementsModule} from "../../components/ui/form-components/form-elements.module"; +import {SelectRequirementOrCapabilityModule} from "../../../../components/logic/select-requirement-or-capability/select-requirement-or-capability.module"; +import {PropertyTableModule} from "../../../../components/logic/properties-table/property-table.module"; +import {FormElementsModule} from "../../../../components/ui/form-components/form-elements.module"; import {ConnectionWizardHeaderComponent} from "./connection-wizard-header/connection-wizard-header.component"; import {ConnectionPropertiesViewComponent} from "./connection-properties-view/connection-properties-view.component"; import {BrowserModule} from "@angular/platform-browser"; @@ -0,0 +1,85 @@ +import {TestBed} from "@angular/core/testing"; +import {WorkspaceService} from "../../../../pages/workspace/workspace.service"; +import { ConnectionWizardService } from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service"; +import { ConnectRelationModel, Match, Requirement, Capability } from "app/models"; +import { Mock } from "ts-mockery/dist"; + +describe('Connection Wizard Service', () => { + + let service: ConnectionWizardService; + + const connectRelationModelMock = Mock.of<ConnectRelationModel>({ + possibleRelations: [ + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement1', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement2', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement3', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability3', type: 'tosca'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement4', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement5', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'tosca'})}), + Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement6', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}), + Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement7', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})}) + ] + }); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ConnectionWizardService, + {provide: WorkspaceService, useValue: {}} + ] + }); + + service = TestBed.get(ConnectionWizardService); + service.connectRelationModel = connectRelationModelMock; + }); + + describe('getOptionalRequirementsByInstanceUniqueId', () => { + it('if no capability to match is sent in and isFromTo is true, ALL isFromTo==true requirements are returned', () => { + const requirements = service.getOptionalRequirementsByInstanceUniqueId(true); + expect(requirements['cap1'].length).toBe(4); + expect(requirements['cap2'].length).toBe(1); + }); + + it('if no capability to match is sent in and isFromTo is false, ALL isFromTo==false requirements are returned', () => { + const requirements = service.getOptionalRequirementsByInstanceUniqueId(false); + expect(requirements['cap1']).toBeUndefined(); + expect(requirements['cap2'].length).toBe(2); + }); + + it('if capability to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => { + const capability = Mock.of<Capability>({uniqueId: 'capability1'}); + const requirements = service.getOptionalRequirementsByInstanceUniqueId(true, capability); + expect(requirements['cap1'].length).toBe(1); + expect(requirements['cap2'].length).toBe(1); + }); + + it('if capability to match IS sent in and isFromTo is false, requirements with the same uniqueID and isFromTo==false are returned', () => { + const capability = Mock.of<Capability>({uniqueId: 'capability1'}); + const requirements = service.getOptionalRequirementsByInstanceUniqueId(false, capability); + expect(requirements['cap1']).toBeUndefined(); + expect(requirements['cap2'].length).toBe(1); + }); + }) + + describe('getOptionalCapabilitiesByInstanceUniqueId', () => { + it('if requirement to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => { + const requirement = Mock.of<Requirement>({uniqueId: 'requirement1'}); + const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true, requirement); + expect(capabilities['othertype'].length).toBe(1); + expect(capabilities['tosca']).toBeUndefined(); + }); + + it('if no requirement to match is sent in and isFromTo is true, a UNIQUE list of all capabilities with isFromTo==true are returned', () => { + const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true); + expect(capabilities['othertype'].length).toBe(1); + expect(capabilities['tosca'].length).toBe(2); + }); + + it('if no requirement to match is sent in and isFromTo is false, all capabilities with isFromTo==false are returned', () => { + const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(false); + expect(capabilities['othertype'].length).toBe(1); + expect(capabilities['tosca'].length).toBe(1); + }); + }); + +}); + @@ -1,20 +1,23 @@ import * as _ from "lodash"; -import {ConnectRelationModel} from "../../../models/graph/connectRelationModel"; +import {ConnectRelationModel} from "app/models/graph/connectRelationModel"; import {Injectable} from "@angular/core"; import { Requirement, Capability} from "app/models"; import {Dictionary} from "lodash"; import {Match, Component, PropertyFEModel} from "app/models"; +import {Store} from "@ngxs/store"; +import {WorkspaceService} from "../../../workspace/workspace.service"; @Injectable() export class ConnectionWizardService { connectRelationModel:ConnectRelationModel; - currentComponent:Component; selectedMatch:Match; changedCapabilityProperties:PropertyFEModel[]; - constructor() { + + constructor(private workspaceService: WorkspaceService) { this.changedCapabilityProperties = []; + } public setRelationMenuDirectiveObj = (connectRelationModel:ConnectRelationModel) => { @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`from-node-step component should match current snapshot 1`] = ` +<from-node-step + connectWizardService={[Function Object]} + preventBack={[Function Function]} + preventNext={[Function Function]} + updateSelectedReqOrCap={[Function Function]} +> + <select-requirement-or-capability /> +</from-node-step> +`; @@ -17,7 +17,6 @@ <select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap" [optionalCapabilitiesMap]="optionalCapabilitiesMap" [selectedReqOrCapModel]="connectWizardService.selectedMatch && (connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.requirement : connectWizardService.selectedMatch.capability)" - [currentComponent]="connectWizardService.currentComponent" [componentInstanceId]="connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId" (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)"> </select-requirement-or-capability>
\ No newline at end of file @@ -0,0 +1,114 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Capability, Match } from 'app/models'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { Requirement } from '../../../../../../models/requirement'; +import { ConnectionWizardService } from '../connection-wizard.service'; +import { FromNodeStepComponent } from './from-node-step.component'; + +describe('from-node-step component', () => { + + let fixture: ComponentFixture<FromNodeStepComponent>; + let connectionWizardServiceMockWithoutSelectedMatch: Partial<ConnectionWizardService>; + let connectionWizardServiceMockWithSelectedMatch: Partial<ConnectionWizardService>; + + const connectionWizardServiceMockSelectedMatchWithRequirements = {requirement: 'val'}; + + connectionWizardServiceMockWithoutSelectedMatch = { + getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5), + getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10), + + connectRelationModel: { + fromNode: { + componentInstance: { + uniqueId : 'testUniqueID' + } + } + } + }; + + connectionWizardServiceMockWithSelectedMatch = { + selectedMatch: connectionWizardServiceMockSelectedMatchWithRequirements, + getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5), + getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10) + }; + + let expectedConnectionWizardServiceMock = connectionWizardServiceMockWithoutSelectedMatch; + + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [FromNodeStepComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ConnectionWizardService, useValue: expectedConnectionWizardServiceMock} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(FromNodeStepComponent); + }); + }) + ); + + + it('should match current snapshot', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('preventBack return true - always', () => { + fixture.componentInstance.ngOnInit(); + const result = fixture.componentInstance.preventBack(); + expect(result).toEqual(true); + }); + + it('preventNext return true since selectedMatch does not exist in connectionWizardServiceMock', () => { + fixture.componentInstance.ngOnInit(); + const result = fixture.componentInstance.preventNext(); + expect(result).toEqual(true); + }); + + it('preventNext return false since to selectedMatch or selectedMatch.capability & selectedMatch.requirement does exist in connectionWizardServiceMock', () => { + fixture.componentInstance.connectWizardService = connectionWizardServiceMockWithSelectedMatch; + fixture.componentInstance.ngOnInit(); + const result = fixture.componentInstance.preventNext(); + expect(result).toEqual(false); + }); + + it('updateSelectedReqOrCap is called with instance of requirement, the selectMatch will be set to an Instance of Match of type Requirement', () => { + const requirement = new Requirement(); + fixture.componentInstance.updateSelectedReqOrCap(requirement); + const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch; + + expect(expectedSelectedMatch).toBeInstanceOf(Match); + expect(expectedSelectedMatch.capability).toBe(null); + expect(expectedSelectedMatch.fromNode).toBe('testUniqueID'); + expect(expectedSelectedMatch.isFromTo).toBe(true); + expect(expectedSelectedMatch.toNode).toBe(null); + expect(expectedSelectedMatch.requirement).toBeInstanceOf(Requirement); + }); + + it('updateSelectedReqOrCap is called with instance of capability, the selectMatch will be set to an Instance of Match of type Capability', () => { + const capability = new Capability(); + fixture.componentInstance.updateSelectedReqOrCap(capability); + const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch; + + expect(expectedSelectedMatch).toBeInstanceOf(Match); + expect(expectedSelectedMatch.requirement).toBe(null); + expect(expectedSelectedMatch.fromNode).toBe(null); + expect(expectedSelectedMatch.isFromTo).toBe(false); + expect(expectedSelectedMatch.toNode).toBe('testUniqueID'); + expect(expectedSelectedMatch.capability).toBeInstanceOf(Capability); + }); + + it('updateSelectedReqOrCap is called with null, the selectMatch will be set to null', () => { + fixture.componentInstance.updateSelectedReqOrCap(null); + const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch; + + expect(expectedSelectedMatch).toBe(null); + }); + +});
\ No newline at end of file @@ -1,11 +1,10 @@ -import {Component, OnInit, Inject, forwardRef} from "@angular/core"; -import {IStepComponent} from "../../../../models/wizard-step"; -import {Dictionary} from "lodash"; -import { Match} from "app/models"; -import {ConnectionWizardService} from "../connection-wizard.service"; -import {Requirement} from "../../../../models/requirement"; -import {Capability} from "../../../../models/capability"; -import {PropertyModel} from "../../../../models/properties"; +import { Component, forwardRef, Inject, OnInit } from '@angular/core'; +import { Match } from 'app/models'; +import { Capability } from 'app/models/capability'; +import { Requirement } from 'app/models/requirement'; +import { IStepComponent } from 'app/models/wizard-step'; +import { Dictionary } from 'lodash'; +import { ConnectionWizardService } from '../connection-wizard.service'; @Component({ selector: 'from-node-step', @@ -14,31 +13,31 @@ import {PropertyModel} from "../../../../models/properties"; export class FromNodeStepComponent implements IStepComponent, OnInit{ - constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {} - optionalRequirementsMap: Dictionary<Requirement[]>; optionalCapabilitiesMap: Dictionary<Capability[]>; - ngOnInit(){ + constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {} + + ngOnInit() { this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(true); this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(false); } - preventNext = ():boolean => { + preventNext = (): boolean => { return !this.connectWizardService.selectedMatch || (!this.connectWizardService.selectedMatch.capability && !this.connectWizardService.selectedMatch.requirement); } - preventBack = ():boolean => { + preventBack = (): boolean => { return true; } - private updateSelectedReqOrCap = (selected:Requirement|Capability):void => { - if(!selected){ + private updateSelectedReqOrCap = (selected: Requirement|Capability): void => { + if (!selected) { this.connectWizardService.selectedMatch = null; - } else if(selected instanceof Requirement){ + } else if (selected instanceof Requirement) { this.connectWizardService.selectedMatch = new Match(<Requirement>selected, null, true, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId, null); - } else{ - this.connectWizardService.selectedMatch = new Match(null,<Capability>selected , false, null, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId); + } else { + this.connectWizardService.selectedMatch = new Match(null, <Capability>selected , false, null, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId); } } @@ -13,8 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <div class="title"> <span class="capability-name"> {{(connectWizardService.selectedMatch.capability && connectWizardService.selectedMatch.capability.getTitle()) || connectWizardService.selectedMatch.relationship.relation.capability}} @@ -1,4 +1,4 @@ -@import '../../../../../assets/styles/variables'; +@import '../../../../../../../assets/styles/variables'; .title{ margin-bottom: 20px; .capability-name-label{ @@ -7,10 +7,10 @@ import {Component, Inject, forwardRef} from '@angular/core'; import {IStepComponent} from "app/models" import {ConnectionWizardService} from "../connection-wizard.service"; -import {PropertyFEModel} from "../../../../models/properties-inputs/property-fe-model"; -import {InstanceFePropertiesMap} from "../../../../models/properties-inputs/property-fe-map"; -import {PropertiesUtils} from "../../properties-assignment/services/properties.utils"; -import {ComponentInstanceServiceNg2} from "../../../services/component-instance-services/component-instance.service"; +import {PropertyFEModel} from "app/models/properties-inputs/property-fe-model"; +import {InstanceFePropertiesMap} from "app/models/properties-inputs/property-fe-map"; +import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; @Component({ selector: 'properties-step', @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`to-node-step component should match current snapshot 1`] = ` +<to-node-step + connectWizardService={[Function Object]} + optionalCapabilitiesMap={[Function Object]} + optionalRequirementsMap={[Function Object]} + preventBack={[Function Function]} + preventNext={[Function Function]} + updateSelectedReqOrCap={[Function Function]} +> + <select-requirement-or-capability /> +</to-node-step> +`; @@ -13,13 +13,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap" [optionalCapabilitiesMap]="optionalCapabilitiesMap" [selectedReqOrCapModel]="connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.capability : connectWizardService.selectedMatch.requirement" [selectedReqOrCapOption]="displayRequirementsOrCapabilities" - [currentComponent]="connectWizardService.currentComponent" [componentInstanceId]="connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId" (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)"> </select-requirement-or-capability>
\ No newline at end of file @@ -0,0 +1,71 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ToNodeStepComponent} from "./to-node-step.component"; +import {ConnectionWizardService} from "../connection-wizard.service"; +import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper"; +import {Match} from "../../../../../../models/graph/match-relation"; + + +describe('to-node-step component', () => { + + let fixture: ComponentFixture<ToNodeStepComponent>; + let connectionWizardServiceMock: Partial<ConnectionWizardService>; + + beforeEach( + async(() => { + + connectionWizardServiceMock = { + // selectedMatch: new Match(null, null, true, '',''), + selectedMatch: { + isFromTo: false + }, + getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5), + getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10) + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ToNodeStepComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ConnectionWizardService, useValue: connectionWizardServiceMock} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ToNodeStepComponent); + }); + }) + ); + + + it('should match current snapshot', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should test the ngOnInit with isFromTo = false', () => { + const component = TestBed.createComponent(ToNodeStepComponent); + let service = TestBed.get(ConnectionWizardService); + service.selectedMatch.isFromTo = false; + component.componentInstance.ngOnInit(); + expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Requirement"); + expect(connectionWizardServiceMock.getOptionalRequirementsByInstanceUniqueId).toHaveBeenCalledWith(false, connectionWizardServiceMock.selectedMatch.capability); + expect(component.componentInstance.optionalRequirementsMap).toEqual(5); + expect(component.componentInstance.optionalCapabilitiesMap).toEqual({}); + }); + + + it('should test the ngOnInit with isFromTo = true', () => { + const component = TestBed.createComponent(ToNodeStepComponent); + let service = TestBed.get(ConnectionWizardService); + service.selectedMatch.isFromTo = true; + component.componentInstance.ngOnInit(); + expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Capability"); + expect(connectionWizardServiceMock.getOptionalCapabilitiesByInstanceUniqueId).toHaveBeenCalledWith(true, connectionWizardServiceMock.selectedMatch.requirement); + expect(component.componentInstance.optionalCapabilitiesMap).toEqual(10); + expect(component.componentInstance.optionalRequirementsMap).toEqual({}); + }); + +});
\ No newline at end of file @@ -2,10 +2,10 @@ import {Component, forwardRef, Inject} from '@angular/core'; import {IStepComponent} from "app/models" import {Dictionary} from "lodash"; import {ConnectionWizardService} from "../connection-wizard.service"; -import {Match} from "../../../../models/graph/match-relation"; -import {Requirement} from "../../../../models/requirement"; -import {Capability} from "../../../../models/capability"; -import {PropertyModel} from "../../../../models/properties"; +import {Match} from "app/models/graph/match-relation"; +import {Requirement} from "app/models/requirement"; +import {Capability} from "app/models/capability"; +import {PropertyModel} from "app/models/properties"; @Component({ selector: 'to-node-step', @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`artifact form component should match current snapshot of artifact form component 1`] = ` +<link-row + source={[Function Array]} + srcCP={[Function Array]} + target={[Function Array]} + targetCP={[Function Array]} +> + <ui-element-dropdown + class="cell link-selector" + data-tests-id="linkSrc" + /><ui-element-dropdown + class="cell link-selector" + data-tests-id="linkSrcCP" + /><ui-element-dropdown + class="cell link-selector" + data-tests-id="linkTarget" + /><ui-element-dropdown + class="cell link-selector" + data-tests-id="linkTargetCP" + /><div + class="cell remove" + data-tests-id="removeLnk" + > + + </div> +</link-row> +`; @@ -1,4 +1,4 @@ -@import './../../../../../assets/styles/variables.less'; +@import './../../../../../../../assets/styles/variables.less'; .remove { display: flex; align-items: center; @@ -0,0 +1,478 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {CacheService} from "../../../../../services/cache.service"; +import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {LinkRowComponent} from "./link-row.component"; +import {DropdownValue} from "../../../../../components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {MapItemData, ServicePathMapItem} from "../../../../../../models/graph/nodes-and-links-map"; + +describe('artifact form component', () => { + + let fixture: ComponentFixture<LinkRowComponent>; + let cacheServiceMock: Partial<CacheService>; + + beforeEach( + async(() => { + + + cacheServiceMock = { + contains: jest.fn(), + remove: jest.fn(), + set: jest.fn(), + get: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [LinkRowComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [] + , + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(LinkRowComponent); + }); + }) + ); + + + it('should match current snapshot of artifact form component', () => { + expect(fixture).toMatchSnapshot(); + }); + + + it('ngOnChanges() -> in case data exist -> call to parseInitialData()' ,() => { + // init values / mock functions + let data = 'something'; + fixture.componentInstance.parseInitialData = jest.fn(); + fixture.componentInstance.data = data; + + // call to the tested function + fixture.componentInstance.ngOnChanges(); + + // expect that + expect(fixture.componentInstance.parseInitialData).toHaveBeenCalledWith(data); + }); + + it('onSourceSelected() -> in case id -> srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP should be updated accordingly' ,() => { + // init values / mock functions + let id = 'id'; + let data = 'data'; + let link = { + fromCP:'testVal', + toNode:'testVal', + toCP:'testVal' + } + let target = ['val1', 'val2']; + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.data = data; + fixture.componentInstance.link = link; + fixture.componentInstance.target = target; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onSourceSelected(id); + + // expect that + expect(fixture.componentInstance.findOptions).toHaveBeenCalledWith(data, id); + expect(fixture.componentInstance.srcCP).toBe('dummyConvertedVal'); + expect(fixture.componentInstance.link.fromCP).toBe(''); + expect(fixture.componentInstance.link.toNode).toBe(''); + expect(fixture.componentInstance.link.toCP).toBe(''); + expect(fixture.componentInstance.target.length).toBe(0); + expect(fixture.componentInstance.targetCP.length).toBe(0); + }); + + it('onSourceSelected() -> in case id undefined -> No Change to srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP' ,() => { + // init values / mock functions + let id; + let data = 'data'; + let link = { + fromCP:'testVal', + toNode:'testVal', + toCP:'testVal' + } + let target = ['val1', 'val2']; + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.data = data; + fixture.componentInstance.link = link; + fixture.componentInstance.target = target; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onSourceSelected(id); + + // expect that + expect(fixture.componentInstance.link.fromCP).toBe(link.fromCP); + expect(fixture.componentInstance.link.toNode).toBe(link.toNode); + expect(fixture.componentInstance.link.toCP).toBe(link.toCP); + expect(fixture.componentInstance.target.length).toBe(2); + expect(fixture.componentInstance.target[0]).toBe('val1') + expect(fixture.componentInstance.targetCP.length).toBe(2); + expect(fixture.componentInstance.targetCP[1]).toBe('val2'); + }); + + it('onSrcCPSelected() -> in case id -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => { + // init values / mock functions + let id = 'id'; + let link = { + fromNode:'testVal', + toCPOriginId: 'initValue_ShouldBeChanged' + }; + let option1 = { + id: 'something' + }; + let option2 = { + id: 'id', + data: {"ownerId":1} + }; + + fixture.componentInstance.link = link; + fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + + // call to the tested function + fixture.componentInstance.onSrcCPSelected(id); + + // expect that + expect(fixture.componentInstance.target).toBe('dummyConvertedVal'); + expect(fixture.componentInstance.link.fromCPOriginId).toBe(option2.data.ownerId); + expect(fixture.componentInstance.link.toNode).toBe(''); + expect(fixture.componentInstance.link.toCP).toBe(''); + expect(fixture.componentInstance.targetCP.length).toBe(0); + + }); + + it('onSrcCPSelected() -> in case id undefined -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => { + // init values / mock functions + let id; + + let targetInput:Array<DropdownValue> = [{value:'Value', label:'Label', hidden:true, selected:true}]; + + let linkInput = { + fromCPOriginId:'expectedLinkFromCPOriginId', + toNode:'expectedLinkToNode', + toCP:'expectedLinkToCP', + // Link Object + canEdit:true, + canRemove:true, + isFirst:true, + // ForwardingPathLink Object + ownerId:'', + fromNode:'', + fromCP:'', + toCPOriginId:'' + } + + fixture.componentInstance.target = targetInput; + fixture.componentInstance.link = linkInput; + fixture.componentInstance.targetCP = targetInput; + + + // call to the tested function + fixture.componentInstance.onSrcCPSelected(id); + + // expect that + expect(fixture.componentInstance.target).toBe(targetInput); + expect(fixture.componentInstance.link.fromCPOriginId).toBe('expectedLinkFromCPOriginId'); + expect(fixture.componentInstance.link.toNode).toBe('expectedLinkToNode'); + expect(fixture.componentInstance.link.toCP).toBe('expectedLinkToCP'); + expect(fixture.componentInstance.targetCP.length).toBe(1); + }); + + it('onTargetSelected() -> in case id -> Verify targetCP & link.toCP' ,() => { + // init values / mock functions + let id = 'id'; + let link = { + toCP:'testVal' + } + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.link = link; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onTargetSelected(id); + + // expect that + expect(fixture.componentInstance.targetCP).toBe('dummyConvertedVal'); + expect(fixture.componentInstance.link.toCP).toBe(''); + + }); + + it('onTargetSelected() -> in case id undefined -> Verify targetCP & link.toCP' ,() => { + // init values / mock functions + let id; + let link = { + toCP:'toCP_testVal' + } + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.link = link; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onTargetSelected(id); + + // expect that + expect(fixture.componentInstance.targetCP.length).toBe(2); + expect(fixture.componentInstance.targetCP).toEqual(['val1', 'val2']); + expect(fixture.componentInstance.link.toCP).toBe('toCP_testVal'); + }); + + it('onTargetCPSelected() -> in case id -> Validate toCPOriginId' ,() => { + // init values / mock functions + let id = 'id'; + let link = { + toNode:'testVal', + toCPOriginId: 'initValue_ShouldBeChanged' + }; + let option1 = { + id: 'something' + }; + let option2 = { + id: 'id', + data: {"ownerId":1} + }; + fixture.componentInstance.link = link; + fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]); + + // call to the tested function + fixture.componentInstance.onTargetCPSelected(id); + + // expect that + expect(fixture.componentInstance.link.toCPOriginId).toBe(option2.data.ownerId); + }); + + it('onTargetCPSelected() -> in case id undefined -> Validate toCPOriginId' ,() => { + // init values / mock functions + let id; + let link = { + toNode:'testVal', + toCPOriginId: 'initValue_ShouldRemain' + }; + let option1 = { + id: 'something' + }; + let option2 = { + id: 'id', + data: {"ownerId":1} + }; + fixture.componentInstance.link = link; + fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]); + + // call to the tested function + fixture.componentInstance.onTargetCPSelected(id); + + // expect that + expect(fixture.componentInstance.link.toCPOriginId).toBe('initValue_ShouldRemain'); + }); + + + it('findOptions() -> in case item.data.options -> Validate return item.data.options' ,() => { + // init values / mock functions + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const innerServicePathItem: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + const mapItemData1: MapItemData = { id: 'mapItemData1_id', name: 'mapItemData1_name', options: [innerServicePathItem]}; + + const servicePathItem: ServicePathMapItem = { id: 'servicePathItem_id', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem]; + + let nodeOrCPId: string = servicePathItem.id; + + // call to the tested function + let res = fixture.componentInstance.findOptions(arrServicePathItems, nodeOrCPId); + + // expect that + expect(res).toEqual([innerServicePathItem]); + }); + + it('findOptions() -> in case NOT item || item.data || item.data.options -> Validate return null' ,() => { + // init values / mock functions + let item = [{ + // data: { + data:{ + name:'data_name', + id: 'data_id' + }, + name:'name', + id: 'id' + // } + }]; + let items: Array<ServicePathMapItem> = item; + let nodeOrCPId: string = 'someString'; + + // call to the tested function + let res = fixture.componentInstance.findOptions(items, nodeOrCPId); + + // expect that + expect(res).toBe(null); + }); + + it('convertValuesToDropDownOptions() -> Verify that the result is sorted' ,() => { + // init values / mock functions + const mapItemData1: MapItemData = { id: 'Z_ID', name: 'Z_NAME'}; + const servicePathItem1: ServicePathMapItem = { id: 'Z_servicePathItem_id', data: mapItemData1 }; + + const mapItemData2: MapItemData = { id: 'A_ID', name: 'A_NAME'}; + const servicePathItem2: ServicePathMapItem = { id: 'A_servicePathItem_id', data: mapItemData2 }; + + const mapItemData3: MapItemData = { id: 'M_ID', name: 'M_NAME'}; + const servicePathItem3: ServicePathMapItem = { id: 'M_servicePathItem_id', data: mapItemData3 }; + + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1, servicePathItem2, servicePathItem3]; + + // call to the tested function + let res = fixture.componentInstance.convertValuesToDropDownOptions(arrServicePathItems); + + // expect that + expect(res.length).toBe(3); + expect(res[0].value).toBe("A_servicePathItem_id"); + expect(res[0].label).toBe("A_NAME"); + expect(res[1].value).toBe("M_servicePathItem_id"); + expect(res[1].label).toBe("M_NAME"); + expect(res[2].value).toBe("Z_servicePathItem_id"); + expect(res[2].label).toBe("Z_NAME"); + + }); + + it('parseInitialData() -> link.fromNode Exist => Verify srcCP' ,() => { + // init values / mock functions + + //Simulate Array<ServicePathMapItem to pass to the function + const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'}; + const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1]; + + //Simulate link + let link = { + fromNode:'testVal' + }; + fixture.componentInstance.link = link; + + //Simulate the response from convertValuesToDropDownOptions() + const value = "expected_id_fromNode"; + const label = "expected_label_fromNode" + let result:Array<DropdownValue> = []; + result[0] = new DropdownValue(value, label); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result); + + //Simulate the response from findOptions() + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + fixture.componentInstance.findOptions = jest.fn(() => options); + + + // call to the tested function + fixture.componentInstance.parseInitialData(arrServicePathItems); + + // expect that + expect(fixture.componentInstance.srcCP.length).toBe(1); + expect(fixture.componentInstance.srcCP[0]).toEqual({ + "value": value, + "label": label, + "hidden": false, + "selected": false + }); + }); + + it('parseInitialData() -> link.fromNode & link.fromCP Exist => Verify srcCP' ,() => { + // init values / mock functions + + //Simulate Array<ServicePathMapItem to pass to the function + const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'}; + const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1]; + + //Simulate link + let link = { + fromNode:'testVal', + fromCP: 'testVal' + }; + fixture.componentInstance.link = link; + + //Simulate the response from convertValuesToDropDownOptions() + const value = "expected_id_fromNode_and_fromCP"; + const label = "expected_label_fromNode_and_fromCP" + let result:Array<DropdownValue> = []; + result[0] = new DropdownValue(value, label); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result); + + //Simulate the response from findOptions() + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + fixture.componentInstance.findOptions = jest.fn(() => options); + + + // call to the tested function + fixture.componentInstance.parseInitialData(arrServicePathItems); + + // expect that + expect(fixture.componentInstance.srcCP.length).toBe(1); + expect(fixture.componentInstance.srcCP[0]).toEqual({ + "value": value, + "label": label, + "hidden": false, + "selected": false + }); + }); + + + it('parseInitialData() -> link.fromNode & link.fromCP & link.toNode Exist => Verify srcCP' ,() => { + // init values / mock functions + + //Simulate Array<ServicePathMapItem to pass to the function + const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'}; + const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1]; + + //Simulate link + let link = { + fromNode:'testVal', + fromCP: 'testVal', + toNode: 'testVal' + }; + fixture.componentInstance.link = link; + + //Simulate the response from convertValuesToDropDownOptions() + const value = "expected_id_fromNode_and_fromCP_and_toNode"; + const label = "expected_label_fromNode_and_fromCP_and_toNode" + let result:Array<DropdownValue> = []; + result[0] = new DropdownValue(value, label); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result); + + //Simulate the response from findOptions() + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + fixture.componentInstance.findOptions = jest.fn(() => options); + + + // call to the tested function + fixture.componentInstance.parseInitialData(arrServicePathItems); + + // expect that + expect(fixture.componentInstance.srcCP.length).toBe(1); + expect(fixture.componentInstance.srcCP[0]).toEqual({ + "value": value, + "label": label, + "hidden": false, + "selected": false + }); + }); + + + +});
\ No newline at end of file @@ -2,6 +2,7 @@ import {Component, Input} from '@angular/core'; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; import {Link} from './link.model'; import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map"; +import * as _ from "lodash"; @Component({ selector: 'link-row', @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="service-path-creator"> <form class="w-sdc-form"> <div class="i-sdc-form-item" > @@ -1,4 +1,4 @@ -@import './../../../../assets/styles/variables.less'; +@import './../../../../../../assets/styles/variables.less'; .service-path-creator { font-family: @font-opensans-regular; .separator-buttons { @@ -25,6 +25,7 @@ import {ForwardingPath} from 'app/models/forwarding-path'; import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; import {ForwardingPathLink} from "app/models/forwarding-path-link"; import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; @Component({ selector: 'service-path-creator', @@ -43,7 +44,8 @@ export class ServicePathCreatorComponent { forwardingPath:ForwardingPath; //isExtendAllowed:boolean = false; - constructor(private serviceService: ServiceServiceNg2) { + constructor(private serviceService: ServiceServiceNg2, + private compositionService: CompositionService) { this.forwardingPath = new ForwardingPath(); this.links = [new Link(new ForwardingPathLink('', '', '', '', '', ''), true, false, true)]; this.headers = ['Source', 'Source Connection Point', 'Target', 'Target Connection Point', ' ']; @@ -57,7 +59,7 @@ export class ServicePathCreatorComponent { } ngOnInit() { - this.serviceService.getNodesAndLinksMap(this.input.service).subscribe((res:any) => { + this.serviceService.getNodesAndLinksMap(this.input.serviceId).subscribe((res:any) => { this.linksMap = res; }); this.processExistingPath(); @@ -66,7 +68,7 @@ export class ServicePathCreatorComponent { private processExistingPath() { if (this.input.pathId) { - let forwardingPath = <ForwardingPath>{...this.input.service.forwardingPaths[this.input.pathId]}; + let forwardingPath = <ForwardingPath>{...this.compositionService.forwardingPaths[this.input.pathId]}; this.forwardingPath.name = forwardingPath.name; this.forwardingPath.destinationPortNumber = forwardingPath.destinationPortNumber; this.forwardingPath.protocol = forwardingPath.protocol; @@ -1,4 +1,4 @@ -@import './../../../../../assets/styles/variables.less'; +@import './../../../../../../assets/styles/variables.less'; .service-path-selector { margin: 10px 35px 10px 0; display: flex; @@ -0,0 +1,142 @@ +import {Component, Input, KeyValueDiffer, IterableDiffers, KeyValueDiffers, DoCheck} from '@angular/core'; +import {Service} from "app/models/components/service"; +import {TranslateService} from "app/ng2/shared/translator/translate.service"; +import {ForwardingPath} from "app/models/forwarding-path"; +import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; +import {EventListenerService} from "app/services/event-listener-service"; +import {GRAPH_EVENTS} from "app/utils/constants"; + +@Component({ + selector: 'service-path-selector', + templateUrl: './service-path-selector.component.html', + styleUrls: ['service-path-selector.component.less'] +}) + +export class ServicePathSelectorComponent { + + defaultSelectedId: string; + hideAllValue: string; + hideAllId: string = '0'; + showAllValue: string; + showAllId: string = '1'; + + paths: Array<ForwardingPath> = []; + dropdownOptions: Array<DropdownValue>; + differ: KeyValueDiffer<string, ForwardingPath>; + + @Input() drawPath: Function; + @Input() deletePaths: Function; + @Input() selectedPathId: string; + + constructor(private differs: KeyValueDiffers, + private translateService: TranslateService, + private compositionService: CompositionService, + private eventListenerService: EventListenerService + ) { + + this.defaultSelectedId = this.hideAllId; + this.convertPathsToDropdownOptions(); + + this.translateService.languageChangedObservable.subscribe(lang => { + this.hideAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_HIDE_ALL_VALUE"); + this.showAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_SHOW_ALL_VALUE"); + this.convertPathsToDropdownOptions(); + }); + + } + + ngOnInit(): void { + + this.selectedPathId = this.defaultSelectedId; + this.differ = this.differs.find(this.compositionService.forwardingPaths).create(); + this.updatePaths(); + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, (createdId) => { + this.selectedPathId = createdId; + this.updatePaths(); + } ) + + } + + updatePaths(): void { + + const pathsChanged = this.differ.diff(this.compositionService.forwardingPaths); + + if (pathsChanged) { + let oldPaths = _.cloneDeep(this.paths); + this.populatePathsFromService(); + + if (!(_.isEqual(oldPaths, this.paths))) { + this.convertPathsToDropdownOptions(); + + let temp = this.selectedPathId; + this.selectedPathId = '-1'; + + setTimeout(() => { + this.selectedPathId = temp; + this.onSelectPath(); + }, 0); + } + } + + } + + populatePathsFromService(): void { + + this.paths = []; + + _.forEach(this.compositionService.forwardingPaths, path => { + this.paths.push(path); + }); + this.paths.sort((a: ForwardingPath, b: ForwardingPath) => { + return a.name.localeCompare(b.name); + }); + + } + + convertPathsToDropdownOptions(): void { + + let result = [ + new DropdownValue(this.hideAllId, this.hideAllValue), + new DropdownValue(this.showAllId, this.showAllValue) + ]; + + _.forEach(this.paths, (value: ForwardingPath) => { + result[result.length] = new DropdownValue(value.uniqueId, value.name); + }); + + this.dropdownOptions = result; + + } + + onSelectPath = (): void => { + + if (this.selectedPathId !== '-1') { + this.deletePaths(); + + switch (this.selectedPathId) { + case this.hideAllId: + break; + + case this.showAllId: + _.forEach(this.paths, path => + this.drawPath(path) + ); + break; + + default: + let path = this.paths.find(path => + path.uniqueId === this.selectedPathId + ); + if (!path) { + this.selectedPathId = this.defaultSelectedId; + this.onSelectPath(); // currently does nothing in default case, but if one day it does, we want the selection to behave accordingly. + break; + } + this.drawPath(path); + break; + } + } + + } +} @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import {CommonModule} from "@angular/common"; import {ServicePathSelectorComponent} from "./service-path-selector.component"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; @NgModule({ declarations: [ @@ -11,11 +12,11 @@ import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; CommonModule, UiElementsModule ], - exports: [], + exports: [ServicePathSelectorComponent], entryComponents: [ ServicePathSelectorComponent ], - providers: [] + providers: [CompositionService] }) export class ServicePathSelectorModule { }
\ No newline at end of file @@ -1,19 +1,3 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - <div class="service-path-list"> <div class="add-path-link" *ngIf="!isViewOnly"><a (click)="onAddServicePath()" data-tests-id="add-service-path-lnk" >+ Add Flow</a></div> <div class="generic-table table-container" > @@ -1,4 +1,4 @@ -@import './../../../../assets/styles/variables.less'; +@import './../../../../../../assets/styles/variables.less'; .add-path-link { display: flex; @@ -24,6 +24,7 @@ import {ForwardingPath} from "app/models/forwarding-path"; import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; import {ModalService} from "app/ng2/services/modal.service"; import {ModalComponent} from "app/ng2/components/ui/modal/modal.component"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; @Component({ selector: 'service-paths-list', @@ -31,7 +32,7 @@ import {ModalComponent} from "app/ng2/components/ui/modal/modal.component"; styleUrls:['service-paths-list.component.less'], providers: [ServiceServiceNg2, ModalService] }) -export default class ServicePathsListComponent { +export class ServicePathsListComponent { modalInstance: ComponentRef<ModalComponent>; headers: Array<string> = []; paths: Array<ForwardingPath> = []; @@ -40,12 +41,13 @@ export default class ServicePathsListComponent { onEditServicePath: Function; isViewOnly: boolean; - constructor(private serviceService:ServiceServiceNg2) { + constructor(private serviceService:ServiceServiceNg2, + private compositionService: CompositionService) { this.headers = ['Flow Name','Actions']; } ngOnInit() { - _.forEach(this.input.service.forwardingPaths, (path: ForwardingPath)=> { + _.forEach(this.compositionService.forwardingPaths, (path: ForwardingPath)=> { this.paths[this.paths.length] = path; }); this.paths.sort((a:ForwardingPath, b:ForwardingPath)=> { @@ -57,8 +59,8 @@ export default class ServicePathsListComponent { } deletePath = (id:string):void => { - this.serviceService.deleteServicePath(this.input.service, id).subscribe((res:any) => { - delete this.input.service.forwardingPaths[id]; + this.serviceService.deleteServicePath(this.input.serviceId, id).subscribe((res:any) => { + delete this.compositionService.forwardingPaths[id]; this.paths = this.paths.filter(function(path){ return path.uniqueId !== id; }); @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; import {CommonModule} from "@angular/common"; -import ServicePathsListComponent from "./service-paths-list.component"; +import { ServicePathsListComponent } from "./service-paths-list.component"; @NgModule({ declarations: [ @@ -0,0 +1,268 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import {ComponentInstance, Match, CompositionCiLinkBase, CompositionCiNodeUcpeCp} from "app/models"; +import {Dictionary, GraphUIObjects} from "app/utils"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {Injectable} from "@angular/core"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service"; +import {RequirementsGroup} from "app/models/requirement"; +import {CapabilitiesGroup} from "app/models/capability"; +import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import {NotificationsService} from "onap-ui-angular/dist/notifications/services/notifications.service"; +import {NotificationSettings} from "onap-ui-angular/dist/notifications/utilities/notification.config"; + +export interface RequirementAndCapabilities { + capabilities: CapabilitiesGroup; + requirements: RequirementsGroup; +} + +@Injectable() +export class CompositionGraphGeneralUtils { + + public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, RequirementAndCapabilities>(); + + constructor(private commonGraphUtils: CommonGraphUtils, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private queueServiceUtils: QueueServiceUtils, + private componentService: ComponentServiceNg2, + private topologyTemplateService: TopologyTemplateService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService) { + } + + /** + * Get the offset for the link creation Menu + * @param point + * @returns {Cy.Position} + */ + public calcMenuOffset: Function = (point: Cy.Position): Cy.Position => { + point.x = point.x + 60; + point.y = point.y + 105; + return point; + }; + + /** + * return the top left position of the link menu + * @param cy + * @param targetNodePosition + * @returns {Cy.Position} + */ + public getLinkMenuPosition = (cy: Cy.Instance, targetNodePosition: Cy.Position) => { + let menuPosition: Cy.Position = this.calcMenuOffset(targetNodePosition); //get the link mid point + if ($(document.body).height() < menuPosition.y + GraphUIObjects.LINK_MENU_HEIGHT + $(document.getElementsByClassName('sdc-composition-graph-wrapper')).offset().top) { // if position menu is overflow bottom + menuPosition.y = $(document.body).height() - GraphUIObjects.TOP_HEADER_HEIGHT - GraphUIObjects.LINK_MENU_HEIGHT; + } + return menuPosition; + }; + + public zoomGraphTo = (cy: Cy.Instance, zoomLevel: number): void => { + let zy = cy.height() / 2; + let zx = cy.width() / 2; + cy.zoom({ + level: zoomLevel, + renderedPosition: {x: zx, y: zy} + }); + } + + //saves the current zoom, and then sets a temporary maximum zoom for zoomAll, and then reverts to old value + public zoomAllWithMax = (cy: Cy.Instance, maxZoom: number): void => { + + let oldMaxZoom: number = cy.maxZoom(); + + cy.maxZoom(maxZoom); + this.zoomAll(cy); + cy.maxZoom(oldMaxZoom); + + }; + + //Zooms to fit all of the nodes in the collection passed in. If no nodes are passed in, will zoom to fit all nodes on graph + public zoomAll = (cy: Cy.Instance, nodes?: Cy.CollectionNodes): void => { + + if (!nodes || !nodes.length) { + nodes = cy.nodes(); + } + + cy.resize(); + cy.animate({ + fit: {eles: nodes, padding: 20}, + center: {eles: nodes} + }, {duration: 400}); + }; + + /** + * will return true/false if two nodes overlapping + * + * @param graph node + */ + private isNodesOverlapping(node: Cy.CollectionFirstNode, draggedNode: Cy.CollectionFirstNode): boolean { + + let nodeBoundingBox: Cy.BoundingBox = node.renderedBoundingBox(); + let secondNodeBoundingBox: Cy.BoundingBox = draggedNode.renderedBoundingBox(); + + return this.isBBoxOverlapping(nodeBoundingBox, secondNodeBoundingBox); + } + + /** + * Checks whether the bounding boxes of two nodes are overlapping on any side + * @param nodeOneBBox + * @param nodeTwoBBox + * @returns {boolean} + */ + private isBBoxOverlapping(nodeOneBBox: Cy.BoundingBox, nodeTwoBBox: Cy.BoundingBox) { + return (((nodeOneBBox.x1 < nodeTwoBBox.x1 && nodeOneBBox.x2 > nodeTwoBBox.x1) || + (nodeOneBBox.x1 < nodeTwoBBox.x2 && nodeOneBBox.x2 > nodeTwoBBox.x2) || + (nodeTwoBBox.x1 < nodeOneBBox.x1 && nodeTwoBBox.x2 > nodeOneBBox.x2)) && + ((nodeOneBBox.y1 < nodeTwoBBox.y1 && nodeOneBBox.y2 > nodeTwoBBox.y1) || + (nodeOneBBox.y1 < nodeTwoBBox.y2 && nodeOneBBox.y2 > nodeTwoBBox.y2) || + (nodeTwoBBox.y1 < nodeOneBBox.y1 && nodeTwoBBox.y2 > nodeOneBBox.y2))) + } + + /** + * Checks whether a specific topologyTemplate instance can be hosted on the UCPE instance + * @param cy - Cytoscape instance + * @param fromUcpeInstance + * @param toComponentInstance + * @returns {Match} + */ + public canBeHostedOn(cy: Cy.Instance, fromUcpeInstance: ComponentInstance, toComponentInstance: ComponentInstance): Match { + + let matches: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy)); + let hostedOnMatch: Match = _.find(matches, (match: Match) => { + return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container'; + }); + + return hostedOnMatch; + }; + + /** + * Checks whether node can be dropped into UCPE + * @param cy + * @param nodeToInsert + * @param ucpeNode + * @returns {boolean} + */ + private isValidDropInsideUCPE(cy: Cy.Instance, nodeToInsert: ComponentInstance, ucpeNode: ComponentInstance): boolean { + + let hostedOnMatch: Match = this.canBeHostedOn(cy, ucpeNode, nodeToInsert); + let result: boolean = !angular.isUndefined(hostedOnMatch) || nodeToInsert.isVl(); //group validation + return result; + + }; + + /** + * For drops from palette, checks whether the node can be dropped. If node is being held over another node, check if capable of hosting + * @param cy + * @param pseudoNodeBBox + * @param paletteComponentInstance + * @returns {boolean} + */ + public isPaletteDropValid(cy: Cy.Instance, pseudoNodeBBox: Cy.BoundingBox) { + + let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { + if (this.isBBoxOverlapping(pseudoNodeBBox, graphNode.renderedBoundingBox())) { + return true; + } + return false; + }); + + return illegalOverlappingNodes.length === 0; + } + + /** + * will return true/false if a drop of a single node is valid + * + * @param graph node + */ + public isValidDrop(cy: Cy.Instance, draggedNode: Cy.CollectionFirstNode): boolean { + + let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { //all sdc nodes, removing child nodes (childe node allways collaps + + if (draggedNode.data().isUcpe && (graphNode.isChild() || graphNode.data().isInsideGroup)) { //ucpe cps always inside ucpe, no overlapping + return false; + } + if (draggedNode.data().isInsideGroup && (!draggedNode.active() || graphNode.data().isUcpe)) { + return false; + } + + if (!draggedNode.data().isUcpe && !(draggedNode.data() instanceof CompositionCiNodeUcpeCp) && graphNode.data().isUcpe) { //case we are dragging a node into UCPE + let isEntirelyInUCPE: boolean = this.commonGraphUtils.isFirstBoxContainsInSecondBox(draggedNode.renderedBoundingBox(), graphNode.renderedBoundingBox()); + if (isEntirelyInUCPE) { + if (this.isValidDropInsideUCPE(cy, draggedNode.data().componentInstance, graphNode.data().componentInstance)) { //if this is valid insert into ucpe, we return false - no illegal overlapping nodes + return false; + } + } + } + return graphNode.data().id !== draggedNode.data().id && this.isNodesOverlapping(draggedNode, graphNode); + + }); + // return false; + return illegalOverlappingNodes.length === 0; + }; + + /** + * will return true/false if the move of the nodes is valid (no node overlapping and verifying if insert into UCPE is valid) + * + * @param nodesArray - the selected drags nodes + */ + public isGroupValidDrop(cy: Cy.Instance, nodesArray: Cy.CollectionNodes): boolean { + let filterDraggedNodes = nodesArray.filter('[?isDraggable]'); + let isValidDrop = _.every(filterDraggedNodes, (node: Cy.CollectionFirstNode) => { + return this.isValidDrop(cy, node); + + }); + return isValidDrop; + }; + + /** + * get all links in diagram + * @param cy + * @returns {any[]|boolean[]} + */ + public getAllCompositionCiLinks = (cy: Cy.Instance): Array<CompositionCiLinkBase> => { + return _.map(cy.edges("[isSdcElement]"), (edge: Cy.CollectionEdges) => { + return edge.data(); + }); + }; + + /** + * + * @param blockAction - true/false if this is a block action + * @param instances + * @param component + */ + public pushMultipleUpdateComponentInstancesRequestToQueue = (instances: Array<ComponentInstance>): void => { + this.queueServiceUtils.addNonBlockingUIAction(() => { + return new Promise<boolean>((resolve, reject) => { + let uniqueId = this.workspaceService.metadata.uniqueId; + let topologyType = this.workspaceService.metadata.componentType; + this.topologyTemplateService.updateMultipleComponentInstances(uniqueId, topologyType, instances).subscribe(instancesResult => { + this.compositionService.updateComponentInstances(instancesResult); + resolve(true); + }); + }); + }); + } +} @@ -0,0 +1,342 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +/** + * Created by obarda on 6/28/2016. + */ +import * as _ from "lodash"; +import {GraphUIObjects} from "app/utils"; +import { + Match, + CompositionCiNodeBase, + RelationshipModel, + ConnectRelationModel, + LinksFactory, + Component, + LinkMenu, + Point, + CompositionCiLinkBase, + Requirement, + Capability, + Relationship, + ComponentInstance +} from "app/models"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link"; +import {Injectable} from "@angular/core"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; + +@Injectable() +export class CompositionGraphLinkUtils { + + constructor(private linksFactory: LinksFactory, + private generalGraphUtils: CompositionGraphGeneralUtils, + private commonGraphUtils: CommonGraphUtils, + private queueServiceUtils: QueueServiceUtils, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private topologyTemplateService: TopologyTemplateService, + private loaderService: SdcUiServices.LoaderService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService) { + + + } + + /** + * Delete the link on server and then remove it from graph + * @param component + * @param releaseLoading - true/false release the loader when finished + * @param link - the link to delete + */ + public deleteLink = (cy: Cy.Instance, component: Component, releaseLoading: boolean, link: Cy.CollectionEdges) => { + + this.loaderService.activate(); + this.queueServiceUtils.addBlockingUIAction(() => { + this.topologyTemplateService.deleteRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.data().relation).subscribe((deletedRelation) => { + this.compositionService.deleteRelation(deletedRelation); + cy.remove(link); + this.loaderService.deactivate(); + }, (error) => {this.loaderService.deactivate()}); + }); + }; + + /** + * create the link on server and than draw it on graph + * @param link - the link to create + * @param cy + * @param component + */ + public createLink = (link: CompositionCiLinkBase, cy: Cy.Instance): void => { + + this.loaderService.activate(); + link.updateLinkDirection(); + + this.queueServiceUtils.addBlockingUIAction(() => { + this.topologyTemplateService.createRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.relation).subscribe((relation) => { + link.setRelation(relation); + this.insertLinkToGraph(cy, link); + this.compositionService.addRelation(relation); + this.loaderService.deactivate(); + }, (error) => {this.loaderService.deactivate()}) + }); + }; + + private createSimpleLink = (match: Match, cy: Cy.Instance): void => { + let newRelation: RelationshipModel = match.matchToRelationModel(); + let linkObg: CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, newRelation, newRelation.relationships[0]); + this.createLink(linkObg, cy); + }; + + public createLinkFromMenu = (cy: Cy.Instance, chosenMatch: Match): void => { + + if (chosenMatch) { + if (chosenMatch && chosenMatch instanceof Match) { + this.createSimpleLink(chosenMatch, cy); + } + } + } + + /** + * open the connect link menu if the link drawn is valid - match requirements & capabilities + * @param cy + * @param fromNode + * @param toNode + * @returns {any} + */ + public onLinkDrawn(cy: Cy.Instance, fromNode: Cy.CollectionFirstNode, toNode: Cy.CollectionFirstNode): ConnectRelationModel { + + let linkModel: Array<CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy); + + let possibleRelations: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance, + toNode.data().componentInstance, linkModel); + + //if found possibleRelations between the nodes we create relation menu directive and open the link menu + if (possibleRelations.length) { + // let menuPosition = this.generalGraphUtils.getLinkMenuPosition(cy, toNode.renderedPoint()); + return new ConnectRelationModel(fromNode.data(), toNode.data(), possibleRelations); + } + return null; + }; + + private handlePathLink(cy: Cy.Instance, event: Cy.EventObject) { + let linkData = event.cyTarget.data(); + let selectedPathId = linkData.pathId; + let pathEdges = cy.collection(`[pathId='${selectedPathId}']`); + if (pathEdges.length > 1) { + setTimeout(() => { + pathEdges.select(); + }, 0); + } + } + + private handleVLLink(event: Cy.EventObject) { + let vl: Cy.CollectionNodes = event.cyTarget[0].target('.vl-node'); + let connectedEdges: Cy.CollectionEdges = vl.connectedEdges(`[type!="${CompositionCiServicePathLink.LINK_TYPE}"]`); + if (vl.length && connectedEdges.length > 1) { + setTimeout(() => { + vl.select(); + connectedEdges.select(); + }, 0); + } + } + + + /** + * Handles click event on links. + * If one edge selected: do nothing. + * Two or more edges: first click - select all, secondary click - select single. + * @param cy + * @param event + */ + public handleLinkClick(cy: Cy.Instance, event: Cy.EventObject) { + if (cy.$('edge:selected').length > 1 && event.cyTarget[0].selected()) { + cy.$(':selected').unselect(); + } else { + if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) { + this.handlePathLink(cy, event); + } + else { + this.handleVLLink(event); + } + } + } + + + /** + * Calculates the position for the menu that modifies an existing link + * @param event + * @param elementWidth + * @param elementHeight + * @returns {Point} + */ + public calculateLinkMenuPosition(event, elementWidth, elementHeight): Point { + let point: Point = new Point(event.originalEvent.clientX, event.originalEvent.clientY); + if (event.originalEvent.view.screen.height - elementHeight < point.y) { + point.y = event.originalEvent.view.screen.height - elementHeight; + } + if (event.originalEvent.view.screen.width - elementWidth < point.x) { + point.x = event.originalEvent.view.screen.width - elementWidth; + } + return point; + }; + + + /** + * Gets the menu that is displayed when you click an existing link. + * @param link + * @param event + * @returns {LinkMenu} + */ + public getModifyLinkMenu(link: Cy.CollectionFirstEdge, event: Cy.EventObject): LinkMenu { + let point: Point = this.calculateLinkMenuPosition(event, GraphUIObjects.MENU_LINK_VL_WIDTH_OFFSET, GraphUIObjects.MENU_LINK_VL_HEIGHT_OFFSET); + let menu: LinkMenu = new LinkMenu(point, true, link); + return menu; + }; + + /** + * Returns relation source and target nodes. + * @param nodes - all nodes in graph in order to find the edge connecting the two nodes + * @param fromNodeId + * @param toNodeId + * @returns [source, target] array of source node and target node. + */ + public getRelationNodes(nodes: Cy.CollectionNodes, fromNodeId: string, toNodeId: string) { + return [ + _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === fromNodeId), + _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === toNodeId) + ]; + } + + + /** + * go over the relations and draw links on the graph + * @param cy + * @param getRelationRequirementCapability - function to get requirement and capability of a relation + */ + public initGraphLinks(cy: Cy.Instance, relations: RelationshipModel[]) { + if (relations) { + _.forEach(relations, (relationshipModel: RelationshipModel) => { + _.forEach(relationshipModel.relationships, (relationship: Relationship) => { + let linkToCreate = this.linksFactory.createGraphLink(cy, relationshipModel, relationship); + this.insertLinkToGraph(cy, linkToCreate); + }); + }); + } + } + + /** + * Add link to graph - only draw the link + * @param cy + * @param link + * @param getRelationRequirementCapability + */ + public insertLinkToGraph = (cy: Cy.Instance, link: CompositionCiLinkBase) => { + const relationNodes = this.getRelationNodes(cy.nodes(), link.source, link.target); + const sourceNode: CompositionCiNodeBase = relationNodes[0] && relationNodes[0].data(); + const targetNode: CompositionCiNodeBase = relationNodes[1] && relationNodes[1].data(); + if ((sourceNode && !sourceNode.certified) || (targetNode && !targetNode.certified)) { + link.classes = 'not-certified-link'; + } + let linkElement = cy.add({ + group: 'edges', + data: link, + classes: link.classes + }); + + const getLinkRequirementCapability = () => + this.getRelationRequirementCapability(link.relation.relationships[0], sourceNode.componentInstance, targetNode.componentInstance); + this.commonGraphUtils.initLinkTooltip(linkElement, link.relation.relationships[0], getLinkRequirementCapability); + }; + + public syncComponentByRelation(relation: RelationshipModel) { + let componentInstances = this.compositionService.getComponentInstances(); + relation.relationships.forEach((rel) => { + if (rel.capability) { + const toComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.toNode); + const toComponentInstanceCapability: Capability = toComponentInstance.findCapability( + rel.capability.type, rel.capability.uniqueId, rel.capability.ownerId, rel.capability.name); + const isCapabilityFulfilled: boolean = rel.capability.isFulfilled(); + if (isCapabilityFulfilled && toComponentInstanceCapability) { + // if capability is fulfilled and in component, then remove it + console.log('Capability is fulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences); + toComponentInstance.capabilities[rel.capability.type].splice( + toComponentInstance.capabilities[rel.capability.type].findIndex((cap) => cap === toComponentInstanceCapability), 1 + ) + } else if (!isCapabilityFulfilled && !toComponentInstanceCapability) { + // if capability is unfulfilled and not in component, then add it + console.log('Capability is unfulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences); + toComponentInstance.capabilities[rel.capability.type].push(rel.capability); + } + } + if (rel.requirement) { + const fromComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.fromNode); + const fromComponentInstanceRequirement: Requirement = fromComponentInstance.findRequirement( + rel.requirement.capability, rel.requirement.uniqueId, rel.requirement.ownerId, rel.requirement.name); + const isRequirementFulfilled: boolean = rel.requirement.isFulfilled(); + if (isRequirementFulfilled && fromComponentInstanceRequirement) { + // if requirement is fulfilled and in component, then remove it + console.log('Requirement is fulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences); + fromComponentInstance.requirements[rel.requirement.capability].splice( + fromComponentInstance.requirements[rel.requirement.capability].findIndex((req) => req === fromComponentInstanceRequirement), 1 + ) + } else if (!isRequirementFulfilled && !fromComponentInstanceRequirement) { + // if requirement is unfulfilled and not in component, then add it + console.log('Requirement is unfulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences); + fromComponentInstance.requirements[rel.requirement.capability].push(rel.requirement); + } + } + }); + } + + public getRelationRequirementCapability(relationship: Relationship, sourceNode: ComponentInstance, targetNode: ComponentInstance): Promise<{ requirement: Requirement, capability: Capability }> { + // try find the requirement and capability in the source and target component instances: + let capability: Capability = targetNode.findCapability(undefined, + relationship.relation.capabilityUid, + relationship.relation.capabilityOwnerId, + relationship.relation.capability); + let requirement: Requirement = sourceNode.findRequirement(undefined, + relationship.relation.requirementUid, + relationship.relation.requirementOwnerId, + relationship.relation.requirement); + + return new Promise<{ requirement: Requirement, capability: Capability }>((resolve, reject) => { + if (capability && requirement) { + resolve({capability, requirement}); + } + else { + // if requirement and/or capability is missing, then fetch the full relation with its requirement and capability: + this.topologyTemplateService.fetchRelation(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, relationship.relation.id).subscribe((fetchedRelation) => { + this.syncComponentByRelation(fetchedRelation); + resolve({ + capability: capability || fetchedRelation.relationships[0].capability, + requirement: requirement || fetchedRelation.relationships[0].requirement + }); + }, reject); + } + }); + } +} + @@ -0,0 +1,158 @@ +import { TestBed } from '@angular/core/testing'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Rx'; +import CollectionNodes = Cy.CollectionNodes; +import { Mock } from 'ts-mockery'; +import { ComponentInstance } from '../../../../../models'; +import { ComponentMetadata } from '../../../../../models/component-metadata'; +import { Resource } from '../../../../../models/components/resource'; +import { CompositionCiNodeCp } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-cp'; +import { CompositionCiNodeVl } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-vl'; +import { EventListenerService } from '../../../../../services'; +import CollectionEdges = Cy.CollectionEdges; +import { GRAPH_EVENTS } from '../../../../../utils/constants'; +import { ServiceServiceNg2 } from '../../../../services/component-services/service.service'; +import { TopologyTemplateService } from '../../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../../services/responses/component-generic-response'; +import { QueueServiceUtils } from '../../../../utils/queue-service-utils'; +import { WorkspaceService } from '../../../workspace/workspace.service'; +import { CompositionService } from '../../composition.service'; +import { CommonGraphUtils } from '../common/common-graph-utils'; +import { CompositionGraphGeneralUtils } from './composition-graph-general-utils'; +import { CompositionGraphNodesUtils } from './composition-graph-nodes-utils'; + +describe('composition graph nodes utils', () => { + + const CP_TO_DELETE_ID = 'cp1'; + const VL_TO_DELETE_ID = 'vl'; + const CP2_ID = 'cp2'; + + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let service: CompositionGraphNodesUtils; + let topologyServiceMock: TopologyTemplateService; + let queueServiceMock: QueueServiceUtils; + let workspaceServiceMock: WorkspaceService; + let compositionServiceMock: CompositionService; + let eventListenerServiceMock: EventListenerService; + const cpInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({ + uniqueId: CP_TO_DELETE_ID, + isVl: () => false + }); + const vlInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({ + uniqueId: VL_TO_DELETE_ID, + isVl: () => true + }); + const cp2InstanceMock: ComponentInstance = Mock.of<ComponentInstance>({ + uniqueId: CP2_ID, + isVl: () => false + }); + + const cyMock = Mock.of<Cy.Instance>({ + remove: jest.fn(), + collection: jest.fn() + }); + + const serviceServiceMock = Mock.of<ServiceServiceNg2>({ + getComponentCompositionData : () => Observable.of(Mock.of<ComponentGenericResponse>()) + }); + + // Instances on the graph cp, vl, cp2 + const cp = Mock.from<CompositionCiNodeCp>({ id: CP_TO_DELETE_ID, componentInstance: cpInstanceMock }); + const vl = Mock.from<CompositionCiNodeVl>({ id: VL_TO_DELETE_ID, componentInstance: vlInstanceMock }); + const cp2 = Mock.from<CompositionCiNodeCp>({ id: CP2_ID, componentInstance: cp2InstanceMock }); + + beforeEach(() => { + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + topologyServiceMock = Mock.of<TopologyTemplateService>({ + deleteComponentInstance : () => Observable.of(cpInstanceMock) + }); + + queueServiceMock = Mock.of<QueueServiceUtils>({ + addBlockingUIAction : ( (f) => f() ) + }); + + workspaceServiceMock = Mock.of<WorkspaceService>({ + metadata: Mock.of<ComponentMetadata>( { uniqueId: 'topologyTemplateUniqueId' } ) + }); + + compositionServiceMock = Mock.of<CompositionService>({ + deleteComponentInstance : jest.fn() + }); + + eventListenerServiceMock = Mock.of<EventListenerService>({ + notifyObservers : jest.fn() + }); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + CompositionGraphNodesUtils, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyServiceMock}, + {provide: CompositionService, useValue: compositionServiceMock}, + {provide: CompositionGraphGeneralUtils, useValue: {}}, + {provide: CommonGraphUtils, useValue: {}}, + {provide: EventListenerService, useValue: eventListenerServiceMock}, + {provide: QueueServiceUtils, useValue: queueServiceMock}, + {provide: ServiceServiceNg2, useValue: serviceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock} + ] + }); + service = TestBed.get(CompositionGraphNodesUtils); + }); + + it('When a CP is deleted which is connected to a VL that has another leg to another CP, the VL is deleted as well', () => { + // Prepare a VL that is connected to both CP and CP2 + const vlToDelete = Mock.of<CollectionNodes>({ + data: () => vl, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 2, + connectedNodes: () => [cp, cp2] as CollectionNodes + }) + }); + + // Prepare a CP which is connected to a VL + const cpToDelete = Mock.of<CollectionNodes>({ + data: () => cp, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 1, + connectedNodes: () => [vlToDelete] as CollectionNodes + }) + }); + service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete); + expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, VL_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID); + expect(cyMock.remove).toHaveBeenCalled(); + }); + + it('When a CP is deleted which is solely connected to another VL the VL is not deleted', () => { + // Prepare a VL that is connected only to 1 CP + const vlToDelete = Mock.of<CollectionNodes>({ + data: () => vl, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 1, + connectedNodes: () => [cp] as CollectionNodes + }) + }); + + // Prepare a CP which is connected to a VL + const cpToDelete = Mock.of<CollectionNodes>({ + data: () => cp, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 1, + connectedNodes: () => [vlToDelete] as CollectionNodes + }) + }); + service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete); + expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledTimes(1); + expect(cyMock.remove).toHaveBeenCalled(); + }); +}); @@ -0,0 +1,202 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Injectable } from '@angular/core'; +import { Component as TopologyTemplate } from 'app/models'; +import { + ComponentInstance, + CompositionCiNodeVl, Service +} from 'app/models'; +import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response'; +import { QueueServiceUtils } from 'app/ng2/utils/queue-service-utils'; +import { EventListenerService } from 'app/services'; +import { GRAPH_EVENTS } from 'app/utils'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { CompositionService } from '../../composition.service'; +import { CommonGraphUtils } from '../common/common-graph-utils'; +import { CompositionGraphGeneralUtils } from './composition-graph-general-utils'; + +/** + * Created by obarda on 11/9/2016. + */ +@Injectable() +export class CompositionGraphNodesUtils { + constructor(private generalGraphUtils: CompositionGraphGeneralUtils, + private commonGraphUtils: CommonGraphUtils, + private eventListenerService: EventListenerService, + private queueServiceUtils: QueueServiceUtils, + private serviceService: ServiceServiceNg2, + private loaderService: SdcUiServices.LoaderService, + private compositionService: CompositionService, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService) { + } + + /** + * Returns component instances for all nodes passed in + * @param nodes - Cy nodes + * @returns {any[]} + */ + public getAllNodesData(nodes: Cy.CollectionNodes) { + return _.map(nodes, (node: Cy.CollectionFirstNode) => { + return node.data(); + }); + } + + public highlightMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string) => { + + cy.batch(() => { + cy.nodes("[name !@^= '" + nameToMatch + "']").style({'background-image-opacity': 0.4}); + cy.nodes("[name @^= '" + nameToMatch + "']").style({'background-image-opacity': 1}); + }); + + } + + // Returns all nodes whose name starts with searchTerm + public getMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string): Cy.CollectionNodes => { + return cy.nodes("[name @^= '" + nameToMatch + "']"); + } + + /** + * Deletes component instances on server and then removes it from the graph as well + * @param cy + * @param component + * @param nodeToDelete + */ + public deleteNode(cy: Cy.Instance, component: TopologyTemplate, nodeToDelete: Cy.CollectionNodes): void { + + this.loaderService.activate(); + const onSuccess: (response: ComponentInstance) => void = (response: ComponentInstance) => { + // check whether the node is connected to any VLs that only have one other connection. If so, delete that VL as well + this.loaderService.deactivate(); + this.compositionService.deleteComponentInstance(response.uniqueId); + + const nodeToDeleteIsNotVl = nodeToDelete.data().componentInstance && !(nodeToDelete.data().componentInstance.isVl()); + if (nodeToDeleteIsNotVl) { + const connectedVls: Cy.CollectionFirstNode[] = this.getConnectedVlToNode(nodeToDelete); + this.handleConnectedVlsToDelete(connectedVls); + } + + // check whether there is a service path going through this node, and if so clean it from the graph. + const nodeId = nodeToDelete.data().id; + const connectedPathLinks = cy.collection(`[type="${CompositionCiServicePathLink.LINK_TYPE}"][source="${nodeId}"], [type="${CompositionCiServicePathLink.LINK_TYPE}"][target="${nodeId}"]`); + _.forEach(connectedPathLinks, (link, key) => { + cy.remove(`[pathId="${link.data().pathId}"]`); + }); + + // update service path list + this.serviceService.getComponentCompositionData(component).subscribe((serviceResponse: ServiceGenericResponse) => { + (component as Service).forwardingPaths = serviceResponse.forwardingPaths; + }); + + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, nodeId); + + // update UI + cy.remove(nodeToDelete); + }; + + const onFailed: (response: any) => void = (response: any) => { + this.loaderService.deactivate(); + }; + + this.queueServiceUtils.addBlockingUIAction( + () => { + const uniqueId = this.workspaceService.metadata.uniqueId; + const componentType = this.workspaceService.metadata.componentType; + this.topologyTemplateService.deleteComponentInstance(componentType, uniqueId, nodeToDelete.data().componentInstance.uniqueId).subscribe(onSuccess, onFailed); + } + ); + } + + /** + * Finds all VLs connected to a single node + * @param node + * @returns {Array<Cy.CollectionFirstNode>} + */ + public getConnectedVlToNode = (node: Cy.CollectionNodes): Cy.CollectionFirstNode[] => { + const connectedVls: Cy.CollectionFirstNode[] = new Array<Cy.CollectionFirstNode>(); + _.forEach(node.connectedEdges().connectedNodes(), (connectedNode: Cy.CollectionFirstNode) => { + const connectedNodeIsVl = connectedNode.data().componentInstance.isVl(); + if (connectedNodeIsVl) { + connectedVls.push(connectedNode); + } + }); + return connectedVls; + } + + /** + * Delete all VLs that have only two connected nodes (this function is called when deleting a node) + * @param connectedVls + */ + public handleConnectedVlsToDelete = (connectedVls: Cy.CollectionFirstNode[]) => { + _.forEach(connectedVls, (vlToDelete: Cy.CollectionNodes) => { + + if (vlToDelete.connectedEdges().length === 2) { // if vl connected only to 2 nodes need to delete the vl + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, vlToDelete.data().componentInstance.uniqueId); + } + }); + } + + /** + * This function will update nodes position. + * @param cy + * @param component + * @param nodesMoved - the node/multiple nodes now moved by the user + */ + public onNodesPositionChanged = (cy: Cy.Instance, component: TopologyTemplate, nodesMoved: Cy.CollectionNodes): void => { + + if (nodesMoved.length === 0) { + return; + } + + const isValidMove: boolean = this.generalGraphUtils.isGroupValidDrop(cy, nodesMoved); + if (isValidMove) { + + const instancesToUpdate: ComponentInstance[] = new Array<ComponentInstance>(); + + _.each(nodesMoved, (node: Cy.CollectionFirstNode) => { // update all nodes new position + + // update position + const newPosition: Cy.Position = this.commonGraphUtils.getNodePosition(node); + node.data().componentInstance.updatePosition(newPosition.x, newPosition.y); + instancesToUpdate.push(node.data().componentInstance); + + }); + + if (instancesToUpdate.length > 0) { + this.generalGraphUtils.pushMultipleUpdateComponentInstancesRequestToQueue(instancesToUpdate); + } + } else { + // reset nodes position + nodesMoved.positions((i, node) => { + return { + x: +node.data().componentInstance.posX, + y: +node.data().componentInstance.posY + }; + }); + } + } + +} @@ -0,0 +1,233 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import {Injectable} from "@angular/core"; +import {CompositionGraphGeneralUtils, RequirementAndCapabilities} from "./composition-graph-general-utils"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {EventListenerService} from "../../../../../services/event-listener-service"; +import {ResourceNamePipe} from "app/ng2/pipes/resource-name.pipe"; +import {ComponentInstanceFactory} from "app/utils/component-instance-factory"; +import {GRAPH_EVENTS, GraphUIObjects} from "app/utils/constants"; +import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service"; +import {DndDropEvent} from "ngx-drag-drop/ngx-drag-drop"; +import {SdcUiServices} from "onap-ui-angular" +import { Component as TopologyTemplate, NodesFactory, CapabilitiesGroup, RequirementsGroup, + CompositionCiNodeBase, ComponentInstance, LeftPaletteComponent, Point } from "app/models"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import { QueueServiceUtils } from "app/ng2/utils/queue-service-utils"; +import {ComponentGenericResponse} from "../../../../services/responses/component-generic-response"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CompositionGraphNodesUtils} from "./index"; + +@Injectable() +export class CompositionGraphPaletteUtils { + + constructor(private generalGraphUtils:CompositionGraphGeneralUtils, + private nodesFactory:NodesFactory, + private commonGraphUtils:CommonGraphUtils, + private queueServiceUtils:QueueServiceUtils, + private eventListenerService:EventListenerService, + private topologyTemplateService: TopologyTemplateService, + private loaderService: SdcUiServices.LoaderService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private nodesGraphUtils: CompositionGraphNodesUtils) { + } + + /** + * + * @param Calculate matching nodes, highlight the matching nodes and fade the non matching nodes + * @param leftPaletteComponent + * @param _cy + * @returns void + * @private + */ + + public onComponentHoverIn = (leftPaletteComponent: LeftPaletteComponent, _cy: Cy.Instance) => { + const nodesData = this.nodesGraphUtils.getAllNodesData(_cy.nodes()); + const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(_cy); + + if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(leftPaletteComponent.uniqueId)) { + const reqAndCap: RequirementAndCapabilities = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(leftPaletteComponent.uniqueId); + const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance( + { uniqueId: leftPaletteComponent.uniqueId, requirements: reqAndCap.requirements, capabilities: reqAndCap.capabilities} as ComponentInstance, nodesData, nodesLinks); + + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, _cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, _cy); + } else { + + this.topologyTemplateService.getCapabilitiesAndRequirements(leftPaletteComponent.componentType, leftPaletteComponent.uniqueId).subscribe((response: ComponentGenericResponse) => { + let reqAndCap: RequirementAndCapabilities = { + capabilities: response.capabilities, + requirements: response.requirements + } + this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.setValue(leftPaletteComponent.uniqueId, reqAndCap); + }); + } + } + + /** + * Calculate the dragged element (html element) position on canvas + * @param cy + * @param event + * @param position + * @returns {Cy.BoundingBox} + * @private + */ + private _getNodeBBox(cy:Cy.Instance, event:DragEvent, position?:Cy.Position, eventPosition?: Point) { + let bbox = <Cy.BoundingBox>{}; + if (!position) { + position = event ? this.commonGraphUtils.getCytoscapeNodePosition(cy, event) : eventPosition; + } + let cushionWidth:number = 40; + let cushionHeight:number = 40; + + bbox.x1 = position.x - cushionWidth / 2; + bbox.y1 = position.y - cushionHeight / 2; + bbox.x2 = position.x + cushionWidth / 2; + bbox.y2 = position.y + cushionHeight / 2; + return bbox; + } + + /** + * Create the component instance, update data from parent component in the left palette and notify on_insert_to_ucpe if component was dragg into ucpe + * @param cy + * @param fullComponent + * @param event + * @param component + */ + private _createComponentInstanceOnGraphFromPaletteComponent(cy:Cy.Instance, fullComponent:LeftPaletteComponent, event:DragEvent) { + + let componentInstanceToCreate:ComponentInstance = ComponentInstanceFactory.createComponentInstanceFromComponent(fullComponent); + let cytoscapePosition:Cy.Position = this.commonGraphUtils.getCytoscapeNodePosition(cy, event); + componentInstanceToCreate.posX = cytoscapePosition.x; + componentInstanceToCreate.posY = cytoscapePosition.y; + + let onFailedCreatingInstance:(error:any) => void = (error:any) => { + this.loaderService.deactivate(); + }; + + //on success - update node data + let onSuccessCreatingInstance = (createInstance:ComponentInstance):void => { + + this.loaderService.deactivate(); + this.compositionService.addComponentInstance(createInstance); + createInstance.name = ResourceNamePipe.getDisplayName(createInstance.name); + createInstance.requirements = new RequirementsGroup(createInstance.requirements); + createInstance.capabilities = new CapabilitiesGroup(createInstance.capabilities); + createInstance.componentVersion = fullComponent.version; + createInstance.icon = fullComponent.icon; + createInstance.setInstanceRC(); + + let newNode:CompositionCiNodeBase = this.nodesFactory.createNode(createInstance); + this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE); + }; + + this.queueServiceUtils.addBlockingUIAction(() => { + let uniqueId = this.workspaceService.metadata.uniqueId; + let componentType = this.workspaceService.metadata.componentType; + this.topologyTemplateService.createComponentInstance(componentType, uniqueId, componentInstanceToCreate).subscribe(onSuccessCreatingInstance, onFailedCreatingInstance); + + }); + } + // + // /** + // * Thid function applay red/green background when component dragged from palette + // * @param cy + // * @param event + // * @param dragElement + // * @param dragComponent + // */ + // public onComponentDrag(cy:Cy.Instance, event) { + // let draggedElement = document.getElementById("draggable_element"); + // // event.dataTransfer.setDragImage(draggableElement, 0, 0); + // if (event.clientX < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop + // draggedElement.className = 'invalid-drag'; + // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0); + // return; + // } + // + // let offsetPosition = { + // x: event.clientX - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, + // y: event.clientY - GraphUIObjects.DIAGRAM_HEADER_OFFSET + // }; + // let bbox = this._getNodeBBox(cy, event, offsetPosition); + // + // if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) { + // draggedElement.className = 'valid-drag'; + // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0); + // // event.dataTransfer.setDragImage(draggedElement, 0, 0); + // // event.dataTransfer.setDragImage(draggedElement, 0, 0); + // + // } else { + // draggedElement.className = 'invalid-drag'; + // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0); + // } + // } + + public isDragValid(cy:Cy.Instance, position: Point):boolean { + if (position.x < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || position.y < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop + return false; + } + + let offsetPosition = { + x: position.x - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, + y: position.y - GraphUIObjects.DIAGRAM_HEADER_OFFSET + }; + let bbox = this._getNodeBBox(cy, null, offsetPosition, position); + + if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) { + return true; + } else { + return false; + } + } + /** + * This function is called when after dropping node on canvas + * Check if the capability & requirements fulfilled and if not get from server + * @param cy + * @param dragEvent + * @param component + */ + public addNodeFromPalette(cy:Cy.Instance, dragEvent:DndDropEvent) { + this.loaderService.activate(); + + let draggedComponent:LeftPaletteComponent = dragEvent.data; + + if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(draggedComponent.uniqueId)) { + let fullComponent = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(draggedComponent.uniqueId); + draggedComponent.capabilities = fullComponent.capabilities; + draggedComponent.requirements = fullComponent.requirements; + this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event); + + } else { + + this.topologyTemplateService.getFullComponent(draggedComponent.componentType, draggedComponent.uniqueId).subscribe((topologyTemplate:TopologyTemplate) => { + draggedComponent.capabilities = topologyTemplate.capabilities; + draggedComponent.requirements = topologyTemplate.requirements; + this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event); + }); + } + } +} + @@ -0,0 +1,148 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {ServiceServiceNg2} from 'app/ng2/services/component-services/service.service'; +import {Service} from "app/models/components/service"; +import {ForwardingPath} from "app/models/forwarding-path"; +import {ForwardingPathLink} from "app/models/forwarding-path-link"; +import {ComponentRef, Injectable} from "@angular/core"; +import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link"; +import {SdcUiServices} from "onap-ui-angular"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {ServicePathsListComponent} from "app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component"; +import {ButtonModel, ModalModel} from "app/models"; +import {ServicePathCreatorComponent} from "app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component"; +import {ModalService} from "app/ng2/services/modal.service"; +import {ModalComponent} from "app/ng2/components/ui/modal/modal.component"; +import {Select, Store} from "@ngxs/store"; +import {WorkspaceState} from "app/ng2/store/states/workspace.state"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import {CompositionService} from "../../composition.service"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {GRAPH_EVENTS} from "app/utils/constants"; +import {EventListenerService} from "app/services/event-listener-service"; + +@Injectable() +export class ServicePathGraphUtils { + + constructor( + private generalGraphUtils: CompositionGraphGeneralUtils, + private serviceService: ServiceServiceNg2, + private commonGraphUtils: CommonGraphUtils, + private loaderService: SdcUiServices.LoaderService, + private queueServiceUtils: QueueServiceUtils, + private modalService: ModalService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private store:Store, + private eventListenerService: EventListenerService + ) { + } + + private isViewOnly = (): boolean => { + return this.store.selectSnapshot(state => state.workspace.isViewOnly); + } + private modalInstance: ComponentRef<ModalComponent>; + + public deletePathsFromGraph(cy: Cy.Instance) { + cy.remove(`[type="${CompositionCiServicePathLink.LINK_TYPE}"]`); + } + + public drawPath(cy: Cy.Instance, forwardingPath: ForwardingPath) { + let pathElements = forwardingPath.pathElements.listToscaDataDefinition; + + _.forEach(pathElements, (link: ForwardingPathLink) => { + let data: CompositionCiServicePathLink = new CompositionCiServicePathLink(link); + data.source = _.find( + this.compositionService.componentInstances, + instance => instance.name === data.forwardingPathLink.fromNode + ).uniqueId; + data.target = _.find( + this.compositionService.componentInstances, + instance => instance.name === data.forwardingPathLink.toNode + ).uniqueId; + data.pathId = forwardingPath.uniqueId; + data.pathName = forwardingPath.name; + this.commonGraphUtils.insertServicePathLinkToGraph(cy, data); + }); + } + + public createOrUpdateServicePath = (path: any): void => { + this.loaderService.activate(); + + let onSuccess: (response: ForwardingPath) => void = (response: ForwardingPath) => { + this.loaderService.deactivate(); + this.compositionService.forwardingPaths[response.uniqueId] = response; + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, response.uniqueId) + }; + + this.queueServiceUtils.addBlockingUIAction( + () => this.serviceService.createOrUpdateServicePath(this.workspaceService.metadata.uniqueId, path).subscribe(onSuccess + , (error) => {this.loaderService.deactivate()}) + ); + }; + + public onCreateServicePath = (): void => { + // this.showServicePathMenu = false; + let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal); + let saveButton: ButtonModel = new ButtonModel('Create', 'blue', this.createPath, this.getDisabled); + let modalModel: ModalModel = new ModalModel('l', 'Create Service Flow', '', [saveButton, cancelButton], 'standard', true); + this.modalInstance = this.modalService.createCustomModal(modalModel); + this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {serviceId: this.workspaceService.metadata.uniqueId}); + this.modalInstance.instance.open(); + }; + + public onListServicePath = (): void => { + // this.showServicePathMenu = false; + let cancelButton: ButtonModel = new ButtonModel('Close', 'outline white', this.modalService.closeCurrentModal); + let modalModel: ModalModel = new ModalModel('md', 'Service Flows List', '', [cancelButton], 'standard', true); + this.modalInstance = this.modalService.createCustomModal(modalModel); + this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathsListComponent, { + serviceId: this.workspaceService.metadata.uniqueId, + onCreateServicePath: this.onCreateServicePath, + onEditServicePath: this.onEditServicePath, + isViewOnly: this.isViewOnly() + }); + this.modalInstance.instance.open(); + }; + + public onEditServicePath = (id: string): void => { + let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal); + let saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createPath, this.getDisabled); + let modalModel: ModalModel = new ModalModel('l', 'Edit Path', '', [saveButton, cancelButton], 'standard', true); + this.modalInstance = this.modalService.createCustomModal(modalModel); + this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, { + serviceId: this.workspaceService.metadata.uniqueId, + pathId: id + }); + this.modalInstance.instance.open(); + }; + + public getDisabled = (): boolean => { + return this.isViewOnly() || !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit(); + }; + + public createPath = (): void => { + this.createOrUpdateServicePath(this.modalInstance.instance.dynamicContent.instance.createServicePathData()); + this.modalService.closeCurrentModal(); + }; +} @@ -0,0 +1,204 @@ +import { + Point, + PolicyInstance, + Zone, + LeftPaletteMetadataTypes, + ZoneInstance, + ZoneInstanceType, + ZoneInstanceAssignmentType +} from "app/models"; +import {CanvasHandleTypes} from "app/utils"; +import {Observable} from "rxjs"; +import {GroupInstance} from "app/models/graph/zones/group-instance"; +import {Injectable} from "@angular/core"; +import {DynamicComponentService} from "app/ng2/services/dynamic-component.service"; +import {PoliciesService} from "app/ng2/services/policies.service"; +import {GroupsService} from "app/ng2/services/groups.service"; +import {Store} from "@ngxs/store"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import { PaletteAnimationComponent } from "app/ng2/pages/composition/palette/palette-animation/palette-animation.component"; + +@Injectable() +export class CompositionGraphZoneUtils { + + constructor(private dynamicComponentService: DynamicComponentService, + private policiesService: PoliciesService, + private groupsService: GroupsService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService) { + } + + + public createCompositionZones = (): Array<Zone> => { + let zones: Array<Zone> = []; + + zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY); + zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP); + + return zones; + } + + public showZone = (zone: Zone): void => { + zone.visible = true; + zone.minimized = false; + } + + public getZoneTypeForPaletteComponent = (componentCategory: LeftPaletteMetadataTypes) => { + if (componentCategory == LeftPaletteMetadataTypes.Group) { + return ZoneInstanceType.GROUP; + } else if (componentCategory == LeftPaletteMetadataTypes.Policy) { + return ZoneInstanceType.POLICY; + } + }; + + public initZoneInstances(zones: Array<Zone>) { + + if (this.compositionService.groupInstances && this.compositionService.groupInstances.length) { + this.showZone(zones[ZoneInstanceType.GROUP]); + zones[ZoneInstanceType.GROUP].instances = []; + _.forEach(this.compositionService.groupInstances, (group: GroupInstance) => { + let newInstance = new ZoneInstance(group, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + this.addInstanceToZone(zones[ZoneInstanceType.GROUP], newInstance); + }); + } + + if (this.compositionService.policies && this.compositionService.policies.length) { + this.showZone(zones[ZoneInstanceType.POLICY]); + zones[ZoneInstanceType.POLICY].instances = []; + _.forEach(this.compositionService.policies, (policy: PolicyInstance) => { + let newInstance = new ZoneInstance(policy, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + this.addInstanceToZone(zones[ZoneInstanceType.POLICY], newInstance); + + }); + } + } + + public findAndUpdateZoneInstanceData(zones: Array<Zone>, instanceData: PolicyInstance | GroupInstance) { + _.forEach(zones, (zone: Zone) => { + _.forEach(zone.instances, (zoneInstance: ZoneInstance) => { + if (zoneInstance.instanceData.uniqueId === instanceData.uniqueId) { + zoneInstance.updateInstanceData(instanceData); + } + }); + }); + } + + public updateTargetsOrMembersOnCanvasDelete = (canvasNodeID: string, zones: Array<Zone>, type: ZoneInstanceAssignmentType): void => { + _.forEach(zones, (zone) => { + _.forEach(zone.instances, (zoneInstance: ZoneInstance) => { + if (zoneInstance.isAlreadyAssigned(canvasNodeID)) { + zoneInstance.addOrRemoveAssignment(canvasNodeID, type); + //remove it from our list of BE targets and members as well (so that it will not be sent in future calls to BE). + zoneInstance.instanceData.setSavedAssignments(zoneInstance.assignments); + } + }); + }); + }; + + public createZoneInstanceFromLeftPalette = (zoneType: ZoneInstanceType, paletteComponentType: string): Observable<ZoneInstance> => { + + if (zoneType === ZoneInstanceType.POLICY) { + return this.policiesService.createPolicyInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => { + let newInstance = new PolicyInstance(response); + this.compositionService.addPolicyInstance(newInstance); + return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + }); + } else if (zoneType === ZoneInstanceType.GROUP) { + return this.groupsService.createGroupInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => { + let newInstance = new GroupInstance(response); + this.compositionService.addGroupInstance(newInstance); + return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + }); + } + } + + public addInstanceToZone(zone: Zone, instance: ZoneInstance, hide?: boolean) { + if (hide) { + instance.hidden = true; + } + zone.instances.push(instance); + + }; + + private findZoneCoordinates(zoneType): Point { + let point: Point = new Point(0, 0); + let zone = angular.element(document.querySelector('.' + zoneType + '-zone')); + let wrapperZone = zone.offsetParent(); + point.x = zone.prop('offsetLeft') + wrapperZone.prop('offsetLeft'); + point.y = zone.prop('offsetTop') + wrapperZone.prop('offsetTop'); + return point; + } + + public createPaletteToZoneAnimation = (startPoint: Point, zoneType: ZoneInstanceType, newInstance: ZoneInstance) => { + let zoneTypeName = ZoneInstanceType[zoneType].toLowerCase(); + let paletteToZoneAnimation = this.dynamicComponentService.createDynamicComponent(PaletteAnimationComponent); + paletteToZoneAnimation.instance.from = startPoint; + paletteToZoneAnimation.instance.type = zoneType; + paletteToZoneAnimation.instance.to = this.findZoneCoordinates(zoneTypeName); + paletteToZoneAnimation.instance.zoneInstance = newInstance; + paletteToZoneAnimation.instance.iconName = zoneTypeName; + paletteToZoneAnimation.instance.runAnimation(); + } + + public startCyTagMode = (cy: Cy.Instance) => { + cy.autolock(true); + cy.nodes().unselectify(); + cy.emit('tagstart'); //dont need to show handles because they're already visible bcz of hover event + + }; + + public endCyTagMode = (cy: Cy.Instance) => { + cy.emit('tagend'); + cy.nodes().selectify(); + cy.autolock(false); + }; + + public handleTagClick = (cy: Cy.Instance, zoneInstance: ZoneInstance, nodeId: string) => { + zoneInstance.addOrRemoveAssignment(nodeId, ZoneInstanceAssignmentType.COMPONENT_INSTANCES); + this.showZoneTagIndicationForNode(nodeId, zoneInstance, cy); + }; + + public showGroupZoneIndications = (groupInstances: Array<ZoneInstance>, policyInstance: ZoneInstance) => { + groupInstances.forEach((groupInstance: ZoneInstance) => { + let handle: string = this.getCorrectHandleForNode(groupInstance.instanceData.uniqueId, policyInstance); + groupInstance.showHandle(handle); + }) + }; + + public hideGroupZoneIndications = (instances: Array<ZoneInstance>) => { + instances.forEach((instance) => { + instance.hideHandle(); + }) + } + + public showZoneTagIndications = (cy: Cy.Instance, zoneInstance: ZoneInstance) => { + + cy.nodes().forEach(node => { + let handleType: string = this.getCorrectHandleForNode(node.id(), zoneInstance); + cy.emit('showhandle', [node, handleType]); + }); + }; + + public showZoneTagIndicationForNode = (nodeId: string, zoneInstance: ZoneInstance, cy: Cy.Instance) => { + let node = cy.getElementById(nodeId); + let handleType: string = this.getCorrectHandleForNode(nodeId, zoneInstance); + cy.emit('showhandle', [node, handleType]); + } + + public hideZoneTagIndications = (cy: Cy.Instance) => { + cy.emit('hidehandles'); + }; + + public getCorrectHandleForNode = (nodeId: string, zoneInstance: ZoneInstance): string => { + if (zoneInstance.isAlreadyAssigned(nodeId)) { + if (zoneInstance.type == ZoneInstanceType.POLICY) { + return CanvasHandleTypes.TAGGED_POLICY; + } else { + return CanvasHandleTypes.TAGGED_GROUP; + } + } else { + return CanvasHandleTypes.TAG_AVAILABLE; + } + }; +} @@ -0,0 +1,29 @@ +/** + * Created by ob0695 on 6/3/2018. + */ +// export * from './composition-graph-general-utils'; +// export * from './composition-graph-links-utils'; +// export * from './composition-graph-nodes-utils'; +// export * from './composition-graph-palette-utils'; +// export * from './composition-graph-service-path-utils'; +// export * from './composition-graph-zone-utils'; + + +import {CompositionGraphGeneralUtils} from './composition-graph-general-utils'; +import {CompositionGraphNodesUtils} from './composition-graph-nodes-utils'; +import {MatchCapabilitiesRequirementsUtils} from './match-capability-requierment-utils' +import {CompositionGraphPaletteUtils} from './composition-graph-palette-utils'; +import {CompositionGraphZoneUtils} from './composition-graph-zone-utils'; +import {ServicePathGraphUtils} from './composition-graph-service-path-utils'; +import {CompositionGraphLinkUtils} from "./composition-graph-links-utils"; + + +export { + CompositionGraphGeneralUtils, + CompositionGraphLinkUtils, + CompositionGraphNodesUtils, + MatchCapabilitiesRequirementsUtils, + CompositionGraphPaletteUtils, + CompositionGraphZoneUtils, + ServicePathGraphUtils +};
\ No newline at end of file @@ -0,0 +1,342 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Mock } from 'ts-mockery'; +import { + CapabilitiesGroup, + Capability, ComponentInstance, CompositionCiLinkBase, CompositionCiNodeBase, CompositionCiNodeCp, + CompositionCiNodeVf, CompositionCiNodeVl, + Requirement, RequirementsGroup +} from '../../../../../models'; +import { MatchCapabilitiesRequirementsUtils } from './match-capability-requierment-utils'; + +describe('match capability requirements utils service ', () => { + + const bindableReq = Mock.of<Requirement>({ + capability : 'tosca.capabilities.network.Bindable', + name: 'virtualBinding', + relationship: 'tosca.relationships.network.BindsTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualBinding', + ownerId : 'extcp0', + ownerName : 's' + }); + + const virtualLinkReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.network.Linkable', + name: 'virtualLink', + relationship: 'tosca.relationships.network.LinksTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualLink', + ownerId : '', + ownerName : 's' + }); + + const storeAttachmentReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.Attachment', + name: 'local_storage', + relationship: 'tosca.relationships.AttachesTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage', + node: 'tosca.nodes.BlockStorage', + ownerId : '', + ownerName : 's' + }); + + const vlAttachmentReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.Attachment', + name: 'local_storage', + relationship: 'tosca.relationships.AttachesTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage', + node: 'tosca.nodes.BlockStorage', + ownerId : '', + ownerName : 's' + }); + + const extVirtualLinkReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.network.Linkable', + name: 'external_virtualLink', + relationship: 'tosca.relationships.network.LinksTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.external_virtualLink' + }); + + const dependencyReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.Node', + name: 'dependency', + relationship: 'tosca.relationships.DependsOn', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.dependency' + }); + + const featureCap = Mock.of<Capability>({ + type: 'tosca.capabilities.Node', + name: 'feature', + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.feature', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const internalConnPointCap = Mock.of<Capability>({ + type: 'tosca.capabilities.Node', + name: 'internal_connectionPoint', + capabilitySources : ['org.openecomp.resource.cp.extCP'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.internal_connectionPoint', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const blockStoreAttachmentCap = Mock.of<Capability>({ + type: 'tosca.capabilities.Attachment', + name: 'attachment', + capabilitySources: ['tosca.nodes.BlockStorage'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.attachment', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const bindingCap = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Bindable', + name: 'binding', + capabilitySources: ['tosca.nodes.Compute'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.binding', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1', + }); + + const linkableCap = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Linkable', + capabilitySources: ['org.openecomp.resource.vl.extVL'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.virtual_linkable', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const nodeCompute = Mock.of<CompositionCiNodeVf>({ + name: 'Compute 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'Compute', + uniqueId : 'compute0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.Node' : [ dependencyReq ], + 'tosca.capabilities.Attachment' : [ storeAttachmentReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.network.Bindable' : [ bindingCap ], + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + const nodeBlockStorage = Mock.of<CompositionCiNodeVf>({ + name: 'BlockStorage 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'BlockStorage', + uniqueId : 'blockstorage0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.Node' : [ dependencyReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.Attachment' : [ blockStoreAttachmentCap ], + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + const nodeVl = Mock.of<CompositionCiNodeVl>({ + name: 'ExtVL 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'BlockStorage', + uniqueId : 'extvl0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.Node' : [ dependencyReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.network.Linkable' : [ linkableCap ], + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + const nodeCp = Mock.of<CompositionCiNodeCp>({ + name: 'ExtCP 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'ExtCP', + uniqueId : 'extcp0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.network.Linkable' : [ virtualLinkReq ], + 'tosca.capabilities.network.Bindable' : [ bindableReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + let service: MatchCapabilitiesRequirementsUtils; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [MatchCapabilitiesRequirementsUtils] + }); + + service = TestBed.get(MatchCapabilitiesRequirementsUtils); + }); + + it('match capability requirements utils should be defined', () => { + console.log(JSON.stringify(service)); + expect(service).toBeDefined(); + }); + + describe('isMatch function ', () => { + + it('capability type not equal to requirement capability, match is false', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable11'}); + const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'}); + expect(service.isMatch(requirement, capability)).toBeFalsy(); + }); + + it('capability type equal to requirement capability and requirement node not exist, match is true', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable'}); + const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'}); + expect(service.isMatch(requirement, capability)).toBeTruthy(); + }); + + it('is match - capability type equal to requirement capability and requirement node exist and includes in capability sources, match is true', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node1'}); + const capability = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Linkable', + capabilitySources: ['node1', 'node2', 'node3'] + }); + expect(service.isMatch(requirement, capability)).toBeTruthy(); + }); + + it('no match - capability type equal to requirement capability and requirement node but not includes in capability sources, match is false', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node4'}); + const capability = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Linkable', + capabilitySources: ['node1', 'node2', 'node3'] + }); + expect(service.isMatch(requirement, capability)).toBeFalsy(); + }); + }); + + describe('hasUnfulfilledRequirementContainingMatch function ', () => { + + it('node have no componentInstance, return false', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: undefined}); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy(); + }); + + it('node have componentInstance data but no unfulfilled requirements, return false', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()}); + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy(); + }); + + it('node have componentInstance data and unfulfilled requirements but no match found, return false', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()}); + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + jest.spyOn(service, 'containsMatch').mockReturnValue(false); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy(); + }); + + it('node have componentInstance data with unfulfilled requirements and match found, return true', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()}); + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + jest.spyOn(service, 'containsMatch').mockReturnValue(true); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeTruthy(); + }); + }); + + describe('getMatches function ', () => { + let fromId: string; + let toId: string; + + beforeEach(() => { + fromId = 'from_id'; + toId = 'to_id'; + }); + + it('node have no unfulfilled requirements, return empty match array', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]); + expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0); + }); + + it('node have unfulfilled requirements but no capabilities, return empty match array', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0); + }); + + it('node have unfulfilled requirements and capabilities but no match found, return empty match array', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + jest.spyOn(service, 'isMatch').mockReturnValue(false); + expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0); + }); + + it('node have 2 unfulfilled requirements and 2 capabilities and match found, return 4 matches', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + const capabilities = {aaa: Mock.of<Capability>(), bbb: Mock.of<Capability>()}; + jest.spyOn(service, 'isMatch').mockReturnValue(true); + expect(service.getMatches({}, capabilities, [], fromId, toId, true)).toHaveLength(4); + }); + }); + + describe('Find matching nodes ===>', () => { + + it('should find matching nodes with component instance', () => { + const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ]; + let matchingNodes: any; + + // Compute can connect to Block Store + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(1); + expect(matchingNodes).toContain(nodeBlockStorage); + + // Block Storage can connect to Compute + matchingNodes = service.findMatchingNodesToComponentInstance(nodeBlockStorage.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(1); + expect(matchingNodes).toContain(nodeCompute); + + // Vl has no matches + matchingNodes = service.findMatchingNodesToComponentInstance(nodeVl.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(0); + + // CP should be able to connect to VL and Compute + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(2); + expect(matchingNodes).toContain(nodeCompute); + expect(matchingNodes).toContain(nodeVl); + }); + + it('try with empty list of nodes', () => { + const nodes = [ ]; + let matchingNodes: any; + + // Compute can connect to Block Store + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(0); + }); + + it('should detect fulfilled connection with compute node', () => { + const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ]; + let matchingNodes: any; + const link = { + relation: { + fromNode: 'extcp0', + toNode: 'compute0', + relationships: [{ + relation: { + requirementOwnerId: 'extcp0', + requirement: 'virtualBinding', + relationship: { + type: 'tosca.relationships.network.BindsTo' + } + + } + }] + } + }; + + const links = [link]; + // CP should be able to connect to VL only since it already has a link with compute + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, links as CompositionCiLinkBase[]); + expect(matchingNodes).toHaveLength(1); + expect(matchingNodes).toContain(nodeVl); + }); + }); +}); @@ -0,0 +1,196 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +import { Injectable } from '@angular/core'; +import { + CapabilitiesGroup, Capability, ComponentInstance, CompositionCiLinkBase, + CompositionCiNodeBase, Match, Requirement, RequirementsGroup +} from 'app/models'; +import * as _ from 'lodash'; + +/** + * Created by obarda on 1/1/2017. + */ +@Injectable() +export class MatchCapabilitiesRequirementsUtils { + + /** + * Shows + icon in corner of each node passed in + * @param filteredNodesData + * @param cy + */ + public highlightMatchingComponents(filteredNodesData, cy: Cy.Instance) { + _.each(filteredNodesData, (data: any) => { + const node = cy.getElementById(data.id); + cy.emit('showhandle', [node]); + }); + } + + /** + * Adds opacity to each node that cannot be linked to hovered node + * @param filteredNodesData + * @param nodesData + * @param cy + * @param hoveredNodeData + */ + public fadeNonMachingComponents(filteredNodesData, nodesData, cy: Cy.Instance, hoveredNodeData?) { + const fadeNodes = _.xorWith(nodesData, filteredNodesData, (node1, node2) => { + return node1.id === node2.id; + }); + if (hoveredNodeData) { + _.remove(fadeNodes, hoveredNodeData); + } + cy.batch(() => { + _.each(fadeNodes, (node) => { + cy.getElementById(node.id).style({'background-image-opacity': 0.4}); + }); + }); + } + + /** + * Resets all nodes to regular opacity + * @param cy + */ + public resetFadedNodes(cy: Cy.Instance) { + cy.batch(() => { + cy.nodes().style({'background-image-opacity': 1}); + }); + } + + public getMatchedRequirementsCapabilities(fromComponentInstance: ComponentInstance, + toComponentInstance: ComponentInstance, + links: CompositionCiLinkBase[]): Match[] { + const fromToMatches: Match[] = this.getMatches(fromComponentInstance.requirements, + toComponentInstance.capabilities, + links, + fromComponentInstance.uniqueId, + toComponentInstance.uniqueId, true); + const toFromMatches: Match[] = this.getMatches(toComponentInstance.requirements, + fromComponentInstance.capabilities, + links, + toComponentInstance.uniqueId, + fromComponentInstance.uniqueId, false); + + return fromToMatches.concat(toFromMatches); + } + + /***** REFACTORED FUNCTIONS START HERE *****/ + + public getMatches(requirements: RequirementsGroup, capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[], + fromId: string, toId: string, isFromTo: boolean): Match[] { + const matches: Match[] = []; + const unfulfilledReqs = this.getUnfulfilledRequirements(fromId, requirements, links); + _.forEach(unfulfilledReqs, (req) => { + _.forEach(_.flatten(_.values(capabilities)), (capability: Capability) => { + if (this.isMatch(req, capability)) { + if (isFromTo) { + matches.push(new Match(req, capability, isFromTo, fromId, toId)); + } else { + matches.push(new Match(req, capability, isFromTo, toId, fromId)); + } + } + }); + }); + return matches; + } + + public getUnfulfilledRequirements = (fromNodeId: string, requirements: RequirementsGroup, links: CompositionCiLinkBase[]): Requirement[] => { + const requirementArray: Requirement[] = []; + _.forEach(_.flatten(_.values(requirements)), (requirement: Requirement) => { + const reqFulfilled = this.isRequirementFulfilled(fromNodeId, requirement, links); + if (requirement.name !== 'dependency' && requirement.parentName !== 'dependency' && !reqFulfilled) { + requirementArray.push(requirement); + } + }); + return requirementArray; + } + + /** + * Returns true if there is a match between the capabilities and requirements that are passed in + * @param requirements + * @param capabilities + * @returns {boolean} + */ + public containsMatch = (requirements: Requirement[], capabilities: CapabilitiesGroup): boolean => { + return _.some(requirements, (req: Requirement) => { + return _.some(_.flatten(_.values(capabilities)), (capability: Capability) => { + return this.isMatch(req, capability); + }); + }); + } + + public hasUnfulfilledRequirementContainingMatch = (node: CompositionCiNodeBase, componentRequirements: Requirement[], capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[]) => { + if (node && node.componentInstance) { + // Check if node has unfulfilled requirement that can be filled by component (#2) + const nodeRequirements: Requirement[] = this.getUnfulfilledRequirements(node.componentInstance.uniqueId, node.componentInstance.requirements, links); + if (!nodeRequirements.length) { + return false; + } + if (this.containsMatch(nodeRequirements, capabilities)) { + return true; + } + } + } + + /** + * Returns array of nodes that can connect to the component. + * In order to connect, one of the following conditions must be met: + * 1. component has an unfulfilled requirement that matches a node's capabilities + * 2. node has an unfulfilled requirement that matches the component's capabilities + * 3. vl is passed in which has the capability to fulfill requirement from component and requirement on node. + */ + public findMatchingNodesToComponentInstance(componentInstance: ComponentInstance, nodeDataArray: CompositionCiNodeBase[], links: CompositionCiLinkBase[]): any[] { + return _.filter(nodeDataArray, (node: CompositionCiNodeBase) => { + const matchedRequirementsCapabilities = this.getMatchedRequirementsCapabilities(node.componentInstance, componentInstance, links); + return matchedRequirementsCapabilities && matchedRequirementsCapabilities.length > 0; + }); + } + + public isMatch(requirement: Requirement, capability: Capability): boolean { + if (capability.type === requirement.capability) { + if (requirement.node) { + if (_.includes(capability.capabilitySources, requirement.node)) { + return true; + } + } else { + return true; + } + } + return false; + } + + private isRequirementFulfilled(fromNodeId: string, requirement: any, links: CompositionCiLinkBase[]): boolean { + return _.some(links, { + relation: { + fromNode: fromNodeId, + relationships: [{ + relation: { + requirementOwnerId: requirement.ownerId, + requirement: requirement.name, + relationship: { + type: requirement.relationship + } + + } + }] + } + }); + } + +} @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`palette component should match current snapshot of palette component 1`] = ` +<composition-palette + buildPaletteByCategories={[Function Function]} + compositionPaletteService={[Function Object]} + eventListenerService={[Function Object]} + numberOfElements="0" + onDragStart={[Function Function]} + onDraggableMoved={[Function Function]} + onDrop={[Function Function]} + onMouseOut={[Function Function]} + onMouseOver={[Function Function]} + onSearchChanged={[Function Function]} + position={[Function Point]} +> + <div + class="composition-palette-component" + > + <div + class="palette-elements-count" + > + Elements + <span + class="palette-elements-count-value" + > + + </span> + </div> + <sdc-filter-bar + placeholder="Search..." + testid="searchAsset" + /> + <div + class="palette-elements-list" + > + <sdc-loader + name="palette-loader" + testid="palette-loader" + /> + + + </div> + </div><div + dnddropzone="" + id="draggable_element" + > + + </div> +</composition-palette> +`; @@ -24,7 +24,7 @@ import { setTimeout } from 'core-js/library/web/timers'; import { EventListenerService } from 'app/services'; import { GRAPH_EVENTS } from 'app/utils'; import { Point } from 'app/models'; -import { ZoneInstanceType, ZoneInstance } from '../../../../models/graph/zones/zone-instance'; +import { ZoneInstanceType, ZoneInstance } from 'app/models/graph/zones/zone-instance'; @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`palette element component should match current snapshot of palette element component 1`] = ` +<palette-element> + <div + class="palette-element" + > + <sdc-element-icon + class="palette-element-icon" + /> + <div + class="palette-element-text" + > + <div + class="palette-element-name" + sdc-tooltip="" + > + + </div> + <span> + V. + </span> + <span> + + </span> + </div> + </div> +</palette-element> +`; @@ -0,0 +1,11 @@ +<div class="palette-element" > + <sdc-element-icon class="palette-element-icon" [iconName]="paletteElement.icon" + [elementType]="paletteElement.componentSubType"[uncertified]="this.paletteElement.certifiedIconClass"></sdc-element-icon> + <div class="palette-element-text"> + <div class="palette-element-name" sdc-tooltip + tooltip-text='{{paletteElement.name | resourceName}}'>{{paletteElement.name | resourceName}} + </div> + <span> V.{{paletteElement.version}}</span> + <span>{{paletteElement.componentSubType}}</span> + </div> +</div>
\ No newline at end of file @@ -0,0 +1,32 @@ +@import "./../../../../../../assets/styles/override"; +.palette-element { + cursor: pointer; + display: flex; + flex-direction: row; + max-height: 65px; + border-bottom: 1px solid @sdcui_color_silver; + padding: 10px; + align-items: center; + .palette-element-icon { + min-width: 45px; + text-align: center; + } + + .palette-element-text { + display: flex; + flex-direction: column; + font-size: 13px; + line-height: 15px; + padding-left: 10px; + font-family: OpenSans-Regular, sans-serif; + overflow: hidden; + + .palette-element-name { + color: @sdcui_color_dark-gray; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + +}
\ No newline at end of file @@ -0,0 +1,30 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {ConfigureFn, configureTests} from "../../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {PaletteElementComponent} from "./palette-element.component"; +import {ResourceNamePipe} from "../../../../pipes/resource-name.pipe"; + +describe('palette element component', () => { + + let fixture: ComponentFixture<PaletteElementComponent>; + + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [PaletteElementComponent, ResourceNamePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(PaletteElementComponent); + }); + }) + ); + + it('should match current snapshot of palette element component', () => { + expect(fixture).toMatchSnapshot(); + }); +});
\ No newline at end of file @@ -1,3 +1,6 @@ +/** + * Created by ob0695 on 6/28/2018. + */ /*- * ============LICENSE_START======================================================= * SDC @@ -18,22 +21,15 @@ * ============LICENSE_END========================================================= */ -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; -import { GroupInstance } from 'app/models/graph/zones/group-instance'; +import {Component, Input} from "@angular/core"; +import {LeftPaletteComponent} from "app/models/components/displayComponent"; @Component({ - selector: 'group-information-tab', - templateUrl: './group-information-tab.component.html', - styleUrls: ['./../base/base-tab.component.less'] + selector: 'palette-element', + templateUrl: './palette-element.component.html', + styleUrls: ['./palette-element.component.less'] }) -export class GroupInformationTabComponent { - - @Input() group: GroupInstance; - @Input() isViewOnly: boolean; - - constructor() { - - } +export class PaletteElementComponent { + @Input() paletteElement: LeftPaletteComponent; } @@ -23,3 +23,8 @@ <div class="popup-panel-title">{{panelTitle}}</div> </div> </div> +<!--<popup-menu-list [menuItemsData]="getMenuItems()">--> + + + +<!--</popup-menu-list>-->
\ No newline at end of file @@ -1,8 +1,8 @@ import {Component, OnInit} from '@angular/core'; -import {GRAPH_EVENTS} from "app/utils"; +import {GRAPH_EVENTS, SdcElementType} from "app/utils"; import {LeftPaletteComponent, Point} from "app/models"; import {EventListenerService} from "app/services"; -import {LeftPaletteMetadataTypes} from "../../../../models/components/displayComponent"; +import {LeftPaletteMetadataTypes} from "app/models/components/displayComponent"; @Component({ selector: 'app-palette-popup-panel', @@ -29,6 +29,18 @@ export class PalettePopupPanelComponent implements OnInit { this.isShowPanel = true; } + public getMenuItems = () => { + return [{ + text: 'Delete', + iconName: 'trash-o', + iconType: 'common', + iconMode: 'secondary', + iconSize: 'small', + type: '', + action: () => this.addZoneInstance() + }]; + } + public onMouseLeave() { this.isShowPanel = false; } @@ -43,9 +55,7 @@ export class PalettePopupPanelComponent implements OnInit { private registerObserverCallbacks() { this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, - (component: Component, displayComponent: LeftPaletteComponent, sectionElem: HTMLElement) => { - - this.component = component; + (displayComponent: LeftPaletteComponent, sectionElem: HTMLElement) => { this.showPopupPanel(displayComponent, sectionElem); }); @@ -60,12 +70,12 @@ export class PalettePopupPanelComponent implements OnInit { }; private setPopupPanelTitle(component: LeftPaletteComponent): void { - if (component.categoryType === LeftPaletteMetadataTypes.Group) { + if (component.componentSubType === SdcElementType.GROUP) { this.panelTitle = "Add Group"; return; } - if (component.categoryType === LeftPaletteMetadataTypes.Policy) { + if (component.componentSubType === SdcElementType.POLICY) { this.panelTitle = "Add Policy"; return; } @@ -0,0 +1,41 @@ +<div class="composition-palette-component"> + <div class="palette-elements-count">Elements + <span class="palette-elements-count-value">{{numberOfElements}}</span> + </div> + + <sdc-filter-bar placeholder="Search..." (valueChange)="onSearchChanged($event)" testId="searchAsset"></sdc-filter-bar> + + <div class="palette-elements-list"> + <sdc-loader [global]="false" name="palette-loader" testId="palette-loader" [active]="this.isPaletteLoading" [class.inactive]="!this.isPaletteLoading"></sdc-loader> + <div *ngIf="numberOfElements === 0 && searchText" class="no-elements-found">No Elements Found</div> + <sdc-accordion *ngFor="let mapByCategory of paletteElements | keyValue; let first = first" [attr.data-tests-id]="'leftPalette.category.'+mapByCategory.key" [title]="mapByCategory.key" [css-class]="'palette-category'"> + <div *ngFor="let mapBySubCategory of mapByCategory.value | keyValue"> + <div class="palette-subcategory">{{mapBySubCategory.key}}</div> + <ng-container *ngIf="!(isViewOnly$ | async)"> + <div *ngFor="let paletteElement of mapBySubCategory.value" + [dndDraggable]="paletteElement" + [dndDisableIf]="paletteElement.componentSubType == 'GROUP' && paletteElement.componentSubType == 'POLICY'" + (dndStart)="onDragStart($event, paletteElement)" + (drag)="onDraggableMoved($event)" + [dndEffectAllowed]="'copyMove'" + (mouseenter)="onMouseOver($event, paletteElement)" + (mouseleave)="onMouseOut(paletteElement)" + [attr.data-tests-id]="paletteElement.name"> + <palette-element [paletteElement]="paletteElement"></palette-element> + </div> + </ng-container> + <ng-container *ngIf="(isViewOnly$ | async)"> + <div *ngFor="let paletteElement of mapBySubCategory.value" + [attr.data-tests-id]="paletteElement.name"> + <palette-element [paletteElement]="paletteElement"></palette-element> + </div> + </ng-container> + </div> + </sdc-accordion> + </div> +</div> + +<div id="draggable_element" dndDropzone (dndDrop)="onDrop($event)" [dndAllowExternal]="true"> + <sdc-element-icon *ngIf="paletteDraggedElement" [iconName]="paletteDraggedElement.icon" + [elementType]="paletteDraggedElement.componentSubType" [uncertified]="paletteDraggedElement.certifiedIconClass"></sdc-element-icon> +</div> @@ -0,0 +1,84 @@ +@import "./../../../../../assets/styles/override"; + +:host(composition-palette) { + display:flex; + flex: 0 0 244px; +} + +sdc-loader.inactive { + display:none; +} + +:host ::ng-deep .sdc-filter-bar .sdc-input { + margin-bottom:0px; +} +:host ::ng-deep .sdc-loader-wrapper { + position:static; +} + +.composition-palette-component { + background-color: @sdcui_color_white; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + position:relative; + width: 244px; + box-shadow: 7px -3px 6px -8px @sdcui_color_gray; + + .palette-elements-count { + background-color: @sdcui_color_gray; + line-height: 40px; + padding: 0 17px; + color: @sdcui_color_white; + .palette-elements-count-value { + float: right; + } + } + + .palette-elements-list { + + .no-elements-found { + padding-left: 40px; + } + /deep/ .palette-category { + display: flex; + margin: 0px; + .sdc-accordion-header { + background-color: @sdcui_color_silver; + margin: 0px; + line-height: 40px; + padding: 0px 10px; + } + .sdc-accordion-body { + padding: 0px; + } + } + .palette-subcategory { + padding: 0 10px; + background-color: @sdcui_color_lighter-silver; + line-height: 35px; + } + } +} + +#draggable_element { + display: inline-block; + border-radius: 50%; + background: transparent; + position: absolute; + top: -9999px; + left: 0; + z-index: 100; +} + +.invalid-drag { + border: 7px solid @red-shadow; +} + +.valid-drag { + border: 7px solid @green-shadow; +} + +@green-shadow: rgba(29, 154, 149, 0.3); +@red-shadow: rgba(218, 31, 61, 0.3); @@ -0,0 +1,102 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {CompositionPaletteService} from "./services/palette.service"; +import {EventListenerService} from "../../../../services/event-listener-service"; +import {PaletteElementComponent} from "./palette-element/palette-element.component"; +import {PaletteComponent} from "./palette.component"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {GRAPH_EVENTS} from "../../../../utils/constants"; +import {KeyValuePipe} from "../../../pipes/key-value.pipe"; +import {ResourceNamePipe} from "../../../pipes/resource-name.pipe"; +import {LeftPaletteComponent} from "../../../../models/components/displayComponent"; +import {Observable} from "rxjs/Observable"; +import {leftPaletteElements} from "../../../../../jest/mocks/left-paeltte-elements.mock"; +import {NgxsModule, Select} from '@ngxs/store'; +import { WorkspaceState } from 'app/ng2/store/states/workspace.state'; + + +describe('palette component', () => { + + const mockedEvent = <MouseEvent>{ target: {} } + let fixture: ComponentFixture<PaletteComponent>; + let eventServiceMock: Partial<EventListenerService>; + let compositionPaletteMockService: Partial<CompositionPaletteService>; + + beforeEach( + async(() => { + eventServiceMock = { + notifyObservers: jest.fn() + } + compositionPaletteMockService = { + subscribeToLeftPaletteElements: jest.fn().mockImplementation(()=> Observable.of(leftPaletteElements)), + getLeftPaletteElements: jest.fn().mockImplementation(()=> leftPaletteElements) + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [PaletteComponent, PaletteElementComponent, KeyValuePipe, ResourceNamePipe], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: CompositionPaletteService, useValue: compositionPaletteMockService}, + {provide: EventListenerService, useValue: eventServiceMock} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(PaletteComponent); + }); + }) + ); + + it('should match current snapshot of palette component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should call on palette component hover in event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'COMPONENT'}; + fixture.componentInstance.onMouseOver(mockedEvent, paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, paletteObject); + }); + + it('should call on palette component hover out event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'COMPONENT'}; + fixture.componentInstance.onMouseOut(paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT); + }); + + it('should call show popup panel event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'GROUP'}; + fixture.componentInstance.onMouseOver(mockedEvent, paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, paletteObject, mockedEvent.target); + }); + + it('should call hide popup panel event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'GROUP'}; + fixture.componentInstance.onMouseOut(paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL); + }); + + it('should build Palette By Categories without searchText', () => { + fixture.componentInstance.buildPaletteByCategories(); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"].length).toBe(5); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"][0].searchFilterTerms).toBe("extvirtualmachineinterfacecp external port for virtual machine interface extvirtualmachineinterfacecp 3.0"); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"][1].searchFilterTerms).toBe("newservice2 asdfasdfa newservice2 0.3"); + + expect(fixture.componentInstance.paletteElements["Generic"]["Configuration"].length).toBe(1); + expect(fixture.componentInstance.paletteElements["Generic"]["Configuration"][0].systemName).toBe("Extvirtualmachineinterfacecp"); + }); + + it('should build Palette By Categories with searchText', () => { + fixture.componentInstance.buildPaletteByCategories("testVal"); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"].length).toBe(1); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"][0].searchFilterTerms).toBe("testVal and other values"); + }); + + it('should change numbers of elements', () => { + fixture.componentInstance.buildPaletteByCategories(); + expect(fixture.componentInstance.numberOfElements).toEqual(6); + fixture.componentInstance.buildPaletteByCategories("testVal"); + expect(fixture.componentInstance.numberOfElements).toEqual(1); + }); +});
\ No newline at end of file @@ -0,0 +1,172 @@ +/** + * Created by ob0695 on 6/28/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Component, HostListener } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { LeftPaletteComponent, LeftPaletteMetadataTypes } from 'app/models/components/displayComponent'; +import { Point } from 'app/models/graph/point'; +import { WorkspaceState } from 'app/ng2/store/states/workspace.state'; +import Dictionary = _.Dictionary; +import { EventListenerService } from 'app/services/event-listener-service'; +import { GRAPH_EVENTS } from 'app/utils/constants'; +import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop'; +import { CompositionPaletteService } from './services/palette.service'; +import {PolicyMetadata} from "../../../../models/policy-metadata"; +import {GenericBrowserDomAdapter} from "@angular/platform-browser/src/browser/generic_browser_adapter"; + +@Component({ + selector: 'composition-palette', + templateUrl: './palette.component.html', + styleUrls: ['./palette.component.less'] +}) +export class PaletteComponent { + + constructor(private compositionPaletteService: CompositionPaletteService, private eventListenerService: EventListenerService) {} + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + private paletteElements: Dictionary<Dictionary<LeftPaletteComponent[]>>; + public numberOfElements: number = 0; + public isPaletteLoading: boolean; + private paletteDraggedElement: LeftPaletteComponent; + public position: Point = new Point(); + + ngOnInit() { + this.isPaletteLoading = true; + + this.compositionPaletteService.subscribeToLeftPaletteElements((leftPaletteElementsResponse) => { + this.paletteElements = leftPaletteElementsResponse; + this.numberOfElements = this.countLeftPalleteElements(this.paletteElements); + this.isPaletteLoading = false; + }, () => { + this.isPaletteLoading = false; + }); + + } + + public buildPaletteByCategories = (searchText?: string) => { // create nested by category & subcategory, filtered by search parans + // Flat the object and run on its leaves + if (searchText) { + searchText = searchText.toLowerCase(); + const paletteElementsAfterSearch = {}; + this.paletteElements = this.compositionPaletteService.getLeftPaletteElements(); + for (const category in this.paletteElements) { + for (const subCategory in this.paletteElements[category]) { + const subCategoryToCheck = this.paletteElements[category][subCategory]; + const res = subCategoryToCheck.filter((item) => item.searchFilterTerms.toLowerCase().indexOf(searchText) >= 0) + if (res.length > 0) { + paletteElementsAfterSearch[category] = {}; + paletteElementsAfterSearch[category][subCategory] = res; + } + } + } + this.paletteElements = paletteElementsAfterSearch; + } else { + this.paletteElements = this.compositionPaletteService.getLeftPaletteElements(); + } + this.numberOfElements = this.countLeftPalleteElements(this.paletteElements); + } + + public onSearchChanged = (searchText: string) => { + + if (this.compositionPaletteService.getLeftPaletteElements()) { + this.buildPaletteByCategories(searchText); + } + } + + private countLeftPalleteElements(leftPalleteElements: Dictionary<Dictionary<LeftPaletteComponent[]>>) { + // Use _ & flat map + let counter = 0; + for (const category in leftPalleteElements) { + for (const subCategory in leftPalleteElements[category]) { + counter += leftPalleteElements[category][subCategory].length; + } + } + return counter; + } + + private isGroupOrPolicy(component: LeftPaletteComponent): boolean { + if (component && + (component.categoryType === LeftPaletteMetadataTypes.Group || + component.categoryType === LeftPaletteMetadataTypes.Policy)) { + return true; + } + return false; + } + @HostListener('document:dragover', ['$event']) + public onDrag(event) { + this.position.x = event.clientX; + this.position.y = event.clientY; + } + + //---------------------------------------Palette Events-----------------------------------------// + + public onDraggableMoved = (event:DragEvent) => { + let draggedElement = document.getElementById("draggable_element"); + draggedElement.style.top = (this.position.y - 80) + "px"; + draggedElement.style.left = (this.position.x - 30) + "px"; + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, this.position); + } + + public onDragStart = (event, draggedElement:LeftPaletteComponent) => { // Applying the dragged svg component to the draggable element + + this.paletteDraggedElement = draggedElement; + event.dataTransfer.dropEffect = "copy"; + let hiddenImg = document.createElement("span"); + event.dataTransfer.setDragImage(hiddenImg, 0, 0); + } + + + public onDrop = (event:DndDropEvent) => { + let draggedElement = document.getElementById("draggable_element"); + draggedElement.style.top = "-9999px"; + if(draggedElement.classList.contains('valid-drag')) { + if(!event.data){ + event.data = this.paletteDraggedElement; + } + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, event); + } else { + console.log("INVALID drop"); + } + this.paletteDraggedElement = undefined; + + } + + public onMouseOver = (sectionElem:MouseEvent, displayComponent:LeftPaletteComponent) => { + console.debug("On palette element MOUSE HOVER: ", displayComponent); + if (this.isGroupOrPolicy(displayComponent)) { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, displayComponent, sectionElem.target); + } else { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, displayComponent); + } + }; + + public onMouseOut = (displayComponent:LeftPaletteComponent) => { + console.debug("On palette element MOUSE OUT: ", displayComponent); + if (this.isGroupOrPolicy(displayComponent)) { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL); + } else { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT); + } + }; + +} @@ -0,0 +1,25 @@ +import { NgModule } from "@angular/core"; +import { CompositionPaletteService } from "./services/palette.service"; +import { PaletteComponent } from "./palette.component"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { GlobalPipesModule } from "app/ng2/pipes/global-pipes.module"; +import { CommonModule } from "@angular/common"; +import { DndModule } from "ngx-drag-drop"; +import {PaletteElementComponent} from "./palette-element/palette-element.component"; +import {EventListenerService} from "app/services/event-listener-service"; +import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; + +@NgModule({ + declarations: [PaletteComponent, PaletteElementComponent], + imports: [CommonModule, SdcUiComponentsModule, GlobalPipesModule, UiElementsModule, DndModule], + exports: [PaletteComponent], + entryComponents: [PaletteComponent], + providers: [CompositionPaletteService, EventListenerService] +}) +export class PaletteModule { + + constructor() { + + } + +}
\ No newline at end of file @@ -0,0 +1,41 @@ +import {TestBed} from "@angular/core/testing"; +import {CompositionPaletteService} from "./palette.service"; +import {ISdcConfig, SdcConfigToken} from "../../../../config/sdc-config.config"; +import {WorkspaceService} from "../../../../pages/workspace/workspace.service"; +import { HttpClient } from "@angular/common/http"; +describe('palette component', () => { + + let service: CompositionPaletteService; + + let httpServiceMock: Partial<HttpClient> = { + get: jest.fn() + } + + let sdcConfigToken: Partial<ISdcConfig> = { + "api": { + "root": '' + } + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [CompositionPaletteService, + {provide: HttpClient, useValue: httpServiceMock}, + {provide: SdcConfigToken, useValue: sdcConfigToken}, + {provide: WorkspaceService, useValue{}} + ] + }); + + service = TestBed.get(CompositionPaletteService); + }); + + it('should create an instance', () => { + expect(service).toBeDefined(); + }); + + // it('should create an instance2', async () => { + // expect(await service.subscribeToLeftPaletteElements("resources")).toEqual([]); + // }); +}); + @@ -0,0 +1,98 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { LeftPaletteComponent, LeftPaletteMetadataTypes } from 'app/models/components/displayComponent'; +import { GroupMetadata } from 'app/models/group-metadata'; +import { PolicyMetadata } from 'app/models/policy-metadata'; +import { SdcConfigToken } from 'app/ng2/config/sdc-config.config'; +import { ISdcConfig } from 'app/ng2/config/sdc-config.config.factory'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import 'rxjs/add/observable/forkJoin'; +import { Observable } from 'rxjs/Rx'; +import Dictionary = _.Dictionary; + + + +@Injectable() +export class CompositionPaletteService { + + protected baseUrl = ''; + + private leftPaletteComponents: Dictionary<Dictionary<LeftPaletteComponent[]>>; + private facadeUrl: string; + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig, private workspaceService: WorkspaceService) { + this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; + this.facadeUrl = sdcConfig.api.uicache_root + sdcConfig.api.GET_uicache_left_palette; + + } + + public subscribeToLeftPaletteElements(next, error) { + + let params = new HttpParams(); + params = params.append('internalComponentType', this.workspaceService.getMetadataType()); + + const loadInstances = this.http.get(this.facadeUrl, {params}); + const loadGroups = this.http.get(this.baseUrl + 'groupTypes', {params}); + const loadPolicies = this.http.get(this.baseUrl + 'policyTypes', {params}); + + Observable.forkJoin( + loadInstances, loadGroups, loadPolicies + ).subscribe( ([resInstances, resGrouops, resPolicies]) => { + const combinedDictionary = this.combineResoponses(resInstances, resGrouops, resPolicies); + this.leftPaletteComponents = combinedDictionary; + next(this.leftPaletteComponents); + }); + } + + public getLeftPaletteElements = (): Dictionary<Dictionary<LeftPaletteComponent[]>> => { + return this.leftPaletteComponents; + } + + + public convertPoliciesOrGroups = (paletteListResult, type: string ) => { + const components: LeftPaletteComponent[] = []; + + if (type === 'Policies') { + _.forEach(paletteListResult, (policyMetadata: PolicyMetadata) => { + components.push(new LeftPaletteComponent(LeftPaletteMetadataTypes.Policy, policyMetadata)); + }); + return { + Policies: components + }; + } + + if (type === 'Groups') { + _.forEach(paletteListResult, (groupMetadata: GroupMetadata) => { + const item = new LeftPaletteComponent(LeftPaletteMetadataTypes.Group, groupMetadata); + components.push(item); + }); + return { + Groups: components + }; + } + + return {}; + } + + private combineResoponses(resInstances: object, resGrouops: object, resPolicies: object) { + const retValObject = {}; + // Generic will be the 1st category in the left Pallete + if (resInstances['Generic']) { + retValObject['Generic'] = resInstances['Generic']; + } + // Add all other categories + for (const category in resInstances) { + if (category === 'Generic') { + continue; + } + retValObject[category] = resInstances[category]; + } + + // Add Groups + retValObject["Groups"] = this.convertPoliciesOrGroups(resGrouops, 'Groups'); + + // Add policies + retValObject["Policies"] = this.convertPoliciesOrGroups(resPolicies, 'Policies'); + + return retValObject; + } +} @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`composition-panel component should match current snapshot of composition-panel component. 1`] = ` +<ng2-composition-panel + activatePreviousActiveTab={[Function Function]} + classes={[Function String]} + initTabs={[Function Function]} + isComponentInstanceSelected={[Function Function]} + isConfiguration={[Function Function]} + isPNF={[Function Function]} + selectedComponentIsServiceProxyInstance={[Function Function]} + setActive={[Function Function]} + store={[Function Store]} + toggleSidebarDisplay={[Function Function]} +> + +</ng2-composition-panel> +`; @@ -0,0 +1,21 @@ +<panel-wrapper-component *ngIf="compositionState$ | async as state"> <!-- HEADER --> + + <ng2-composition-panel-header [isViewOnly]="state.isViewOnly" + [selectedComponent]="state.selectedComponent"></ng2-composition-panel-header> + + <!-- TABS --> + <div class="component-details-panel-tabs"> + <sdc-loader [global]="false" name="panel" testId="panel-loader" [active]="state.panelLoading"></sdc-loader> + <sdc-tabs (selectedTab)="setActive($event)" [iconsSize]="'large'"> + <sdc-tab *ngFor="let tab of tabs" [titleIcon]="tab.titleIcon" [active]="tab.isActive" + [tooltipText]="tab.tooltipText"> + <panel-tab [isActive]="tab.isActive" [component]="selectedComponent" + [componentType]="state.selectedComponentType" [isViewOnly]="isViewOnly$ | async" + [input]="tab.input" [panelTabType]="tab.component"></panel-tab> + </sdc-tab> + </sdc-tabs> + </div> + +</panel-wrapper-component> + + @@ -0,0 +1,27 @@ +@import '../../../../../assets/styles/variables'; +@import '../../../../../assets/styles/mixins_old'; + +:host ::ng-deep .sdc-loader-wrapper { + position:static; +} + +.component-details-panel-tabs { + flex: 1; + display:flex; + overflow:hidden; + } + +.component-details-panel-tabs /deep/ sdc-tabs { + display:flex; + flex-direction:column; + + /deep/ sdc-tab { + display: flex; + flex-direction: column; + overflow-y: auto; + } + .svg-icon-wrapper.label-placement-left .svg-icon-label { + margin-right: 0; + } +} + @@ -0,0 +1,228 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxsModule, Store } from '@ngxs/store'; +import { Observable } from 'rxjs'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { Service } from '../../../../models/components/service'; +import { Resource } from '../../../../models/components/resource'; +import { GroupInstance } from '../../../../models/graph/zones/group-instance'; +import { PolicyInstance } from '../../../../models/graph/zones/policy-instance'; +import { ArtifactGroupType, ResourceType } from '../../../../utils/constants'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { CompositionPanelComponent } from './composition-panel.component'; +import { ArtifactsTabComponent } from './panel-tabs/artifacts-tab/artifacts-tab.component'; +import { GroupMembersTabComponent } from './panel-tabs/group-members-tab/group-members-tab.component'; +import { GroupOrPolicyPropertiesTab } from './panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component'; +import { InfoTabComponent } from './panel-tabs/info-tab/info-tab.component'; +import { PolicyTargetsTabComponent } from './panel-tabs/policy-targets-tab/policy-targets-tab.component'; +import { PropertiesTabComponent } from './panel-tabs/properties-tab/properties-tab.component'; +import { ReqAndCapabilitiesTabComponent } from './panel-tabs/req-capabilities-tab/req-capabilities-tab.component'; + +describe('composition-panel component', () => { + + let fixture: ComponentFixture<CompositionPanelComponent>; + let store: Store; + + const tabs = { + infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'}, + policyProperties: { + titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'policy'}, isActive: false, tooltipText: 'Properties' + }, + policyTargets: {titleIcon: 'inputs-o', component: PolicyTargetsTabComponent, input: {}, isActive: false, tooltipText: 'Targets'}, + groupMembers: {titleIcon: 'inputs-o', component: GroupMembersTabComponent, input: {}, isActive: false, tooltipText: 'Members'}, + groupProperties: { + titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'group'}, isActive: false, tooltipText: 'Properties' + }, + deploymentArtifacts: { + titleIcon: 'deployment-artifacts-o', component: ArtifactsTabComponent, + input: { type: ArtifactGroupType.DEPLOYMENT}, isActive: false, tooltipText: 'Deployment Artifacts' + }, + apiArtifacts: { + titleIcon: 'api-o', component: ArtifactsTabComponent, + input: { type: ArtifactGroupType.SERVICE_API}, isActive: false, tooltipText: 'API Artifacts' + }, + infoArtifacts: { + titleIcon: 'info-square-o', component: ArtifactsTabComponent, + input: { type: ArtifactGroupType.INFORMATION}, isActive: false, tooltipText: 'Information Artifacts' + }, + properties: { + titleIcon: 'settings-o', component: PropertiesTabComponent, + input: {title: 'Properties and Attributes'}, isActive: false, tooltipText: 'Properties' + }, + reqAndCapabilities : { + titleIcon: 'req-capabilities-o', component: ReqAndCapabilitiesTabComponent, input: {}, + isActive: false, tooltipText: 'Requirements and Capabilities' + }, + inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'}, + settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'}, + }; + + beforeEach( + async(() => { + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [CompositionPanelComponent], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(CompositionPanelComponent); + store = testBed.get(Store); + }); + }) + ); + + it('When PolicyInstance Selected => Expect (info, policyTargets and policyProperties) tabs appear', () => { + + const testInstance = new PolicyInstance(); + + fixture.componentInstance.initTabs(testInstance); + + expect (fixture.componentInstance.tabs.length).toBe(3); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.policyTargets); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.policyProperties); + }); + + it('should match current snapshot of composition-panel component.', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('When Topology Template is Service and no instance is selected Expect (info, deployment, inputs, info and api)', () => { + + const selectedComponent: Service = new Service(null, null); + selectedComponent.isResource = jest.fn(() => false); + selectedComponent.isService = jest.fn(() => true ); + + fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent)); + + // const pnfMock = Mock.of<Service>({ isResource : () => false }); + fixture.componentInstance.topologyTemplate = selectedComponent; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.inputs); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.apiArtifacts); + + }); + + it('When Topology Template is Resource and no instance is selected Expect (info, deployment, inputs, info and api)', () => { + + const selectedComponent: Service = new Service(null, null); + selectedComponent.isResource = jest.fn(() => true); + selectedComponent.isService = jest.fn(() => false ); + + fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent)); + + fixture.componentInstance.topologyTemplate = selectedComponent; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When Topology Template is Service and proxyService instance is selected ' + + 'Expect (info, deployment, inputs, info and api)', () => { + + const selectedComponent: Service = new Service(null, null); + selectedComponent.isResource = jest.fn(() => false); + selectedComponent.isService = jest.fn(() => true ); + + fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent)); + fixture.componentInstance.selectedComponentIsServiceProxyInstance = jest.fn(() => true); + + // const pnfMock = Mock.of<Service>({ isResource : () => false }); + fixture.componentInstance.topologyTemplate = selectedComponent; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When Topology Template is Resource and VL is selected ' + + 'Expect (info, deployment, inputs, info and api)', () => { + + const topologyTemplate: Resource = new Resource(null, null); + topologyTemplate.isResource = jest.fn(() => true); + topologyTemplate.isService = jest.fn(() => false ); + + const vlMock = Mock.of<Resource>({ resourceType : 'VL', isResource : () => true, isService : () => false }); + fixture.componentInstance.store.select = jest.fn(() => Observable.of(vlMock)); + + fixture.componentInstance.topologyTemplate = topologyTemplate; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When Topology Template is Service and VL is selected ' + + 'Expect (info, deployment, inputs, info and api)', () => { + + const topologyTemplate: Service = new Service(null, null); + topologyTemplate.isResource = jest.fn(() => true); + topologyTemplate.isService = jest.fn(() => false ); + + const vlMock = Mock.of<Resource>({ resourceType : 'VL', isResource : () => true, isService : () => false }); + fixture.componentInstance.store.select = jest.fn(() => Observable.of(vlMock)); + + fixture.componentInstance.topologyTemplate = topologyTemplate; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When GroupInstance Selected => Expect (info, groupMembers and groupProperties) tabs appear.', () => { + + const testInstance = new GroupInstance(); + fixture.componentInstance.initTabs(testInstance); + + expect (fixture.componentInstance.tabs.length).toBe(3); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.groupMembers); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.groupProperties); + }); + +}); @@ -0,0 +1,171 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Component, HostBinding, Input } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { Component as TopologyTemplate, ComponentInstance, FullComponentInstance, GroupInstance, PolicyInstance, Resource, Service } from 'app/models'; +import { ArtifactsTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component'; +import { GroupMembersTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component'; +import { GroupOrPolicyPropertiesTab } from 'app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component'; +import { InfoTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component'; +import { PolicyTargetsTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component'; +import { PropertiesTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component'; +import { ReqAndCapabilitiesTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component'; +import { ComponentType, ResourceType } from 'app/utils'; +import * as _ from 'lodash'; +import { Subscription } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; +import { ArtifactGroupType, COMPONENT_FIELDS } from '../../../../utils/constants'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { OnSidebarOpenOrCloseAction } from '../common/store/graph.actions'; +import { CompositionStateModel, GraphState } from '../common/store/graph.state'; +import { ServiceConsumptionTabComponent } from './panel-tabs/service-consumption-tab/service-consumption-tab.component'; +import { ServiceDependenciesTabComponent } from './panel-tabs/service-dependencies-tab/service-dependencies-tab.component'; + +const tabs = { + infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'}, + policyProperties: {titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'policy'}, isActive: false, tooltipText: 'Properties'}, + policyTargets: {titleIcon: 'inputs-o', component: PolicyTargetsTabComponent, input: {}, isActive: false, tooltipText: 'Targets'}, + groupMembers: {titleIcon: 'inputs-o', component: GroupMembersTabComponent, input: {}, isActive: false, tooltipText: 'Members'}, + groupProperties: {titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'group'}, isActive: false, tooltipText: 'Properties'}, + deploymentArtifacts: {titleIcon: 'deployment-artifacts-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.DEPLOYMENT}, isActive: false, tooltipText: 'Deployment Artifacts'}, + apiArtifacts: {titleIcon: 'api-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.SERVICE_API}, isActive: false, tooltipText: 'API Artifacts'}, + infoArtifacts: {titleIcon: 'info-square-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.INFORMATION}, isActive: false, tooltipText: 'Information Artifacts'}, + properties: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {title: 'Properties and Attributes'}, isActive: false, tooltipText: 'Properties'}, + reqAndCapabilities : { titleIcon: 'req-capabilities-o', component: ReqAndCapabilitiesTabComponent, input: {}, isActive: false, tooltipText: 'Requirements and Capabilities'}, + inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'}, + settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'}, + consumption: {titleIcon: 'api-o', component: ServiceConsumptionTabComponent, input: {title: 'OPERATION CONSUMPTION'}, isActive: false, tooltipText: 'Service Consumption'}, + dependencies: {titleIcon: 'archive', component: ServiceDependenciesTabComponent, input: {title: 'SERVICE DEPENDENCIES'}, isActive: false, tooltipText: 'Service Dependencies'} +}; + +@Component({ + selector: 'ng2-composition-panel', + templateUrl: './composition-panel.component.html', + styleUrls: ['./composition-panel.component.less', './panel-tabs/panel-tabs.less'], +}) +export class CompositionPanelComponent { + + @Input() topologyTemplate: TopologyTemplate; + @HostBinding('class') classes = 'component-details-panel'; + @Select(GraphState) compositionState$: Observable<CompositionStateModel>; + @Select(GraphState.withSidebar) withSidebar$: boolean; + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + tabs: any[]; + subscription: Subscription; + + private selectedComponent; + + constructor(public store: Store) { + } + + ngOnInit() { + this.subscription = this.store.select(GraphState.getSelectedComponent).subscribe((component) => { + this.selectedComponent = component; + this.initTabs(component); + this.activatePreviousActiveTab(); + }); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public setActive = (tabToSelect) => { + this.tabs.map((tab) => tab.isActive = (tab.titleIcon === tabToSelect.titleIcon) ? true : false); + } + + public activatePreviousActiveTab = () => { // sets the info tab to active if no other tab selected + + this.setActive(this.tabs.find((tab) => tab.isActive) || tabs.infoTab); + + } + + private initTabs = (component) => { + this.tabs = []; + + // Information + this.tabs.push(tabs.infoTab); + + if (component instanceof PolicyInstance) { + this.tabs.push(tabs.policyTargets); + this.tabs.push(tabs.policyProperties); + return; + } + + if (component instanceof GroupInstance) { + this.tabs.push(tabs.groupMembers); + this.tabs.push(tabs.groupProperties); + return; + } + + // Deployment artifacts + if (!this.isPNF() && !this.isConfiguration() && !this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.deploymentArtifacts); + } + + // Properties or Inputs + if (component.isResource() || this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.properties); + } else { + this.tabs.push(tabs.inputs); + } + + if (!this.isConfiguration() && !this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.infoArtifacts); + } + + if (!(component.isService()) || this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.reqAndCapabilities); + } + + if (component.isService() && !this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.apiArtifacts); + } + if (component.isService() && this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.consumption); + this.tabs.push(tabs.dependencies); + } + + } + + private toggleSidebarDisplay = () => { + // this.withSidebar = !this.withSidebar; + this.store.dispatch(new OnSidebarOpenOrCloseAction()); + } + + private isPNF = (): boolean => { + return this.topologyTemplate.isResource() && (this.topologyTemplate as Resource).resourceType === ResourceType.PNF; + } + + private isConfiguration = (): boolean => { + return this.topologyTemplate.isResource() && (this.topologyTemplate as Resource).resourceType === ResourceType.CONFIGURATION; + } + + private isComponentInstanceSelected = (): boolean => { + return this.selectedComponent instanceof FullComponentInstance; + } + + private selectedComponentIsServiceProxyInstance = (): boolean => { + return this.isComponentInstanceSelected() && this.selectedComponent.isServiceProxy(); + } +} @@ -0,0 +1,106 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { BrowserModule } from "@angular/platform-browser"; +import { CompositionPanelComponent } from "./composition-panel.component"; +import { CompositionPanelHeaderModule } from "app/ng2/pages/composition/panel/panel-header/panel-header.module"; +import { SdcUiComponentsModule, SdcUiServices } from "onap-ui-angular"; +// import { SdcUiServices } from "onap-ui-angular/"; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { AddElementsModule } from "../../../components/ui/modal/add-elements/add-elements.module"; +import { TranslateModule } from "app/ng2/shared/translator/translate.module"; +import { InfoTabComponent } from './panel-tabs/info-tab/info-tab.component'; +import { PanelTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/panel-tab.component"; +import { ArtifactsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component"; +import { PropertiesTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component"; +import { ReqAndCapabilitiesTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component"; +import { RequirementListComponent } from "app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component"; +import { PolicyTargetsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component"; +import { GroupMembersTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component"; +import { GroupOrPolicyPropertiesTab } from "app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component"; +import { GlobalPipesModule } from "app/ng2/pipes/global-pipes.module"; +import {ModalModule} from "../../../components/ui/modal/modal.module"; +import {EnvParamsComponent} from "../../../components/forms/env-params/env-params.component"; +import {ModalsModule} from "../../../components/modals/modals.module"; +// import {EnvParamsModule} from "../../../components/forms/env-params/env-params.module"; +import { NgxDatatableModule } from "@swimlane/ngx-datatable"; +import {EnvParamsModule} from "../../../components/forms/env-params/env-params.module"; +import { ServiceConsumptionTabComponent } from "./panel-tabs/service-consumption-tab/service-consumption-tab.component"; +import { ServiceDependenciesTabComponent } from "./panel-tabs/service-dependencies-tab/service-dependencies-tab.component"; +import { ServiceDependenciesModule } from "../../../components/logic/service-dependencies/service-dependencies.module"; +import { ServiceConsumptionModule } from "../../../components/logic/service-consumption/service-consumption.module"; + + + +@NgModule({ + declarations: [ + CompositionPanelComponent, + PolicyTargetsTabComponent, + GroupOrPolicyPropertiesTab, + GroupMembersTabComponent, + InfoTabComponent, + PanelTabComponent, + ArtifactsTabComponent, + PropertiesTabComponent, + ReqAndCapabilitiesTabComponent, + ServiceConsumptionTabComponent, + ServiceDependenciesTabComponent, + RequirementListComponent, + EnvParamsComponent + ], + imports: [ + GlobalPipesModule, + BrowserModule, + FormsModule, + CompositionPanelHeaderModule, + SdcUiComponentsModule, + UiElementsModule, + AddElementsModule, + TranslateModule, + NgxDatatableModule, + ServiceDependenciesModule, + ServiceConsumptionModule + // EnvParamsModule + ], + entryComponents: [ + CompositionPanelComponent, + PolicyTargetsTabComponent, + GroupOrPolicyPropertiesTab, + GroupMembersTabComponent, + InfoTabComponent, + ArtifactsTabComponent, + PropertiesTabComponent, + ReqAndCapabilitiesTabComponent, + ServiceConsumptionTabComponent, + ServiceDependenciesTabComponent, + RequirementListComponent, + PanelTabComponent, + EnvParamsComponent + ], + exports: [ + CompositionPanelComponent + // EnvParamsModule + ], + providers: [SdcUiServices.ModalService] +}) +export class CompositionPanelModule { + +} @@ -1,11 +1,11 @@ import { Component, Input } from "@angular/core"; @Component({ - selector: 'value-edit', - templateUrl: './value-edit.component.html', - styleUrls: ['./value-edit.component.less'] + selector: 'edit-name-modal', + templateUrl: './edit-name-modal.component.html', + styleUrls: ['./edit-name-modal.component.less'] }) -export class ValueEditComponent { +export class EditNameModalComponent { @Input() name:String; @Input() validityChangedCallback: Function; @@ -1,30 +1,23 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<div class="component-details-panel-header" data-tests-id="w-sdc-designer-sidebar-head"> - +<div *ngIf="selectedComponent" class="component-details-panel-header" data-tests-id="w-sdc-designer-sidebar-head"> <div class="icon"> - <div class="large {{iconClassName}}"> - <div [ngClass]="{'non-certified': nonCertified}" tooltip="Not certified"></div> - </div> + <div *ngIf="iconClassName; else svgIcon" class="large {{iconClassName}}"></div> + <ng-template #svgIcon> + <sdc-element-icon + [elementType]="selectedComponent.componentType === 'RESOURCE' ? selectedComponent.resourceType: (selectedComponent.originType || selectedComponent.componentType)" + [iconName]="selectedComponent.icon" + [uncertified]="!isTopologyTemplateSelected && selectedComponent.lifecycleState && 'CERTIFIED' !== selectedComponent.lifecycleState"></sdc-element-icon> + </ng-template> </div> - <div class="title" data-tests-id="selectedCompTitle" tooltip="​{{name}}">{{name}}</div> + <div class="title" data-tests-id="selectedCompTitle" tooltip="​{{selectedComponent.name}}"> + {{selectedComponent.name}} + </div> + + <svg-icon-label *ngIf="!isViewOnly && !isTopologyTemplateSelected && !selectedComponent.archived" name="edit-file-o" + clickable="true" size="small" class="rename-instance" data-tests-id="renameInstance" + (click)="renameInstance()"></svg-icon-label> + <svg-icon-label *ngIf="!isViewOnly && !isTopologyTemplateSelected && !selectedComponent.archived" name="trash-o" + clickable="true" size="small" class="delete-instance" data-tests-id="deleteInstance" + (click)="deleteInstance()"></svg-icon-label> - <svg-icon-label *ngIf="!isViewOnly" name="edit-file-o" clickable="true" size="small" class="rename-instance" data-tests-id="renameInstance" (click)="renameInstance()"></svg-icon-label> - <svg-icon-label *ngIf="!isViewOnly" name="trash-o" clickable="true" size="small" class="delete-instance" data-tests-id="deleteInstance" (click)="deleteInstance()"></svg-icon-label> - </div>
\ No newline at end of file @@ -7,6 +7,7 @@ .icon { margin: 0 20px; + display:flex; } .title { @@ -31,4 +32,17 @@ cursor: pointer; } + + .non-certified { + position: absolute; + background-image: url('../../../../../../assets/styles/images/sprites/sprite-global-old.png'); + background-position: -157px -3386px; width: 15px; height: 15px; + + &.smaller-icon { + left: 35px; + bottom: -14px; + } + } + + }
\ No newline at end of file @@ -0,0 +1,123 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { EventListenerService } from '../../../../../services/event-listener-service'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { GroupsService } from 'app/services-ng2'; +import { PoliciesService } from 'app/services-ng2'; +import { CompositionPanelHeaderComponent } from './panel-header.component'; +import {SdcUiServices} from 'onap-ui-angular'; +import { Capability, Requirement, RequirementsGroup, CapabilitiesGroup, ComponentInstance, Component, FullComponentInstance, PolicyInstance, GroupInstance } from "app/models"; +import { of, Observable } from "rxjs"; + +describe('CompositionPanelHeaderComponent', () => { + let component: CompositionPanelHeaderComponent; + let fixture: ComponentFixture<CompositionPanelHeaderComponent>; + const componentInstanceServiceNg2Stub = { + updateComponentInstance: jest.fn() + }; + const valueEditModalInstance = { + innerModalContent : { + instance: { name : "VF Test" } + }, + buttons: [{id: 'saveButton', text: 'OK', size: 'xsm', callback: jest.fn(), closeModal: false}], + closeModal : jest.fn() + }; + + beforeEach( + () => { + const compositionServiceStub = {}; + const eventListenerServiceStub = {}; + + const workspaceServiceStub = { + metadata: { + componentType: "SERVICE", + uniqueId: "123" + } + }; + const groupsServiceStub = { + updateName: jest.fn() + }; + const policiesServiceStub = { + updateName: jest.fn() + }; + + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [CompositionPanelHeaderComponent], + providers: [ + { provide: CompositionService, useValue: compositionServiceStub }, + { provide: EventListenerService, useValue: eventListenerServiceStub }, + { + provide: ComponentInstanceServiceNg2, + useValue: componentInstanceServiceNg2Stub + }, + { provide: WorkspaceService, useValue: workspaceServiceStub }, + { provide: GroupsService, useValue: groupsServiceStub }, + { provide: PoliciesService, useValue: policiesServiceStub }, + { provide: SdcUiServices.ModalService, useValue: {}} + ] + }); + fixture = TestBed.createComponent(CompositionPanelHeaderComponent); + component = fixture.componentInstance; + } + ); + + it('can load instance', () => { + expect(component).toBeTruthy(); + }); + + it('should close the modal without saving if the name has not changed', () => { + component.selectedComponent = <FullComponentInstance>{name: "VF Test"}; + component.valueEditModalInstance = valueEditModalInstance; + + component.saveInstanceName(); + expect(component.componentInstanceService.updateComponentInstance).not.toHaveBeenCalled(); + expect(component.valueEditModalInstance.closeModal).toHaveBeenCalled(); + }); + + it('after editing instance name, capabilities/requirements should be updated with new name', () => { + const newName = "New VF NAME"; + component.selectedComponent = new FullComponentInstance(<ComponentInstance>{ + name: "VF Test", + requirements: <RequirementsGroup>{"key": [<Requirement>{ownerName: "VF Test"}, <Requirement>{ownerName: "VF Test"}]}, + capabilities: new CapabilitiesGroup() + }, <Component>{}); + component.selectedComponent.capabilities['key'] = [<Capability>{ownerName: "VF Test"}]; + component.valueEditModalInstance = valueEditModalInstance; + component.valueEditModalInstance.innerModalContent.instance.name = newName; + jest.spyOn(component.componentInstanceService, 'updateComponentInstance').mockReturnValue(of(<ComponentInstance>{name: newName})); + component.saveInstanceName(); + + expect(component.selectedComponent.name).toBe(newName); + expect(component.selectedComponent.requirements['key'][0].ownerName).toEqual(newName); + expect(component.selectedComponent.requirements['key'][1].ownerName).toEqual(newName); + expect(component.selectedComponent.capabilities['key'][0].ownerName).toEqual(newName); + }); + + it('if update fails, name is reverted to old value', () => { + component.selectedComponent = new GroupInstance(<GroupInstance>{name: "GROUP NAME"}); + component.valueEditModalInstance = valueEditModalInstance; + jest.spyOn(component.groupService, 'updateName').mockReturnValue(Observable.throw(new Error('Error'))); + component.saveInstanceName(); + expect(component.selectedComponent.name).toEqual("GROUP NAME"); + }); + + it('policy instance uses policies service for update name', () => { + component.selectedComponent = new PolicyInstance(<PolicyInstance>{name: "Policy OLD NAME"}); + component.valueEditModalInstance = valueEditModalInstance; + jest.spyOn(component.policiesService, 'updateName').mockReturnValue(of(true)); + component.saveInstanceName(); + expect(component.policiesService.updateName).toHaveBeenCalledTimes(1); + }); + + it('group instance uses groups service for update name', () => { + component.selectedComponent = new GroupInstance(<GroupInstance>{name: "GROUP NAME"}); + component.valueEditModalInstance = valueEditModalInstance; + jest.spyOn(component.groupService, 'updateName').mockReturnValue(of(true)); + component.saveInstanceName(); + expect(component.groupService.updateName).toHaveBeenCalledTimes(1); + }); + +}); @@ -18,64 +18,70 @@ * ============LICENSE_END========================================================= */ -import { Component, Input, AfterViewInit, SimpleChanges, OnInit, OnChanges } from "@angular/core"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -import { IModalConfig } from 'sdc-ui/lib/angular/modals/models/modal-config'; -import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance'; -import { ValueEditComponent } from './../../../../components/ui/forms/value-edit/value-edit.component'; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { PoliciesService } from '../../../../services/policies.service'; -import { GroupsService } from '../../../../services/groups.service'; -import {IZoneService} from "../../../../../models/graph/zones/zone"; -import { EventListenerService, LoaderService } from "../../../../../services"; -import { GRAPH_EVENTS, EVENTS } from "../../../../../utils"; +import { Component, Input, OnInit } from "@angular/core"; +import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular"; +import { EditNameModalComponent } from "app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component"; +import {Component as TopologyTemplate, FullComponentInstance, GroupInstance, PolicyInstance, Requirement, Capability, ComponentInstance} from "app/models"; +import { Select } from "@ngxs/store"; +import { Observable } from "rxjs/Observable"; +import { Subscription } from "rxjs"; +import {GRAPH_EVENTS} from "../../../../../utils/constants"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import {EventListenerService} from "../../../../../services/event-listener-service"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { GroupsService, PoliciesService } from "app/services-ng2"; import { UIZoneInstanceObject } from "../../../../../models/ui-models/ui-zone-instance-object"; -import { ModalButtonComponent } from "sdc-ui/lib/angular/components"; +import {SelectedComponentType} from "../../common/store/graph.actions"; +import * as _ from 'lodash'; +import {GraphState} from "../../common/store/graph.state"; + @Component({ selector: 'ng2-composition-panel-header', templateUrl: './panel-header.component.html', styleUrls: ['./panel-header.component.less'] }) -export class CompositionPanelHeaderComponent implements OnInit, OnChanges { - - @Input() topologyTemplate: TopologyTemplate; - @Input() selectedZoneInstanceType: ZoneInstanceType; - @Input() selectedZoneInstanceId: string; - @Input() name: string; - @Input() nonCertified: boolean; +export class CompositionPanelHeaderComponent implements OnInit { @Input() isViewOnly: boolean; - @Input() isLoading: boolean; + @Input() selectedComponent: FullComponentInstance | TopologyTemplate | GroupInstance | PolicyInstance; + @Select(GraphState.getSelectedComponentType) selectedComponentType$:Observable<SelectedComponentType>; + - constructor(private groupsService:GroupsService, private policiesService: PoliciesService, - private modalService:SdcUiComponents.ModalService, private eventListenerService:EventListenerService) { } + constructor(private modalService: SdcUiServices.ModalService, + private groupService: GroupsService, + private policiesService: PoliciesService, + private eventListenerService: EventListenerService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private componentInstanceService: ComponentInstanceServiceNg2) { } - private service:IZoneService; private iconClassName: string; + private valueEditModalInstance: SdcUiComponents.ModalComponent; + private isTopologyTemplateSelected: boolean; + private componentTypeSubscription: Subscription; ngOnInit(): void { - this.init(); - } + this.componentTypeSubscription = this.selectedComponentType$.subscribe((newComponentType) => { - ngOnChanges (changes:SimpleChanges):void { - if(changes.selectedZoneInstanceId){ - this.init(); - } + this.initClasses(newComponentType); + this.isTopologyTemplateSelected = (newComponentType === SelectedComponentType.TOPOLOGY_TEMPLATE) ? true : false; + }); } ngOnDestroy() { - - + if(this.componentTypeSubscription) { + this.componentTypeSubscription.unsubscribe(); + } } - private init = (): void => { - if (this.selectedZoneInstanceType === ZoneInstanceType.POLICY) { + + private initClasses = (componentType:SelectedComponentType): void => { + if (componentType === SelectedComponentType.POLICY) { this.iconClassName = "sprite-policy-icons policy"; - this.service = this.policiesService; - } else if (this.selectedZoneInstanceType === ZoneInstanceType.GROUP) { + } else if (componentType === SelectedComponentType.GROUP) { this.iconClassName = "sprite-group-icons group"; - this.service = this.groupsService; } else { - this.iconClassName = "sprite-resource-icons defaulticon"; + this.iconClassName = undefined; } } @@ -83,53 +89,95 @@ export class CompositionPanelHeaderComponent implements OnInit, OnChanges { const modalConfig = { title: "Edit Name", size: "sm", - type: "custom", + type: SdcUiCommon.ModalType.custom, testId: "renameInstanceModal", buttons: [ {id: 'saveButton', text: 'OK', size: 'xsm', callback: this.saveInstanceName, closeModal: false}, - {id: 'cancelButton', text: 'Cancel', size: 'sm', closeModal: true} - ] as ModalButtonComponent[] - } as IModalConfig; - this.modalService.openCustomModal(modalConfig, ValueEditComponent, {name: this.name, validityChangedCallback: this.enableOrDisableSaveButton}); + {id: 'cancelButton', text: 'Cancel', size: 'sm', closeModal: true} + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig; + this.valueEditModalInstance = this.modalService.openCustomModal(modalConfig, EditNameModalComponent, {name: this.selectedComponent.name, validityChangedCallback: this.enableOrDisableSaveButton}); }; private enableOrDisableSaveButton = (shouldEnable: boolean): void => { - let saveButton: ModalButtonComponent = this.modalService.getCurrentInstance().getButtonById('saveButton'); + let saveButton: SdcUiComponents.ModalButtonComponent = this.valueEditModalInstance.getButtonById('saveButton'); saveButton.disabled = !shouldEnable; } private saveInstanceName = ():void => { - let currentModal = this.modalService.getCurrentInstance(); - let nameFromModal:string = currentModal.innerModalContent.instance.name; - - if(nameFromModal != this.name){ - currentModal.buttons[0].disabled = true; - this.service.updateName(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId, nameFromModal).subscribe((success)=>{ - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_NAME_CHANGED, nameFromModal); - this.modalService.closeModal(); - }, (error)=> { - currentModal.buttons[0].disabled = false; - }); - } else { - this.modalService.closeModal(); + let nameFromModal:string = this.valueEditModalInstance.innerModalContent.instance.name; + + if(nameFromModal != this.selectedComponent.name){ + let oldName = this.selectedComponent.name; + this.selectedComponent.name = nameFromModal; + this.valueEditModalInstance.buttons[0].disabled = true; + + let onFailed = (error) => { + this.selectedComponent.name = oldName; + this.valueEditModalInstance.buttons[0].disabled = false; + }; + + if(this.selectedComponent instanceof FullComponentInstance){ + let onSuccess = (componentInstance:ComponentInstance) => { + //update requirements and capabilities owner name + _.forEach((<FullComponentInstance>this.selectedComponent).requirements, (requirementsArray:Array<Requirement>) => { + _.forEach(requirementsArray, (requirement:Requirement):void => { + requirement.ownerName = componentInstance.name; + }); + }); + + _.forEach((<FullComponentInstance>this.selectedComponent).capabilities, (capabilitiesArray:Array<Capability>) => { + _.forEach(capabilitiesArray, (capability:Capability):void => { + capability.ownerName = componentInstance.name; + }); + }); + this.valueEditModalInstance.closeModal(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, this.selectedComponent); + }; + + this.componentInstanceService.updateComponentInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, new ComponentInstance(this.selectedComponent)) + .subscribe(onSuccess, onFailed); + } else if (this.selectedComponent instanceof PolicyInstance) { + this.policiesService.updateName(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId, nameFromModal).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.selectedComponent); + this.valueEditModalInstance.closeModal(); + }, onFailed); + } else if (this.selectedComponent instanceof GroupInstance){ + this.groupService.updateName(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId, nameFromModal).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.selectedComponent); + this.valueEditModalInstance.closeModal(); + }, onFailed); + } + } else { + this.valueEditModalInstance.closeModal(); } }; - + private deleteInstance = (): void => { let title:string = "Delete Confirmation"; - let message:string = "Are you sure you would like to delete "+ this.name + "?"; - this.modalService.openAlertModal(title, message, "OK", this.deleteInstanceConfirmed, "deleteInstanceModal"); + let message:string = "Are you sure you would like to delete "+ this.selectedComponent.name + "?"; + const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.warning, callback: this.deleteInstanceConfirmed, closeModal: true} as SdcUiComponents.ModalButtonComponent; + this.modalService.openWarningModal(title, message, "delete-modal", [okButton]); }; - private deleteInstanceConfirmed = () => { - this.eventListenerService.notifyObservers(EVENTS.SHOW_LOADER_EVENT + 'composition-graph'); - this.service.deleteZoneInstance(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).finally(()=> { - this.eventListenerService.notifyObservers(EVENTS.HIDE_LOADER_EVENT + 'composition-graph'); - }).subscribe(()=> { - let deletedItem:UIZoneInstanceObject = new UIZoneInstanceObject(this.selectedZoneInstanceId, this.selectedZoneInstanceType, this.name); - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, deletedItem); - }); - }; + private deleteInstanceConfirmed: Function = () => { + if(this.selectedComponent instanceof FullComponentInstance){ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE , this.selectedComponent.uniqueId); + } + else if(this.selectedComponent instanceof PolicyInstance){ + this.policiesService.deletePolicy(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE , + new UIZoneInstanceObject(this.selectedComponent.uniqueId, 1)); + }, (err) => {}); + + } + else if(this.selectedComponent instanceof GroupInstance){ + this.groupService.deleteGroup(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE , + new UIZoneInstanceObject(this.selectedComponent.uniqueId, 0)); + }, (err) => {}); + } + }; } @@ -18,29 +18,26 @@ * ============LICENSE_END========================================================= */ import { NgModule } from "@angular/core"; -import { HttpModule } from "@angular/http"; import { FormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; import { CompositionPanelHeaderComponent } from "./panel-header.component"; import { UiElementsModule } from './../../../../components/ui/ui-elements.module'; -import { ValueEditComponent } from './../../../../components/ui/forms/value-edit/value-edit.component'; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { ModalFormsModule } from "app/ng2/components/ui/forms/modal-forms.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { EditNameModalComponent } from "app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component"; @NgModule({ declarations: [ - CompositionPanelHeaderComponent + CompositionPanelHeaderComponent, + EditNameModalComponent ], imports: [ BrowserModule, FormsModule, - HttpModule, UiElementsModule, - SdcUiComponentsModule, - ModalFormsModule + SdcUiComponentsModule ], entryComponents: [ - CompositionPanelHeaderComponent, ValueEditComponent + CompositionPanelHeaderComponent, EditNameModalComponent ], exports: [ CompositionPanelHeaderComponent @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`artifact-tab component should match current snapshot of artifact-tab component 1`] = ` +<artifacts-tab + addOrUpdate={[Function Function]} + allowDeleteAndUpdateArtifact={[Function Function]} + artifactService={[Function Object]} + componentInstanceService="undefined" + compositionService={[Function Object]} + delete={[Function Function]} + getEnvArtifact={[Function Function]} + getTitle={[Function Function]} + heatToEnv={[Function Map]} + isLicenseArtifact={[Function Function]} + loadArtifacts={[Function Function]} + store={[Function Store]} + topologyTemplateService="undefined" + updateEnvParams={[Function Function]} + viewEnvParams={[Function Function]} + workspaceService="undefined" +> + <div + class="w-sdc-designer-sidebar-tab-content artifacts" + > + <div + class="w-sdc-designer-sidebar-section" + > + <ng2-expand-collapse + state="0" + > + <header + sdc-tooltip="" + > + + </header> + <content + class="artifacts-container" + > + <div + class="w-sdc-designer-sidebar-section-content" + > + + </div> + + </content> + </ng2-expand-collapse> + </div> + </div> +</artifacts-tab> +`; @@ -0,0 +1,303 @@ +import { async, ComponentFixture } from '@angular/core/testing'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { NgxsModule, Store } from '@ngxs/store'; +import { WorkspaceState } from '../../../../../store/states/workspace.state'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ArtifactsTabComponent } from './artifacts-tab.component'; +import { CompositionService } from '../../../composition.service'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { ComponentInstanceServiceNg2 } from '../../../../../services/component-instance-services/component-instance.service'; +import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service'; +import { ArtifactsService } from '../../../../../components/forms/artifacts-form/artifacts.service'; +import { ArtifactModel } from '../../../../../../models/artifacts'; +import { ArtifactType } from '../../../../../../utils/constants'; +import { FullComponentInstance } from '../../../../../../models/componentsInstances/fullComponentInstance'; +import { ComponentInstance } from '../../../../../../models/componentsInstances/componentInstance'; +import { Component } from '../../../../../../models/components/component'; +import { GetInstanceArtifactsByTypeAction } from '../../../../../store/actions/instance-artifacts.actions'; +import { Observable } from 'rxjs'; + + +describe('artifact-tab component', () => { + + let fixture: ComponentFixture<ArtifactsTabComponent>; + let compositionMockService: Partial<CompositionService>; + const workspaceMockService: Partial<WorkspaceService>; + const componentInstanceMockService: Partial<ComponentInstanceServiceNg2>; + const topologyTemplateMockService: Partial<TopologyTemplateService>; + let artifactsServiceMockService: Partial<ArtifactsService>; + let store: Store; + + beforeEach( + async(() => { + compositionMockService = { + updateInstance: jest.fn() + } + + artifactsServiceMockService = { + deleteArtifact: jest.fn(), + openUpdateEnvParams: jest.fn(), + openArtifactModal: jest.fn() + } + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [ArtifactsTabComponent], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: CompositionService, useValue: compositionMockService}, + {provide: WorkspaceService, useValue: workspaceMockService}, + {provide: ComponentInstanceServiceNg2, useValue: componentInstanceMockService}, + {provide: TopologyTemplateService, useValue: topologyTemplateMockService}, + {provide: ArtifactsService, useValue: artifactsServiceMockService} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(ArtifactsTabComponent); + store = testBed.get(Store); + }); + }) + ); + + it ('on delete -> deleteArtifact is being called from artifactService', () => { + const artifact = new ArtifactModel(); + const topologyTemplateType: string = undefined; + const topologyTemplateId: string = undefined; + + fixture.componentInstance.delete(artifact); + expect(artifactsServiceMockService.deleteArtifact).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, artifact); + }); + + it('should match current snapshot of artifact-tab component', () => { + expect(fixture).toMatchSnapshot(); + }); + + + it ('should get API Artifacts as Title', () => { + const artifactType = ArtifactType.SERVICE_API; + + const res = fixture.componentInstance.getTitle(artifactType); + expect(res).toBe('API Artifacts'); + }); + + + it ('should get Deployment Artifacts as Title', () => { + const artifactType = ArtifactType.DEPLOYMENT; + + const res = fixture.componentInstance.getTitle(artifactType); + expect(res).toBe('Deployment Artifacts'); + }); + + it ('should get Informational Artifacts as Title', () => { + const artifactType = ArtifactType.INFORMATION; + + const res = fixture.componentInstance.getTitle(artifactType); + expect(res).toBe('Informational Artifacts'); + }); + + it ('should get SomeString as Title - This is the default case (return the last val)', () => { + // So the last value will be "SomeString" + fixture.componentInstance.getTitle('SomeString'); + + const res = fixture.componentInstance.getTitle('SomeString'); + expect(res).toBe('SomeString Artifacts'); + }); + + + it ('should return isLicenseArtifact false', () => { + const artifact = new ArtifactModel(); + const componentInstance = new ComponentInstance(); + const component = new Component(); + fixture.componentInstance.component = new FullComponentInstance(componentInstance, component); + + let res = fixture.componentInstance.isLicenseArtifact(artifact); + expect(res).toBe(false); + }); + + it ('should return isLicenseArtifact true', () => { + const artifact = new ArtifactModel(); + const componentInstance = new ComponentInstance(); + const component = new Component(); + fixture.componentInstance.component = new FullComponentInstance(componentInstance, component); + fixture.componentInstance.component.isResource = jest.fn(() => true); + fixture.componentInstance.component.isCsarComponent = true; + + artifact.artifactType = ArtifactType.VENDOR_LICENSE; + const res = fixture.componentInstance.isLicenseArtifact(artifact); + expect(res).toBe(true); + }); + + it ('should verify getEnvArtifact with match', () => { + const artifact = new ArtifactModel(); + artifact.uniqueId = 'matchUniqueID'; + + const testItem1 = new ArtifactModel(); + testItem1.generatedFromId = 'matchUniqueID'; + + const testItem2 = new ArtifactModel(); + testItem2.generatedFromId = '123456'; + + const artifacts: ArtifactModel[] = [testItem1, testItem2]; + + const res = fixture.componentInstance.getEnvArtifact(artifact, artifacts); + expect(res.generatedFromId).toBe('matchUniqueID'); + }); + + it ('should verify getEnvArtifact with no match', () => { + const artifact = new ArtifactModel(); + artifact.uniqueId = 'matchUniqueID'; + + const testItem1 = new ArtifactModel(); + testItem1.generatedFromId = '654321'; + + const testItem2 = new ArtifactModel(); + testItem2.generatedFromId = '123456'; + + const artifacts: ArtifactModel[] = [testItem1, testItem2]; + + const res = fixture.componentInstance.getEnvArtifact(artifact, artifacts); + expect(res).toBe(undefined); + }); + + it ('on updateEnvParams -> openUpdateEnvParams is being called from artifactService when isComponentInstanceSelected = true', () => { + const artifact = new ArtifactModel(); + artifact.envArtifact = new ArtifactModel(); + + const topologyTemplateType: string = undefined; + const topologyTemplateId: string = undefined; + + const component = new Component(); + component.uniqueId = 'id'; + + const isComponentInstanceSelected = true; + + fixture.componentInstance.component = component; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.updateEnvParams(artifact); + + expect(artifactsServiceMockService.openUpdateEnvParams).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, undefined, component.uniqueId); + }); + + it ('on updateEnvParams -> openUpdateEnvParams is being called from artifactService when isComponentInstanceSelected = false', () => { + const artifact = new ArtifactModel(); + + const topologyTemplateType: string = undefined + const topologyTemplateId: string = undefined; + + const component = new Component(); + + const isComponentInstanceSelected = false; + + fixture.componentInstance.component = component; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.updateEnvParams(artifact); + + expect(artifactsServiceMockService.openUpdateEnvParams).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, artifact); + }); + + it ('on addOrUpdate -> openArtifactModal is being called from artifactService when isComponentInstanceSelected = true', () => { + const artifact = new ArtifactModel(); + + const topologyTemplateType: string = 'testType'; + const topologyTemplateId: string = 'testID'; + const type: string = 'testType'; + const isViewOnly: boolean = false; + + const component = new Component(); + component.uniqueId = 'id'; + + const isComponentInstanceSelected = true; + + fixture.componentInstance.component = component; + fixture.componentInstance.type = type; + fixture.componentInstance.topologyTemplateId = topologyTemplateId; + fixture.componentInstance.topologyTemplateType = topologyTemplateType; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.isViewOnly = isViewOnly; + fixture.componentInstance.addOrUpdate(artifact); + + + expect(artifactsServiceMockService.openArtifactModal).toHaveBeenCalledWith(topologyTemplateId, topologyTemplateType, artifact, type, isViewOnly, component.uniqueId); + }); + + it ('on addOrUpdate -> openArtifactModal is being called from artifactService when isComponentInstanceSelected = false', () => { + const artifact = new ArtifactModel(); + + const topologyTemplateType: string = 'testType'; + const topologyTemplateId: string = 'testID'; + const type: string = 'testType'; + const isViewOnly: boolean = false; + + const isComponentInstanceSelected = false; + + fixture.componentInstance.type = type; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.topologyTemplateId = topologyTemplateId; + fixture.componentInstance.topologyTemplateType = topologyTemplateType; + fixture.componentInstance.isViewOnly = isViewOnly; + fixture.componentInstance.addOrUpdate(artifact); + + expect(artifactsServiceMockService.openArtifactModal).toHaveBeenCalledWith(topologyTemplateId, topologyTemplateType, artifact, type, isViewOnly); + }); + + + it ('verify allowDeleteAndUpdateArtifact return false since isViewOnly=true', () => { + const artifact = new ArtifactModel(); + fixture.componentInstance.isViewOnly = true; + + const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact); + expect(res).toBe(false) + }); + + it ('verify allowDeleteAndUpdateArtifact return artifact.isFromCsar since isViewOnly=false && artifactGroupType = DEPLOYMENT', () => { + const artifact = new ArtifactModel(); + artifact.artifactGroupType = ArtifactType.DEPLOYMENT; + artifact.isFromCsar = false; + + fixture.componentInstance.isViewOnly = false; + + const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact); + expect(res).toBe(!artifact.isFromCsar); + }); + + it ('verify allowDeleteAndUpdateArtifact return !artifact.isHEAT() && !artifact.isThirdParty() &&' + + ' !this.isLicenseArtifact(artifact) since isViewOnly=false && artifactGroupType != DEPLOYMENT', () => { + const artifact = new ArtifactModel(); + artifact.artifactGroupType = 'NOT_DEPLOYMENT'; + artifact.isHEAT = () => false; + artifact.isThirdParty = () => false; + + fixture.componentInstance.isLicenseArtifact = jest.fn(() => false); + + fixture.componentInstance.isViewOnly = false; + + const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact); + expect(res).toBe(true ) + }); + + it('verify action on loadArtifacts in case isComponentInstanceSelected = true', () => { + fixture.componentInstance.isComponentInstanceSelected = true; + fixture.componentInstance.topologyTemplateType = 'topologyTemplateType'; + fixture.componentInstance.topologyTemplateId = 'topologyTemplateId'; + const component = new Component(); + component.uniqueId = 'uniqueId'; + fixture.componentInstance.component = component; + fixture.componentInstance.type = 'type'; + + const action = new GetInstanceArtifactsByTypeAction(({ + componentType: 'topologyTemplateType', + componentId: 'topologyTemplateId', + instanceId: 'uniqueId', + artifactType: 'type' + })) + + fixture.componentInstance.store.dispatch = jest.fn(() => Observable.of(true)); + fixture.componentInstance.loadArtifacts(); + + expect(store.dispatch).toBeCalledWith(action); + + }); +}); @@ -0,0 +1,119 @@ +<div class="w-sdc-designer-sidebar-tab-content artifacts"> + <div class="w-sdc-designer-sidebar-section"> + <ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{title}}">{{title}}</header> + <content class="artifacts-container"> + <div class="w-sdc-designer-sidebar-section-content"> + <div class="i-sdc-designer-sidebar-section-content-item" *ngFor="let artifact of artifacts$ | async"> + <div class="i-sdc-designer-sidebar-section-content-item-artifact" + *ngIf="(!isComponentInstanceSelected || artifact.esId) && 'HEAT_ENV' !== artifact.artifactType" + attr.data-tests-id="'artifact-item-' + artifact.artifactDisplayName"> + <span *ngIf="artifact.heatParameters?.length" + class="i-sdc-designer-sidebar-section-content-item-file-link"></span> + <div class="i-sdc-designer-sidebar-section-content-item-artifact-details" + [class.heat]="artifact.isHEAT() && artifact.heatParameters?.length"> + <div *ngIf="artifact.artifactName" + class="i-sdc-designer-sidebar-section-content-item-artifact-filename" + attr.data-tests-id="artifactName-{{artifact.artifactDisplayName}}" + sdc-tooltip tooltip-text="{{artifact.artifactName}}">{{artifact.artifactName}} + </div> + <div class="artifact-buttons-container upper-buttons"> + + + <svg-icon + *ngIf="!isViewOnly && !artifact.isFromCsar && artifact.artifactName" + name="trash-o" clickable="true" size="medium" mode="info" + class="artifact-button" testId="delete_{{artifact.artifactDisplayName}}" + (click)="delete(artifact)"></svg-icon> + + <!--Display env parameters edit button for Instance --> + <svg-icon + *ngIf="!isViewOnly && artifact.isHEAT() && isComponentInstanceSelected && artifact.heatParameters?.length" + name="indesign_status" clickable="true" size="medium" mode="info" + class="artifact-button" + testId="edit-parameters-of-{{artifact.artifactDisplayName}}" + (click)="updateEnvParams(artifact)" + tooltip="Edit ENV Params" + ></svg-icon> + + <!--Display env parameters VIEW button for Instance --> + <svg-icon + *ngIf="isViewOnly && artifact.isHEAT() && isComponentInstanceSelected && artifact.heatParameters?.length" + name="inputs-o" clickable="true" size="medium" mode="info" + class="artifact-button" + testId="view-parameters-of-{{artifact.artifactDisplayName}}" + (click)="viewEnvParams(artifact)" + tooltip="View ENV Params" + ></svg-icon> + + <!--Display env parameters edit button for VF --> + <svg-icon + *ngIf = "!isViewOnly && !isComponentInstanceSelected && artifact.heatParameters?.length" + name="indesign_status" clickable="true" size="medium" mode="info" + class="artifact-button" + testId="edit-parameters-of-{{artifact.artifactDisplayName}}" + (click)="updateEnvParams(artifact)"></svg-icon> + + + <download-artifact *ngIf="artifact.esId && 'deployment' != type" + class="artifact-button" + [artifact]="artifact" [componentType]="component.componentType" + [componentId]="component.uniqueId" + testId="download_{{artifact.artifactDisplayName}}" + [isInstance]="isComponentInstanceSelected"></download-artifact> + <download-artifact *ngIf="artifact.esId && 'deployment' == type" + class="artifact-button" + [artifact]="artifact" [componentType]="component.componentType" + [componentId]="component.uniqueId" + [isInstance]="isComponentInstanceSelected" + testId="download_{{artifact.artifactDisplayName}}" + [showLoader]="artifact.isHEAT()"></download-artifact> + + <button *ngIf="!isViewOnly && !artifact.esId && type==='deployment' && !isComponentInstanceSelected && !artifact.isThirdParty()" + class="artifact-button attach sprite e-sdc-small-icon-upload" + (click)="addOrUpdate(artifact)" type="button" + attr.data-tests-id="add_Artifact"></button> + </div> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-name" + attr.data-tests-id="artifact_Display_Name-{{artifact.artifactDisplayName}}" + [ngClass]="{'hand enabled': artifact.allowDeleteAndUpdate}" + (click)="artifact.allowDeleteAndUpdate && addOrUpdate(artifact)" + sdc-tooltip tooltip-text="{{artifact.artifactDisplayName}}">{{artifact.artifactDisplayName}}</span> + <div class="i-sdc-designer-sidebar-section-content-item-artifact-heat-env" + *ngIf="artifact.heatParameters?.length"> + <span attr.data-tests-id="heat_env_{{artifact.artifactDisplayName}}">{{artifact.artifactDisplayName}} (ENV)</span> + <div class="artifact-buttons-container"> + <svg-icon *ngIf="!isViewOnly && envArtifactOf(artifact)" + name="edit-o" clickable="true" size="medium" + mode="info" class="artifact-button edit-pencil" + testId="edit_{{artifact.artifactDisplayName}}" + (click)="addOrUpdate(envArtifactOf(artifact))"></svg-icon> + + <download-artifact [artifact]="envArtifactOf(artifact)" + class="artifact-button" + [componentType]="component.componentType" + [componentId]="component.uniqueId" + [isInstance]="isComponentInstanceSelected" + testId="download_env_{{artifact.artifactDisplayName}}"></download-artifact> + </div> + </div> + </div> + + <div class="i-sdc-designer-sidebar-section-content-item-artifact-details-desc"> + <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-desc-label" + *ngIf="artifact.description">Description:</span>{{artifact.description}} + </div> + </div> + </div> + </div> + </div> + <div class="w-sdc-designer-sidebar-section-footer" + *ngIf="!isViewOnly && type!=='api' && (!isComponentInstanceSelected || isVfOrPnf() && (type !== 'deployment') || isComplex)"> + <sdc-button testId="add_Artifact_Button" size="large" type="primary" text="Add Artifact" + (click)="addOrUpdate({})"></sdc-button> + </div> + </content> + </ng2-expand-collapse> + </div> +</div> @@ -0,0 +1,169 @@ +@import '../../../../../../../assets/styles/override'; + + +.artifacts /deep/ .expand-collapse-content { + padding: 10px 0px; + + &.collapsed { + padding: 0 0; + } +} + +.i-sdc-designer-sidebar-section-content-item-artifact { + + &:not(:hover) .artifact-button { + display:none; + } + .artifact-buttons-container { + display: inline-flex; + flex-direction: row-reverse; + position: absolute; + right:0; + + &.upper-buttons { + margin-top: 8px; + } + + .artifact-button { + cursor:pointer; + padding-right:5px; + + &.edit-pencil { + margin-top: 10px; + } + } + } +} + +.w-sdc-designer-sidebar-section-footer { + padding: 20px; + display: flex; + justify-content: center; + +} + + +.w-sdc-designer-sidebar-tab-content.artifacts { + + .i-sdc-designer-sidebar-section-content-item-artifact.hand { + cursor: pointer; + } + + .w-sdc-designer-sidebar-section-content { + padding: 0; + } + .w-sdc-designer-sidebar-section-title { + &.expanded { + margin-bottom: 0; + } + } + + .i-sdc-designer-sidebar-section-content-item-artifact-details { + display: inline-block; + margin-left: 5px; + vertical-align: middle; + width: 180px; + &.heat { + line-height: 18px; + width: 250px; + } + } + + .i-sdc-designer-sidebar-section-content-item-artifact-details-name { + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width:220px; + display: inline-block; + //text-transform: capitalize; + &.enabled { + &:hover { + color: @sdcui_color_dark-blue; + } + } + + } + + .i-sdc-designer-sidebar-section-content-item-artifact-heat-env { + color: @sdcui_color_dark-gray; + margin-top: 6px; + line-height: 42px; + padding-top: 10px; + border-top:1px solid #c8cdd1; + .enabled { + &:hover { + cursor: pointer; + color: @sdcui_color_dark-blue; + } + } + } + + .i-sdc-designer-sidebar-section-content-item-artifact-filename { + color: @sdcui_color_dark-gray; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 225px; + display: inline-block; + font-weight: bold; + &.enabled { + &:hover { + color: @sdcui_color_dark-blue; + } + } + } + + + .i-sdc-designer-sidebar-section-content-item-file-link{ + border-left: 1px #848586 solid; + height: 58px; + margin-left: -11px; + margin-top: 11px; + border-top: 1px #848586 solid; + border-bottom: 1px #848586 solid; + width: 12px; + float: left; + } + + .i-sdc-designer-sidebar-section-content-item-artifact-details-desc { + display: none; + line-height: 16px; + word-wrap: break-word; + white-space: normal; + } + + .i-sdc-designer-sidebar-section-content-item-artifact-details-desc-label { + color: @sdcui_color_dark-gray; + } + + + .i-sdc-designer-sidebar-section-content-item-artifact { + border-bottom: 1px solid #c8cdd1; + padding: 5px 10px 5px 18px; + position: relative; + // line-height: 36px; + min-height: 61px; + //cursor: default; + display: flex; + align-items: center; + + + .i-sdc-designer-sidebar-section-content-item-button { + top: 20px; + line-height: 10px; + } + + &:hover { + //background-color: @color_c; + background-color: white; + transition: all .3s; + + .i-sdc-designer-sidebar-section-content-item-button { + display: block; + + } + + } + } +} @@ -0,0 +1,204 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { ArtifactModel, Component as TopologyTemplate, FullComponentInstance, Resource } from 'app/models'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { ResourceNamePipe } from 'app/ng2/pipes/resource-name.pipe'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ArtifactType } from 'app/utils'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; +import { ArtifactsService } from '../../../../../components/forms/artifacts-form/artifacts.service'; +import { GetArtifactsByTypeAction } from '../../../../../store/actions/artifacts.action'; +import { GetInstanceArtifactsByTypeAction } from '../../../../../store/actions/instance-artifacts.actions'; +import { ArtifactsState } from '../../../../../store/states/artifacts.state'; +import { InstanceArtifactsState } from '../../../../../store/states/instance-artifacts.state'; +import { SelectedComponentType, TogglePanelLoadingAction } from '../../../common/store/graph.actions'; +import { CompositionService } from '../../../composition.service'; + +@Component({ + selector: 'artifacts-tab', + styleUrls: ['./artifacts-tab.component.less'], + templateUrl: './artifacts-tab.component.html', + providers: [SdcUiServices.ModalService] +}) + +export class ArtifactsTabComponent { + + @Input() component: FullComponentInstance | TopologyTemplate; + @Input() isViewOnly: boolean; + @Input() input: any; + @Input() componentType: SelectedComponentType; + + public title: string; + public type: string; + public isComponentInstanceSelected: boolean; + public artifacts$: Observable<ArtifactModel[]>; + private topologyTemplateType: string; + private topologyTemplateId: string; + private heatToEnv: Map<string, ArtifactModel>; + private resourceType: string; + private isComplex: boolean; + + constructor(private store: Store, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private componentInstanceService: ComponentInstanceServiceNg2, + private topologyTemplateService: TopologyTemplateService, + private artifactService: ArtifactsService) { + this.heatToEnv = new Map(); + } + + ngOnInit() { + this.topologyTemplateType = this.workspaceService.metadata.componentType; + this.topologyTemplateId = this.workspaceService.metadata.uniqueId; + this.type = this.input.type; + this.title = this.getTitle(this.type); + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.resourceType = this.component['resourceType']; + this.isComplex = this.component.isComplex(); + this.loadArtifacts(); + } + + public addOrUpdate = (artifact: ArtifactModel): void => { + if (this.isComponentInstanceSelected) { + this.artifactService.openArtifactModal(this.topologyTemplateId, this.topologyTemplateType, artifact, this.type, this.isViewOnly, this.component.uniqueId); + } else { + this.artifactService.openArtifactModal(this.topologyTemplateId, this.topologyTemplateType, artifact, this.type, this.isViewOnly); + } + } + + public updateEnvParams = (artifact: ArtifactModel) => { + if (this.isComponentInstanceSelected) { + this.artifactService.openUpdateEnvParams(this.topologyTemplateType, this.topologyTemplateId, this.heatToEnv.get(artifact.uniqueId), this.component.uniqueId); + } else { + this.artifactService.openUpdateEnvParams(this.topologyTemplateType, this.topologyTemplateId, artifact); + } + } + + public viewEnvParams = (artifact: ArtifactModel) => { + if (this.isComponentInstanceSelected) { + this.artifactService.openViewEnvParams(this.topologyTemplateType, this.topologyTemplateId, this.heatToEnv.get(artifact.uniqueId), this.component.uniqueId); + } else { + this.artifactService.openViewEnvParams(this.topologyTemplateType, this.topologyTemplateId, artifact); + } + } + + public getEnvArtifact = (heatArtifact: ArtifactModel, artifacts: ArtifactModel[]): ArtifactModel => { + const envArtifact = _.find(artifacts, (item: ArtifactModel) => { + return item.generatedFromId === heatArtifact.uniqueId; + }); + if (envArtifact && heatArtifact) { + envArtifact.artifactDisplayName = heatArtifact.artifactDisplayName; + envArtifact.timeout = heatArtifact.timeout; + } + return envArtifact; + } + + public delete = (artifact: ArtifactModel): void => { + if (this.isComponentInstanceSelected) { + this.artifactService.deleteArtifact(this.topologyTemplateType, this.topologyTemplateId, artifact, this.component.uniqueId); + } else { + this.artifactService.deleteArtifact(this.topologyTemplateType, this.topologyTemplateId, artifact); + } + } + + public isVfOrPnf(): boolean { + if (this.component.isResource()){ + if (this.resourceType) { + return this.resourceType === 'VF' || this.resourceType == 'PNF'; + } + return false; + } + + return false; + } + + private envArtifactOf(artifact: ArtifactModel): ArtifactModel { + return this.heatToEnv.get(artifact.uniqueId); + } + + private isLicenseArtifact = (artifact: ArtifactModel): boolean => { + let isLicense: boolean = false; + if (this.component.isResource && (this.component as Resource).isCsarComponent) { + if (ArtifactType.VENDOR_LICENSE === artifact.artifactType || ArtifactType.VF_LICENSE === artifact.artifactType) { + isLicense = true; + } + } + + return isLicense; + } + + private getTitle = (artifactType: string): string => { + switch (artifactType) { + case ArtifactType.SERVICE_API: + return 'API Artifacts'; + case ArtifactType.DEPLOYMENT: + return 'Deployment Artifacts'; + case ArtifactType.INFORMATION: + return 'Informational Artifacts'; + default: + return ResourceNamePipe.getDisplayName(artifactType) + ' Artifacts'; + } + } + + private loadArtifacts = (forceLoad?: boolean): void => { + + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + let action; + if (this.isComponentInstanceSelected) { + action = new GetInstanceArtifactsByTypeAction(({ + componentType: this.topologyTemplateType, + componentId: this.topologyTemplateId, + instanceId: this.component.uniqueId, + artifactType: this.type + })); + } else { + action = new GetArtifactsByTypeAction({ + componentType: this.topologyTemplateType, + componentId: this.topologyTemplateId, + artifactType: this.type + }); + } + this.store.dispatch(action).subscribe(() => { + const stateSelector = this.isComponentInstanceSelected ? InstanceArtifactsState.getArtifactsByType : ArtifactsState.getArtifactsByType; + this.artifacts$ = this.store.select(stateSelector).pipe(map((filterFn) => filterFn(this.type))).pipe(map((artifacts) => { + _.forEach(artifacts, (artifact: ArtifactModel): void => { + const envArtifact = this.getEnvArtifact(artifact, artifacts); // Extract the env artifact (if exist) of the HEAT artifact + if (envArtifact) { + // Set a mapping between HEAT to HEAT_ENV + this.heatToEnv.set(artifact.uniqueId, envArtifact); + } + }); + return _.orderBy(artifacts, ['mandatory', 'artifactDisplayName'], ['desc', 'asc']); + })); + + this.artifacts$.subscribe((artifacts) => { + _.forEach(artifacts, (artifact: ArtifactModel) => { + artifact.allowDeleteAndUpdate = this.allowDeleteAndUpdateArtifact(artifact); + }); + if (this.component instanceof FullComponentInstance) { + this.compositionService.updateInstance(this.component); + } + }); + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }, () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }); + } + + private allowDeleteAndUpdateArtifact = (artifact: ArtifactModel): boolean => { + if (!this.isViewOnly) { + if (artifact.artifactGroupType === ArtifactType.DEPLOYMENT) { + return !artifact.isFromCsar; + } else { + + return (!artifact.isHEAT() && !artifact.isThirdParty() && !this.isLicenseArtifact(artifact)); + } + } + return false; + } +} @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<div class="w-sdc-designer-sidebar-section-title" tooltip="Members">Members +<h1 class="w-sdc-designer-sidebar-section-title" tooltip="Members">Members <svg-icon-label *ngIf="!isViewOnly" class="add-members-btn" name="plus-circle-o" @@ -24,7 +24,7 @@ labelPlacement="right" (click)="openAddMembersModal()"> </svg-icon-label> -</div> +</h1> <div class="expand-collapse-content"> <ul> <li *ngFor="let member of members; let i = index" class="component-details-panel-large-item" @@ -40,7 +40,7 @@ </li> </ul> - <div *ngIf="members.length===0" class="component-details-panel-tab-no-data"> + <div *ngIf="!members || members.length===0" class="component-details-panel-tab-no-data"> <div class="component-details-panel-tab-no-data-title">No data to display yet</div> <div class="component-details-panel-tab-no-data-content">Add members to group to see members</div> </div> @@ -0,0 +1,127 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Rx'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { GroupInstance } from '../../../../../../models/graph/zones/group-instance'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { GroupsService } from '../../../../../services/groups.service'; +import { TranslateService } from '../../../../../shared/translator/translate.service'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { CompositionService } from '../../../composition.service'; +import { GroupMembersTabComponent } from './group-members-tab.component'; + +describe('group members tab component', () => { + + let fixture: ComponentFixture<GroupMembersTabComponent>; + + // Mocks + let workspaceServiceMock: Partial<WorkspaceService>; + let eventsListenerServiceMock: Partial<EventListenerService>; + let groupServiceMock: Partial<GroupsService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let compositionServiceMock: Partial<CompositionService>; + let modalServiceMock: Partial<SdcUiServices.ModalService>; + + const membersToAdd = [ + {uniqueId: '1', name: 'inst1'}, + {uniqueId: '2', name: 'inst2'}, + ]; + + beforeEach( + async(() => { + + eventsListenerServiceMock = {}; + + groupServiceMock = Mock.of<GroupsService>( + { + updateMembers: jest.fn().mockImplementation((compType, uid, groupUniqueId, updatedMembers) => { + if (updatedMembers === undefined) { + return Observable.throwError('error'); + } else { + return Observable.of(updatedMembers); + } + } + )}); + + compositionServiceMock = { + getComponentInstances: jest.fn().mockImplementation( () => { + return [{uniqueId: '1', name: 'inst1'}, + {uniqueId: '2', name: 'inst2'}, + {uniqueId: '3', name: 'inst3'}, + {uniqueId: '4', name: 'inst4'}, + {uniqueId: '5', name: 'inst5'} + ]; + } + ) + }; + + workspaceServiceMock = { + metadata: Mock.of<ComponentMetadata>() + }; + + const addMemberModalInstance = { + innerModalContent: { instance: { existingElements: membersToAdd }}, + closeModal: jest.fn() + }; + + modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => addMemberModalInstance) + }; + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const groupInstanceMock = Mock.of<GroupInstance>(); + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [GroupMembersTabComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: GroupsService, useValue: groupServiceMock}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: EventListenerService, useValue: eventsListenerServiceMock }, + {provide: CompositionService, useValue: compositionServiceMock }, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(GroupMembersTabComponent); + fixture.componentInstance.group = groupInstanceMock; + }); + }) + ); + + it('test that initially all members are available for adding', () => { + const testedComponent = fixture.componentInstance; + + // No members are currently in the group, all 5 members should be returned + const optionalMembersToAdd = testedComponent.getOptionalsMembersToAdd(); + expect(optionalMembersToAdd).toHaveLength(5); + }); + + it('test list of available instances to add does not include existing members', () => { + const testedComponent = fixture.componentInstance; + + // Mock the group instance to return the members that we are about to add + testedComponent.group.getMembersAsUiObject = jest.fn().mockImplementation( () => membersToAdd); + + // The opened modal shall return 2 members to be added + testedComponent.openAddMembersModal(); + testedComponent.addMembers(); // Shall add 2 members (1,2) + + // Now the getOptionalsMembersToAdd shall return 3 which are the members that were no added yet + const optionalMembersToAdd = testedComponent.getOptionalsMembersToAdd(); + expect(optionalMembersToAdd).toHaveLength(3); + }); +}); @@ -0,0 +1,158 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { GroupInstance } from 'app/models/graph/zones/group-instance'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { EventListenerService } from 'app/services/event-listener-service'; +import { GRAPH_EVENTS } from 'app/utils'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable, Subscription } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { ComponentInstance } from '../../../../../../models/componentsInstances/componentInstance'; +import { MemberUiObject } from '../../../../../../models/ui-models/ui-member-object'; +import { AddElementsComponent } from '../../../../../components/ui/modal/add-elements/add-elements.component'; +import {GraphState} from "../../../common/store/graph.state"; +import { GroupsService } from '../../../../../services/groups.service'; +import { TranslateService } from '../../../../../shared/translator/translate.service'; + +@Component({ + selector: 'group-members-tab', + templateUrl: './group-members-tab.component.html', + styleUrls: ['./../policy-targets-tab/policy-targets-tab.component.less'] +}) + +export class GroupMembersTabComponent implements OnInit, OnDestroy { + + @Input() group: GroupInstance; + @Input() isViewOnly: boolean; + @Select(GraphState.getSelectedComponent) group$: Observable<GroupInstance>; + @HostBinding('class') classes = 'component-details-panel-tab-group-members'; + + private members: MemberUiObject[]; + private addMemberModalInstance: SdcUiComponents.ModalComponent; + private subscription: Subscription; + + constructor( + private translateService: TranslateService, + private groupsService: GroupsService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService + ) { + } + + ngOnInit() { + this.subscription = this.group$.pipe( + tap((group) => { + this.group = group; + this.members = this.group.getMembersAsUiObject(this.compositionService.componentInstances); + })).subscribe(); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + deleteMember = (member: MemberUiObject): void => { + this.loaderService.activate(); + this.groupsService.deleteGroupMember( + this.workspaceService.metadata.componentType, + this.workspaceService.metadata.uniqueId, + this.group, + member.uniqueId).subscribe( + (updatedMembers: string[]) => { + this.group.members = updatedMembers; + this.initMembers(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); + }, + () => console.log('Error deleting member!'), + () => this.loaderService.deactivate() + ); + } + + addMembers = (): void => { + // TODO refactor sdc-ui modal in order to return the data + const membersToAdd: MemberUiObject[] = this.addMemberModalInstance.innerModalContent.instance.existingElements; + if (membersToAdd.length > 0) { + this.addMemberModalInstance.closeModal(); + this.loaderService.activate(); + const locallyUpdatedMembers: MemberUiObject[] = _.union(this.members, membersToAdd); + this.groupsService.updateMembers( + this.workspaceService.metadata.componentType, + this.workspaceService.metadata.uniqueId, + this.group.uniqueId, + locallyUpdatedMembers).subscribe( + (updatedMembers: string[]) => { + this.group.members = updatedMembers; + this.initMembers(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); + }, + () => { + console.log('Error updating members!'); + }, () => + this.loaderService.deactivate() + ); + } + } + + getOptionalsMembersToAdd(): MemberUiObject[] { + const optionalsMembersToAdd: MemberUiObject[] = []; + // adding all instances as optional members to add if not already exist + _.forEach(this.compositionService.getComponentInstances(), (instance: ComponentInstance) => { + if (!_.some(this.members, (member: MemberUiObject) => { + return member.uniqueId === instance.uniqueId; + })) { + optionalsMembersToAdd.push(new MemberUiObject(instance.uniqueId, instance.name)); + } + }); + return optionalsMembersToAdd; + } + + openAddMembersModal(): void { + const addMembersModalConfig = { + title: this.group.name + ' ADD MEMBERS', + size: 'md', + type: SdcUiCommon.ModalType.custom, + testId: 'addMembersModal', + buttons: [ + {text: 'ADD MEMBERS', size: 'medium', callback: this.addMembers, closeModal: false}, + {text: 'CANCEL', size: 'sm', type: 'secondary', closeModal: true} + ] + } as SdcUiCommon.IModalConfig; + const optionalsMembersToAdd = this.getOptionalsMembersToAdd(); + this.addMemberModalInstance = this.modalService.openCustomModal(addMembersModalConfig, AddElementsComponent, { + elementsToAdd: optionalsMembersToAdd, + elementName: 'member' + }); + } + + private initMembers = (groupInstance?: GroupInstance) => { + this.group = groupInstance ? groupInstance : this.group; + this.members = this.group.getMembersAsUiObject(this.compositionService.getComponentInstances()); + } +} @@ -18,7 +18,7 @@ <header tooltip="Properties">Properties</header> <content> <ul> - <li *ngFor="let property of properties; let i = index" + <li *ngFor="let property of component.properties; let i = index" class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow"> <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label hand" [attr.data-tests-id]="'propertyName_'+property.name" @@ -32,7 +32,7 @@ </li> </ul> - <div *ngIf="properties.length===0" class="component-details-panel-tab-no-data"> + <div *ngIf="!component.properties || component.properties.length===0" class="component-details-panel-tab-no-data"> <div class="component-details-panel-tab-no-data-title">No properties to display</div> </div> </content> @@ -19,44 +19,32 @@ */ import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core"; +import { Component, Inject, Input} from "@angular/core"; import { TranslateService } from './../../../../../shared/translator/translate.service'; import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; -import { PropertyBEModel } from 'app/models'; import { PropertyModel } from './../../../../../../models/properties'; import { ModalsHandler } from "app/utils"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; +import { Component as TopologyTemplate, GroupInstance } from "app/models"; @Component({ - selector: 'policy-properties-tab', - templateUrl: './policy-properties-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'policy-properties-tab.component.less'], - host: {'class': 'component-details-panel-tab-policy-properties'} + selector: 'group-or-policy-properties-tab', + templateUrl: './group-or-policy-properties-tab.component.html', + styleUrls: ['./../properties-tab/properties-tab.component.less'], }) -export class PolicyPropertiesTabComponent implements OnChanges { +export class GroupOrPolicyPropertiesTab { - @Input() policy:PolicyInstance; + @Input() component: GroupInstance | PolicyInstance; @Input() topologyTemplate:TopologyTemplate; @Input() isViewOnly: boolean; + @Input() input: {type: string}; - private properties:Array<PropertyModel>; constructor(private translateService:TranslateService, private ModalsHandler:ModalsHandler) { } - ngOnChanges(changes: SimpleChanges): void { - console.log("PolicyPropertiesTabComponent: ngAfterViewInit: "); - console.log("policy: " + this.policy); - this.properties = []; - this.initProperties(); - } - - initProperties = ():void => { - this.properties= this.policy.properties; - } editProperty = (property?:PropertyModel):void => { - this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.properties, false, 'policy', this.policy.uniqueId).then((updatedProperty:PropertyModel) => { + this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.component.properties, false, this.input.type, this.component.uniqueId).then((updatedProperty:PropertyModel) => { console.log("ok"); }); } @@ -1,47 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - - -<ng2-expand-collapse state="0"> - - <header tooltip="General Information">General Info</header> - - <content> - <!-- CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_category" tooltip="Group">Group</span> - </div> - - <!-- SUB CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_subCategory" tooltip="Group">Group</span> - </div> - - <!-- VERSION --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span> - <span class="value" data-tests-id="rightTab_version" tooltip="{{group.version}}">{{group.version}}</span> - </div> - - <!-- DESCRIPTION --> - <div class="component-details-panel-item description"> - <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span> - <span class="value" ellipsis="group.description" max-chars="55" data-tests-id="rightTab_description">{{group.description}}</span> - </div> - </content> -</ng2-expand-collapse> @@ -1,13 +0,0 @@ -/deep/ -.component-details-panel-tab-group-members { - .component-details-panel-large-item { - display: flex; - flex-direction: row; - justify-content: space-between; - } - - .w-sdc-designer-sidebar-section-title { - display: flex; - justify-content: space-between; - } -}
\ No newline at end of file @@ -1,133 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import * as _ from "lodash"; -import { Component, Input, Output, EventEmitter, OnChanges, HostBinding } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { Component as TopologyTemplate } from "app/models"; -import { GroupInstance } from "app/models/graph/zones/group-instance"; -import { GroupsService } from "../../../../../services/groups.service"; -import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks"; -import { MemberUiObject } from "../../../../../../models/ui-models/ui-member-object"; -import { IModalConfig } from "sdc-ui/lib/angular/modals/models/modal-config"; -import { AddElementsComponent } from "../../../../../components/ui/modal/add-elements/add-elements.component"; -import { GRAPH_EVENTS } from 'app/utils'; -import { EventListenerService } from 'app/services/event-listener-service'; -import { ComponentInstance } from "../../../../../../models/componentsInstances/componentInstance"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; - -@Component({ - selector: 'group-members-tab', - templateUrl: './group-members-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'group-members-tab.component.less'] -}) - -export class GroupMembersTabComponent implements OnChanges { - - - private members: Array<MemberUiObject>; - - @Input() group: GroupInstance; - @Input() topologyTemplate: TopologyTemplate; - @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); - @HostBinding('class') classes = 'component-details-panel-tab-group-members'; - - constructor(private translateService: TranslateService, - private groupsService: GroupsService, - private modalService: SdcUiComponents.ModalService, - private eventListenerService: EventListenerService - ) { - this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.initMembers) - } - - ngOnChanges(changes:SimpleChanges):void { - this.initMembers(); - } - - deleteMember = (member: MemberUiObject):void => { - this.isLoading.emit(true); - this.groupsService.deleteGroupMember(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.group, member.uniqueId).subscribe( - (updatedMembers:Array<string>) => { - this.group.members = updatedMembers; - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); - }, - error => console.log("Error deleting member!"), - () => this.isLoading.emit(false) - ); - } - - private initMembers = (groupInstance?: GroupInstance) => { - this.group = groupInstance ? groupInstance : this.group; - this.members = this.group.getMembersAsUiObject(this.topologyTemplate.componentInstances); - } - - addMembers = ():void => { - var membersToAdd:Array<MemberUiObject> = this.modalService.getCurrentInstance().innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data - if(membersToAdd.length > 0) { - this.modalService.closeModal(); - this.isLoading.emit(true); - var updatedMembers: Array<MemberUiObject> = _.union(this.members, membersToAdd); - this.groupsService.updateMembers(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.group.uniqueId, updatedMembers).subscribe( - (updatedMembers:Array<string>) => { - this.group.members = updatedMembers; - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); - }, - error => { - console.log("Error updating members!"); - }, () => - this.isLoading.emit(false) - ); - } - } - - getOptionalsMembersToAdd():Array<MemberUiObject> { - - let optionalsMembersToAdd:Array<MemberUiObject> = []; - - // adding all instances as optional members to add if not already exist - _.forEach(this.topologyTemplate.componentInstances, (instance:ComponentInstance) => { - if (!_.some(this.members, (member:MemberUiObject) => { - return member.uniqueId === instance.uniqueId - })) { - optionalsMembersToAdd.push(new MemberUiObject(instance.uniqueId, instance.name)); - } - }); - return optionalsMembersToAdd; - } - - openAddMembersModal():void { - let addMembersModalConfig:IModalConfig = { - title: this.group.name + " ADD MEMBERS", - size: "md", - type: "custom", - testId: "addMembersModal", - buttons: [ - {text: 'ADD MEMBERS', size: 'xsm', callback: this.addMembers, closeModal: false}, - {text: 'CANCEL', size: 'sm', type: "secondary", closeModal: true} - ] - }; - var optionalsMembersToAdd = this.getOptionalsMembersToAdd(); - this.modalService.openCustomModal(addMembersModalConfig, AddElementsComponent, { - elementsToAdd: optionalsMembersToAdd, - elementName: "member" - }); - } -} @@ -1,39 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<ng2-expand-collapse state="0"> - <header tooltip="Properties">Properties</header> - <content> - <ul> - <li *ngFor="let property of properties; let i = index" - class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow"> - <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label hand" - [attr.data-tests-id]="'propertyName_'+property.name" - tooltip="{{property.name}}" - (click)="!isViewOnly && editProperty(property)">{{property.name}} - </div> - <div class="i-sdc-designer-sidebar-section-content-item-property-value" - [attr.data-tests-id]="'value_'+property.name" - tooltip="{{property.value || property.defaultValue}}">{{property.value || property.defaultValue}} - </div> - </li> - </ul> - - <div *ngIf="properties.length===0" class="component-details-panel-tab-no-data"> - <div class="component-details-panel-tab-no-data-title">No properties to display</div> - </div> - </content> -</ng2-expand-collapse> @@ -1,64 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { GroupInstance } from 'app/models/graph/zones/group-instance'; -import { PropertyBEModel } from 'app/models'; -import { PropertyModel } from './../../../../../../models/properties'; -import { ModalsHandler } from "app/utils"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; - -@Component({ - selector: 'group-properties-tab', - templateUrl: './group-properties-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'group-properties-tab.component.less'], - host: {'class': 'component-details-panel-tab-group-properties'} -}) -export class GroupPropertiesTabComponent implements OnChanges { - - @Input() group:GroupInstance; - @Input() topologyTemplate:TopologyTemplate; - @Input() isViewOnly: boolean; - - private properties:Array<PropertyModel>; - - constructor(private translateService:TranslateService, private ModalsHandler:ModalsHandler) { - } - - ngOnChanges(changes: SimpleChanges): void { - console.log("GroupPropertiesTabComponent: ngAfterViewInit: "); - console.log("group: " + JSON.stringify(this.group)); - this.properties = []; - this.initProperties(); - } - - initProperties = ():void => { - this.properties= this.group.properties; - } - - editProperty = (property?:PropertyModel):void => { - this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.properties, false, 'group', this.group.uniqueId).then((updatedProperty:PropertyModel) => { - console.log("ok"); - }); - } - -} @@ -1,67 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, SimpleChanges, OnChanges } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { GroupsService } from '../../../../../services/groups.service'; -import { GroupInstance } from "app/models/graph/zones/group-instance"; - -@Component({ - selector: 'group-tabs', - templateUrl: './group-tabs.component.html' -}) -export class GroupTabsComponent implements OnChanges { - - @Input() topologyTemplate:TopologyTemplate; - @Input() selectedZoneInstanceType:string; - @Input() selectedZoneInstanceId:string; - @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); - - private group:GroupInstance; - - constructor(private translateService:TranslateService, - private groupsService:GroupsService - ) { - } - - ngOnChanges(changes: SimpleChanges): void { - this.initGroup(); - } - - private initGroup = ():void => { - this.isLoading.emit(true); - this.groupsService.getSpecificGroup(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).subscribe( - group => { - this.group = group; - console.log(JSON.stringify(group)); - }, - error => console.log("Error getting group!"), - () => this.isLoading.emit(false) - ); - } - - private setIsLoading = (value) :void => { - this.isLoading.emit(value); - } - -} @@ -1,71 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ -import { NgModule } from "@angular/core"; -import { HttpModule } from "@angular/http"; -import { FormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; -import { ExpandCollapseComponent } from 'app/ng2/components/ui/expand-collapse/expand-collapse.component'; -import { PoliciesService } from "../../../../../services/policies.service"; -import { GroupInformationTabComponent } from './group-information-tab.component'; -import { TooltipModule } from './../../../../../components/ui/tooltip/tooltip.module'; -import { GroupTabsComponent } from "./group-tabs.component"; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { GroupMembersTabComponent } from './group-members-tab.component'; -import { TranslateModule } from './../../../../../shared/translator/translate.module'; -import { GroupPropertiesTabComponent } from "./group-properties-tab.component"; - -@NgModule({ - declarations: [ - GroupInformationTabComponent, - GroupMembersTabComponent, - GroupTabsComponent, - GroupPropertiesTabComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpModule, - TooltipModule, - UiElementsModule, - SdcUiComponentsModule, - TranslateModule - ], - entryComponents: [ - GroupInformationTabComponent, - GroupMembersTabComponent, - GroupTabsComponent, - GroupPropertiesTabComponent, - ExpandCollapseComponent - ], - exports: [ - TooltipModule, - GroupInformationTabComponent, - GroupMembersTabComponent, - GroupTabsComponent, - GroupPropertiesTabComponent - ], - providers: [ - PoliciesService - ] -}) -export class GroupTabsModule { - -} @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InfoTabComponent can load instance 1`] = ` +<panel-info-tab + componentInstanceService={[Function Object]} + compositionPaletteService={[Function Object]} + compositionService={[Function Object]} + eventListenerService={[Function Object]} + flatLeftPaletteElementsFromService={[Function Function]} + getPathNamesVersionChangeModal={[Function Function]} + initEditResourceVersion={[Function Function]} + modalService={[Function Object]} + onChangeVersion={[Function Function]} + sdcMenu={[Function Object]} + serviceService={[Function Object]} + store={[Function Object]} + versioning={[Function Function]} + workspaceService={[Function Object]} +> + <ng2-expand-collapse + state="0" + > + <header + tooltip="General Information" + > + General Info + </header> + <content + class="general-info-container" + > + + + <div + class="component-details-panel-item" + > + <span + class="name" + /> + + + </div> + + + + + + + + + + + + <div + class="component-details-panel-item description" + > + <span + class="name" + /> + <chars-ellipsis /> + </div> + + + </content> + </ng2-expand-collapse> +</panel-info-tab> +`; @@ -0,0 +1,174 @@ +<!-- + ~ Copyright (C) 2018 AT&T Intellectual Property. 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. + --> + +<ng2-expand-collapse state="0"> + <header tooltip="General Information">General Info</header> + <content class="general-info-container"> + <!-- TYPE --> + <div class="component-details-panel-item" *ngIf="component.componentType"> + <span class="name" [innerHTML]="'Type:'"></span> + <span class="value" data-tests-id="rightTab_componentType" tooltip="{{component.componentType}}">{{component.componentType}}</span> + </div> + + <!-- RESOURCE TYPE--> + <div class="component-details-panel-item" *ngIf="component.resourceType"> + <span class="name" [innerHTML]="'Resource Type:'"></span> + <span class="value" data-tests-id="rightTab_resourceType" tooltip="{{component.resourceType}}">{{component.resourceType}}</span> + </div> + + <!-- VERSION --> + <div class="component-details-panel-item" > + <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span> + <span class="value" *ngIf="!isComponentSelectedFlag" data-tests-id="rightTab_version" tooltip="{{component.version}}">{{component.version}}</span> + <ng-container *ngIf="isComponentSelectedFlag"> + <select #versionDropdown (change)="onChangeVersion(versionDropdown)" [ngModel]="component.getComponentUid()" data-tests-id="changeVersion"> + <option *ngFor="let version of versions" value="{{version.value}}" + [disabled]="isDisabledFlag" [class.minor]="(component.componentVersion)%1" + >{{version.label}}</option> + </select> + </ng-container> + </div> + + <!-- CATEGORY --> + <ng-container *ngIf="component.categories && component.categories[0]"> + <div class="component-details-panel-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_category" tooltip="{{component.categories[0].name}}">{{component.categories[0].name}}</span> + </div> + + <!-- SUB CATEGORY --> + <div class="component-details-panel-item" *ngIf="component.categories[0].subcategories && component.categories[0].subcategories[0]"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_subCategory" tooltip="{{component.categories[0].subcategories[0].name}}">{{component.categories[0].subcategories[0].name}}</span> + </div> + </ng-container> + + <!-- CREATION DATE --> + <div class="component-details-panel-item" *ngIf="component.creationDate"> + <span class="name" [innerHTML]="'Creation Date:'"></span> + <span class="value" data-tests-id="rightTab_version" tooltip="{{component.creationDate | date: 'MM/dd/yyyy'}}">{{component.creationDate | date: 'MM/dd/yyyy'}}</span> + </div> + + <!-- AUTHOR --> + <div class="component-details-panel-item" *ngIf="component.creatorFullName"> + <span class="name" [innerHTML]="'Author:'"></span> + <span class="value" data-tests-id="rightTab_author" tooltip="{{component.creatorFullName}}">{{component.creatorFullName}}</span> + </div> + + <!-- Vendor Name data-ng-if="selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.vendorName"> + <span class="name" [innerHTML]="'Vendor Name:'"></span> + <span class="value" data-tests-id="rightTab_vendorName" tooltip="{{component.vendorName}}">{{component.vendorName}}</span> + </div> + + <!-- Vendor Release data-ng-if="selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.vendorRelease"> + <span class="name" [innerHTML]="'Vendor Release:'"></span> + <span class="value" data-tests-id="rightTab_vendorRelease" tooltip="{{component.vendorRelease}}">{{component.vendorRelease}}</span> + </div> + + <!-- Vendor Release data-ng-if="selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.resourceVendorModelNumber"> + <span class="name" [innerHTML]="'GENERAL_LABEL_RESOURCE_MODEL_NUMBER' | translate"></span> + <span class="value" data-tests-id="rightTab_resourceVendorModelNumber" tooltip="{{component.resourceVendorModelNumber}}">{{component.resourceVendorModelNumber}}</span> + </div> + + <!-- Service Type data-ng-if="selectedComponent.isService()"--> + <div class="component-details-panel-item" *ngIf="component.serviceType"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SERVICE_TYPE' | translate"></span> + <span class="value" data-tests-id="rightTab_serviceType" tooltip="{{component.serviceType}}">{{component.serviceType}}</span> + </div> + + <!-- Service Role data-ng-if="selectedComponent.isService()"--> + <div class="component-details-panel-item" *ngIf="component.serviceRole"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SERVICE_ROLE' | translate"></span> + <span class="value" data-tests-id="rightTab_serviceRole" tooltip="{{component.serviceRole}}">{{component.serviceRole}}</span> + </div> + + <!-- Contact ID --> + <div class="component-details-panel-item" *ngIf="component.contactId"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CONTACT_ID' | translate"></span> + <span class="value" data-tests-id="rightTab_contactId" tooltip="{{component.contactId}}">{{component.contactId}}</span> + </div> + + <!-- Service Name data-ng-if="isComponentInstanceSelected() && currentComponent.selectedInstance.isServiceProxy()"--> + <div class="component-details-panel-item" *ngIf="component.sourceModelName"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SOURCE_SERVICE_NAME' | translate"></span> + <span class="value" data-tests-id="rightTab_sourceModelName" tooltip="{{component.sourceModelName}}">{{component.sourceModelName}}</span> + </div> + + <!-- Customization UUID data-ng-if="isViewMode() && currentComponent.isService() && selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.customizationUUID"> + <span class="name" [innerHTML]="'GENERAL_LABEL_RESOURCE_CUSTOMIZATION_UUID' | translate"></span> + <span class="value" data-tests-id="rightTab_customizationModuleUUID" tooltip="{{component.customizationUUID}}">{{component.customizationUUID}}</span> + </div> + + <!-- DESCRIPTION --> + <div class="component-details-panel-item description"> + <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span> + <chars-ellipsis [text]="component.description" [maxChars]="55" [testId]="'rightTab_description'"></chars-ellipsis> + </div> + + + <!--TODO: move to separate component!--> + <ng-container *ngIf="componentType == 'POLICY'"> + <!-- TYPE --> + <div class="component-details-panel-item policy-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_TYPE' | translate"></span> + <span class="value" data-tests-id="rightTab_componentType" tooltip="{{component.policyTypeUid}}">{{component.policyTypeUid}}</span> + </div> + + <!-- CATEGORY --> + <div class="component-details-panel-item policy-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_category" tooltip="Policy">Policy</span> + </div> + + <!-- SUB CATEGORY --> + <div class="component-details-panel-item policy-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_subCategory" tooltip="Policy">Policy</span> + </div> + </ng-container> + + <!--TODO: move to separate component!--> + <ng-container *ngIf="componentType == 'GROUP'"> + <!-- CATEGORY --> + <div class="component-details-panel-item group-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_category" tooltip="Group">Group</span> + </div> + + <!-- SUB CATEGORY --> + <div class="component-details-panel-item group-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_subCategory" tooltip="Group">Group</span> + </div> + + </ng-container> + + </content> +</ng2-expand-collapse> + +<ng2-expand-collapse *ngIf="component.tags || isComponentInstanceSelected()"> + <header tooltip="Tags">Tags</header> + <content class="tags-container"> + <span *ngIf="component.tags?.indexOf(component.name)===-1" class="i-sdc-designer-sidebar-section-content-item-tag" + data-tests-id="rightTab_tag" tooltip="{{component.name}}">{{component.name}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-tag" *ngFor="let tag of component.tags" + data-tests-id="rightTab_tag" tooltip="{{tag}}">{{tag}}</span> + </content> +</ng2-expand-collapse> @@ -0,0 +1,51 @@ +@import '../../../../../../../assets/styles/variables'; + +.general-info-container { + display: flex; + flex-direction: column; + padding: 10px 20px; +} + +.component-details-panel-item { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 5px; + order:1; + + .name { font-family: OpenSans-Semibold, sans-serif; } + .value { padding-left: 10px; } + + + &.description { + margin-top: 28px; + white-space: normal; + word-wrap: break-word; + overflow: ellipsis; + + .value { + padding-left: 0; + max-width: none; + font-weight: normal; + font-family: @font-opensans-regular; + } + } + + &.group-item, &.policy-item { + order:0; + } +} + +.tags-container { + display: flex; + flex-wrap: wrap; + padding: 10px 20px; + + .i-sdc-designer-sidebar-section-content-item-tag { + padding: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: all; + } +} @@ -0,0 +1,98 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { CompositionPaletteService } from '../../../../../pages/composition/palette/services/palette.service'; +import { IAppMenu, SdcMenuToken } from '../../../../../../../app/ng2/config/sdc-menu.config'; +import { CompositionService } from '../../../../../pages/composition/composition.service'; +import { ServiceServiceNg2 } from '../../../../../../../app/services-ng2'; +import { WorkspaceService } from '../../../../../../../app/ng2/pages/workspace/workspace.service'; +import { ComponentInstanceServiceNg2 } from '../../../../../../../app/ng2/services/component-instance-services/component-instance.service'; +import { EventListenerService } from '../../../../../../../app/services'; +import { InfoTabComponent } from './info-tab.component'; +import { ConfigureFn, configureTests } from "../../../../../../../jest/test-config.helper"; +import { Observable } from "rxjs"; +import { leftPaletteElements } from "../../../../../../../jest/mocks/left-paeltte-elements.mock"; +import { TranslatePipe } from "../../../../../shared/translator/translate.pipe"; +import { HttpClientModule } from "@angular/common/http"; +import { TranslateModule } from "../../../../../../../app/ng2/shared/translator/translate.module"; +import _ from "lodash"; +import { TranslateService } from "../../../../../shared/translator/translate.service"; +import { SdcUiServices } from "onap-ui-angular"; +import { Component as TopologyTemplate, FullComponentInstance, ComponentInstance } from '../../../../../../../app/models'; + + +describe('InfoTabComponent', () => { + // let comp: InfoTabComponent; + let fixture: ComponentFixture<InfoTabComponent>; + + // let eventServiceMock: Partial<EventListenerService>; + let storeStub:Partial<Store>; + let compositionPaletteServiceStub:Partial<CompositionPaletteService>; + let iAppMenuStub:Partial<IAppMenu>; + let compositionServiceStub:Partial<CompositionService>; + let serviceServiceNg2Stub:Partial<ServiceServiceNg2>; + let workspaceServiceStub:Partial<WorkspaceService>; + let componentInstanceServiceNg2Stub:Partial<ComponentInstanceServiceNg2>; + let eventListenerServiceStub:Partial<EventListenerService>; + + beforeEach( + async(() => { + storeStub = {}; + iAppMenuStub = {}; + eventListenerServiceStub = { + notifyObservers: jest.fn() + } + compositionPaletteServiceStub = { + getLeftPaletteElements: jest.fn().mockImplementation(()=> Observable.of(leftPaletteElements)) + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + imports: [ ], + declarations: [ InfoTabComponent, TranslatePipe ], + schemas: [ NO_ERRORS_SCHEMA ], + providers: [ + { provide: Store, useValue: {} }, + { provide: CompositionPaletteService, useValue: compositionPaletteServiceStub }, + { provide: SdcMenuToken, useValue: {} }, + { provide: CompositionService, useValue: {} }, + { provide: SdcUiServices.ModalService, useValue: {}}, + { provide: ServiceServiceNg2, useValue: {} }, + { provide: WorkspaceService, useValue: {} }, + { provide: ComponentInstanceServiceNg2, useValue: {} }, + { provide: EventListenerService, useValue: eventListenerServiceStub }, + { provide: TranslateService, useValue: {}} + ] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(InfoTabComponent); + let comp = fixture.componentInstance; + + }); + }) + ); + + + it('can load instance', () => { + expect(fixture).toMatchSnapshot(); + }); + + describe('Version dropdown', () => { + it('is undefined for topologyTemplate', () => { + fixture.componentInstance.component = <TopologyTemplate>{}; + fixture.componentInstance.initEditResourceVersion(fixture.componentInstance.component, fixture.componentInstance.flatLeftPaletteElementsFromService(leftPaletteElements)); + expect(fixture.componentInstance.versions).toBe(undefined); + }); + it('does not contain the highest minor version if it is checked out', () => { + fixture.componentInstance.component = new ComponentInstance(); + fixture.componentInstance.component.allVersions = + {'1.0': "9c829122-af05-4bc9-b537-5d84f4c8ae25", '1.1': "930d56cb-868d-4e35-bd0f-e737d2fdb171"}; + fixture.componentInstance.component.version = "1.0"; + fixture.componentInstance.component.uuid = "a8cf015e-e4e5-4d4b-a01e-8624e8d36095"; + fixture.componentInstance.initEditResourceVersion(fixture.componentInstance.component, fixture.componentInstance.flatLeftPaletteElementsFromService(leftPaletteElements)); + expect(fixture.componentInstance.versions).toHaveLength(1); + }); + }); + +}); @@ -0,0 +1,189 @@ +import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core'; +import { + PolicyInstance, + GroupInstance, + Component as TopologyTemplate, + ComponentInstance, + LeftPaletteComponent, + FullComponentInstance +} from "app/models"; +import {Store} from "@ngxs/store"; +import { EVENTS, GRAPH_EVENTS } from 'app/utils'; +import {IDropDownOption} from "onap-ui-angular/dist/form-elements/dropdown/dropdown-models"; +import { CompositionPaletteService } from "app/ng2/pages/composition/palette/services/palette.service"; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from "onap-ui-angular"; +import { SdcMenuToken, IAppMenu } from "app/ng2/config/sdc-menu.config"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { ServiceServiceNg2 } from "app/services-ng2"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; +import { EventListenerService } from "app/services"; +import * as _ from 'lodash'; +import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions"; +import Dictionary = _.Dictionary; + + +@Component({ + selector: 'panel-info-tab', + templateUrl: './info-tab.component.html', + styleUrls: ['./info-tab.component.less'], + // providers: [SdcUiServices.ModalService] +}) +export class InfoTabComponent implements OnInit, OnDestroy { + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: TopologyTemplate | PolicyInstance | GroupInstance | ComponentInstance; + public versions: IDropDownOption[]; + private leftPalletElements: LeftPaletteComponent[]; + private isDisabledFlag: boolean; + private isComponentSelectedFlag: boolean; + + constructor(private store: Store, + private compositionPaletteService: CompositionPaletteService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private modalService: SdcUiServices.ModalService, + private componentInstanceService: ComponentInstanceServiceNg2, + private serviceService: ServiceServiceNg2, + private eventListenerService: EventListenerService, + @Inject(SdcMenuToken) public sdcMenu:IAppMenu) { + } + + ngOnInit() { + this.leftPalletElements = this.flatLeftPaletteElementsFromService(this.compositionPaletteService.getLeftPaletteElements()); + this.initEditResourceVersion(this.component, this.leftPalletElements); + this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, (comp) => { + this.component = comp; + }); + this.isComponentSelectedFlag = this.isComponentInstanceSelected(); + this.isDisabledFlag = this.isDisabled(); + + } + + ngOnDestroy() { + this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT); + } + + flatLeftPaletteElementsFromService = (leftPalleteElementsFromService: Dictionary<Dictionary<LeftPaletteComponent[]>>): LeftPaletteComponent[] => { + let retValArr = []; + for (const category in leftPalleteElementsFromService) { + for (const subCategory in leftPalleteElementsFromService[category]) { + retValArr = retValArr.concat(leftPalleteElementsFromService[category][subCategory].slice(0)); + } + } + return retValArr; + } + + private isComponentInstanceSelected () { + return this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + } + + private versioning: Function = (versionNumber: string): string => { + let version: Array<string> = versionNumber && versionNumber.split('.'); + return '00000000'.slice(version[0].length) + version[0] + '.' + '00000000'.slice(version[1].length) + version[1]; + }; + + + private onChangeVersion = (versionDropdown) => { + let newVersionValue = versionDropdown.value; + versionDropdown.value = (<FullComponentInstance>this.component).getComponentUid(); + + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + // let service = <Service>this.$scope.currentComponent; + if(this.component instanceof FullComponentInstance) { + + let onCancel = (error:any) => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + if (error) { + console.log(error); + } + }; + + let onUpdate = () => { + //this function will update the instance version than the function call getComponent to update the current component and return the new instance version + this.componentInstanceService.changeResourceInstanceVersion(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.component.uniqueId, newVersionValue) + .subscribe((component) => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_VERSION_CHANGED, component); + }, onCancel); + }; + + if (this.component.isService() || this.component.isServiceProxy()) { + this.serviceService.checkComponentInstanceVersionChange(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, + this.component.uniqueId, newVersionValue).subscribe((pathsToDelete:string[]) => { + if (pathsToDelete && pathsToDelete.length) { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + + + const {title, message} = this.sdcMenu.alertMessages['upgradeInstance']; + let pathNames:string = this.getPathNamesVersionChangeModal(pathsToDelete); + let onOk: Function = () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + onUpdate(); + }; + const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.info, callback: onOk, closeModal: true} as SdcUiComponents.ModalButtonComponent; + const cancelButton = {testId: "Cancel", text: "Cancel", type: SdcUiCommon.ButtonType.secondary, callback: <Function>onCancel, closeModal: true} as SdcUiComponents.ModalButtonComponent; + const modal = this.modalService.openInfoModal(title, message.format([pathNames]), 'confirm-modal', [okButton, cancelButton]); + modal.getCloseButton().onClick(onCancel); + } else { + onUpdate(); + } + }, onCancel); + } else { + onUpdate(); + } + } + }; + + + private getPathNamesVersionChangeModal = (pathsToDelete:string[]):string => { + const relatedPaths = _.filter(this.compositionService.forwardingPaths, path => + _.find(pathsToDelete, id => + path.uniqueId === id + ) + ).map(path => path.name); + const pathNames = _.join(relatedPaths, ', ') || 'none'; + return pathNames; + }; + + + private initEditResourceVersion = (component, leftPaletteComponents): void => { + if(this.component instanceof ComponentInstance) { + + this.versions = []; + let sorted:any = _.sortBy(_.toPairs(component.allVersions), (item) => { + return item[0] !== "undefined" && this.versioning(item[0]); + }); + _.forEach(sorted, (item) => { + this.versions.push({label: item[0], value: item[1]}); + }); + + let highestVersion = _.last(sorted)[0]; + + if (parseFloat(highestVersion) % 1) { //if highest is minor, make sure it is the latest checked in - + let latestVersionComponent: LeftPaletteComponent = _.maxBy( + _.filter(leftPaletteComponents, (leftPaletteComponent: LeftPaletteComponent) => { //latest checked in + return (leftPaletteComponent.systemName === component.systemName || leftPaletteComponent.uuid === component.uuid); + }) + , (component) => { + return component.version + }); + + let latestVersion: string = latestVersionComponent ? latestVersionComponent.version : highestVersion; + + if (latestVersion && highestVersion != latestVersion) { //highest is checked out - remove from options + this.versions = this.versions.filter(version => version.label != highestVersion); + } + } + } + } + + private isDisabled() { + return this.isViewOnly || this.component['archived'] || this.component['resourceType'] === 'CVFC' + } + +}; + @@ -0,0 +1,55 @@ +import { NgModule, Component, Compiler, ViewContainerRef, ViewChild, Input, ComponentRef, ComponentFactoryResolver, ChangeDetectorRef } from '@angular/core'; +import {Component as TopologyTemplate} from "app/models"; +import { SdcUiServices } from "onap-ui-angular"; + +// Helper component to add dynamic tabs +@Component({ + selector: 'panel-tab', + template: `<div #content></div>` +}) +export class PanelTabComponent { + @ViewChild('content', { read: ViewContainerRef }) content; + @Input() isActive:boolean; + @Input() panelTabType; + @Input() input; + @Input() isViewOnly:boolean; + @Input() component:TopologyTemplate; + @Input() componentType; + cmpRef: ComponentRef<any>; + private isViewInitialized: boolean = false; + + constructor(private componentFactoryResolver: ComponentFactoryResolver, + private cdRef: ChangeDetectorRef) { } + + updateComponent() { + if (!this.isViewInitialized || !this.isActive) { + return; + } + if (this.cmpRef) { + this.cmpRef.destroy(); + } + + let factory = this.componentFactoryResolver.resolveComponentFactory(this.panelTabType); + this.cmpRef = this.content.createComponent(factory); + this.cmpRef.instance.input = this.input; + this.cmpRef.instance.isViewOnly = this.isViewOnly; + this.cmpRef.instance.component = this.component; + this.cmpRef.instance.componentType = this.componentType; + this.cdRef.detectChanges(); + } + + ngOnChanges() { + this.updateComponent(); + } + + ngAfterViewInit() { + this.isViewInitialized = true; + this.updateComponent(); + } + + ngOnDestroy() { + if (this.cmpRef) { + this.cmpRef.destroy(); + } + } +}
\ No newline at end of file @@ -0,0 +1,65 @@ +@import '../../../../../../assets/styles/variables'; +@import '../../../../../../assets/styles/override'; + + +// --------------------------------------------------------------------------------------------------- +///* override sdc-ui library tabs */ +// --------------------------------------------------------------------------------------------------- + + +:host ::ng-deep .sdc-tabs { + + .sdc-tabs-list { + display: flex; + border-bottom: 1px solid @sdcui_color_silver; + min-height: min-content; + } + .sdc-tab { + background-color: @sdcui_color_white; + border: 1px solid @sdcui_color_silver; + border-left: none; + border-bottom: none; + height: 36px; + width: 60px; + display: flex; + align-content: center; + justify-content: center; + cursor: pointer; + padding: 0; + margin: 0; + + + &.sdc-tab-active { + background-color: @sdcui_color_silver; + border-bottom: none; + } + &[disabled] { + opacity: 0.3; + cursor: default; + } + } + &.sdc-tabs-header { + .sdc-tab { + font-size: 24px; + } + } + &.sdc-tabs-menu { + .sdc-tab { + font-size: 14px; + padding: 0px 10px 4px 10px; + } + } + .sdc-tab-content { + margin-top: 0; + flex:1; + overflow-y:auto; + } +} + + +:host ::ng-deep .expand-collapse-title { + margin-top: 1px; + background-color: #eaeaea; + color: #5a5a5a; + font-family: OpenSans-Semibold, sans-serif; +} @@ -1,50 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<ng2-expand-collapse state="0"> - <header tooltip="General Information">General Info</header> - <content> - <!-- TYPE --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_TYPE' | translate"></span> - <span class="value" data-tests-id="rightTab_componentType" tooltip="{{policy.policyTypeUid}}">{{policy.policyTypeUid}}</span> - </div> - - <!-- CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_category" tooltip="Policy">Policy</span> - </div> - - <!-- SUB CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_subCategory" tooltip="Policy">Policy</span> - </div> - - <!-- VERSION --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span> - <span class="value" data-tests-id="rightTab_version" tooltip="{{policy.version}}">{{policy.version}}</span> - </div> - - <!-- DESCRIPTION --> - <div class="component-details-panel-item description"> - <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span> - <span class="value" ellipsis="policy.description" max-chars="55" data-tests-id="rightTab_description">{{policy.description}}</span> - </div> - </content> -</ng2-expand-collapse> @@ -1,28 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<sdc-tabs> - <sdc-tab titleIcon="info-circle"> - <policy-information-tab [policy]="policy" [isViewOnly]="isViewOnly" *ngIf="policy"></policy-information-tab> - </sdc-tab> - <sdc-tab titleIcon="inputs-o"> - <policy-targets-tab [policy]="policy" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" (isLoading)="setIsLoading($event)" *ngIf="policy"></policy-targets-tab> - </sdc-tab> - <sdc-tab titleIcon="settings-o"> - <policy-properties-tab [policy]="policy" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" *ngIf="policy"></policy-properties-tab> - </sdc-tab> -</sdc-tabs> - @@ -1,72 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, AfterViewInit, OnChanges } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { PoliciesService } from "../../../../../services/policies.service"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; -import { GRAPH_EVENTS } from './../../../../../../utils/constants'; -import { EventListenerService } from 'app/services/event-listener-service'; -import { ZoneInstance } from 'app/models/graph/zones/zone-instance'; -import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks"; - -@Component({ - selector: 'policy-tabs', - templateUrl: './policy-tabs.component.html' -}) -export class PolicyTabsComponent implements OnChanges { - - @Input() topologyTemplate:TopologyTemplate; - @Input() selectedZoneInstanceType:string; - @Input() selectedZoneInstanceId:string; - @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); - - private policy:PolicyInstance; - - constructor(private translateService:TranslateService, - private policiesService:PoliciesService - ) { - - } - - ngOnChanges(changes: SimpleChanges): void { - this.initPolicy(); - } - - private initPolicy = ():void => { - this.isLoading.emit(true); - this.policiesService.getSpecificPolicy(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).subscribe( - policy => { - this.policy = policy; - console.log(JSON.stringify(policy)); - }, - error => console.log("Error getting policy!"), - () => this.isLoading.emit(false) - ); - } - - private setIsLoading = (value) :void => { - this.isLoading.emit(value); - } - -} @@ -1,68 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ -import { NgModule } from "@angular/core"; -import { HttpModule } from "@angular/http"; -import { FormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; -import { ExpandCollapseComponent } from 'app/ng2/components/ui/expand-collapse/expand-collapse.component'; -import { PoliciesService } from "../../../../../services/policies.service"; -import { PolicyInformationTabComponent } from "./policy-information-tab.component"; -import { PolicyTargetsTabComponent } from "./policy-targets-tab.component"; -import { PolicyTabsComponent } from "./policy-tabs.component"; -import { PolicyPropertiesTabComponent } from "./policy-properties-tab.component"; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { TranslateModule } from './../../../../../shared/translator/translate.module'; - -@NgModule({ - declarations: [ - PolicyInformationTabComponent, - PolicyTargetsTabComponent, - PolicyPropertiesTabComponent, - PolicyTabsComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpModule, - SdcUiComponentsModule, - TranslateModule, - UiElementsModule - ], - entryComponents: [ - PolicyInformationTabComponent, - PolicyTargetsTabComponent, - PolicyPropertiesTabComponent, - PolicyTabsComponent, - ExpandCollapseComponent - ], - exports: [ - PolicyInformationTabComponent, - PolicyTargetsTabComponent, - PolicyPropertiesTabComponent, - PolicyTabsComponent - ], - providers: [ - PoliciesService - ] -}) -export class PolicyTabsModule { - -} @@ -1,12 +0,0 @@ -/deep/ -.component-details-panel-tab-policy-targets { - .component-details-panel-large-item { - display: flex; - flex-direction: row; - justify-content: space-between; - } - .w-sdc-designer-sidebar-section-title { - display: flex; - justify-content: space-between; - } -}
\ No newline at end of file @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<div class="w-sdc-designer-sidebar-section-title" titleTooltip="Targets">Targets +<h1 class="w-sdc-designer-sidebar-section-title" titleTooltip="Targets">Targets <svg-icon-label *ngIf="!isViewOnly" class="add-policy-button" name="plus-circle-o" @@ -24,7 +24,7 @@ labelPlacement="right" (click)="openAddTargetModal()"> </svg-icon-label> -</div> +</h1> <div class="expand-collapse-content"> <ul> <li *ngFor="let target of targets; let i = index" class="component-details-panel-large-item" @@ -40,7 +40,7 @@ </li> </ul> - <div *ngIf="targets.length===0" class="component-details-panel-tab-no-data"> + <div *ngIf="!targets || targets.length===0" class="component-details-panel-tab-no-data"> <div class="component-details-panel-tab-no-data-title">No data to display yet</div> <div class="component-details-panel-tab-no-data-content">Add targets to policy to see targets</div> </div> @@ -1,8 +1,6 @@ @import './../../../../../../../assets/styles/mixins'; -@import "./../../../../../../../assets/styles/variables-old"; -@import './../../../../../../../assets/styles/mixins_old'; -/deep/ + .expand-collapse-content { padding: 20px; } @@ -25,7 +23,9 @@ white-space: nowrap; height: 32px; line-height: 32px; - vertical-align: middle; + display: flex; + flex-direction: row; + justify-content: space-between; &:hover { background-color: #f8f8f8; @@ -37,30 +37,25 @@ } } -.component-details-panel-item { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - height: 22px; - line-height: 22px; - vertical-align: middle; - - &.description { - margin-top: 28px; - white-space: normal; - word-wrap: break-word; - .value { - max-width: none; - font-weight: normal; - font-family: @font-opensans-regular; - } - } - - .name { font-family: OpenSans-Semibold, sans-serif; } - .value { } -} - .component-details-panel-item-delete { cursor: pointer; visibility: hidden; } + +/deep/ .w-sdc-designer-sidebar-section-title { + color: #5a5a5a; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + background-color: #eaeaea; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + text-transform: uppercase; + line-height: 32px; + padding: 0 10px 0 20px; + margin-top: 1px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} @@ -0,0 +1,113 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Rx'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { TranslateService } from '../../../../../shared/translator/translate.service'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { CompositionService } from '../../../composition.service'; +import { PolicyTargetsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component"; +import { PoliciesService } from "app/services-ng2"; +import { PolicyInstance, GroupInstance } from "app/models"; +import { NgxsModule } from "@ngxs/store"; +import { GraphState } from "app/ng2/pages/composition/common/store/graph.state"; +import { WorkspaceState } from "app/ng2/store/states/workspace.state"; +import { TargetUiObject } from "app/models/ui-models/ui-target-object"; +import { TargetOrMemberType } from "app/utils"; + + + + +describe('policy targets tab component', () => { + + let fixture: ComponentFixture<PolicyTargetsTabComponent>; + let component: PolicyTargetsTabComponent; + + let policiesServiceMock = Mock.of<PoliciesService>( + { + updateTargets: jest.fn().mockImplementation((compType, uid, policyUniqueId, updatedTargets) => { + if (updatedTargets === undefined) { + return Observable.throwError('error'); + } else { + return Observable.of(updatedTargets); + } + } + )}); + + let compositionServiceMock = { + componentInstances: [{uniqueId: '1', name: 'inst1'}, + {uniqueId: '2', name: 'inst2'}, + {uniqueId: '3', name: 'inst3'}, + {uniqueId: '4', name: 'inst4'}, + {uniqueId: '5', name: 'inst5'} + ], + groupInstances : [ + Mock.of<GroupInstance>({uniqueId: "group1", name: "group1"}), + Mock.of<GroupInstance>({uniqueId: "group2", name: "group2"}), + Mock.of<GroupInstance>({uniqueId: "group3", name: "group3"}) + ] + }; + + let workspaceServiceMock = { + metadata: Mock.of<ComponentMetadata>() + }; + + let modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => { return { + innerModalContent: { instance: { existingElements: targetsToAdd }}, + closeModal: jest.fn() + }}) + }; + + let loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const targetsToAdd = [ + <TargetUiObject>{uniqueId: '1', name: 'inst1', type: TargetOrMemberType.COMPONENT_INSTANCES}, + <TargetUiObject>{uniqueId: "group1", name: "group1", type: TargetOrMemberType.GROUPS} + ]; + + const policyInstanceMock = Mock.of<PolicyInstance>( + { getTargetsAsUiObject: jest.fn().mockImplementation( () => targetsToAdd) + }); + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [PolicyTargetsTabComponent], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: PoliciesService, useValue: policiesServiceMock}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: EventListenerService, useValue: {} }, + {provide: CompositionService, useValue: compositionServiceMock }, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + + fixture = TestBed.createComponent(PolicyTargetsTabComponent); + component = fixture.componentInstance; + component.policy = policyInstanceMock; + }); + + + it('if there are no existing targets, all component instances AND all groups are available for adding', () => { + component.targets = []; + const optionalTargetsToAdd = component.getOptionalsTargetsToAdd(); + expect(optionalTargetsToAdd).toHaveLength(8); + }); + + it('list of available instances to add does not include existing targets', () => { + component.targets = targetsToAdd; + const optionalMembersToAdd = component.getOptionalsTargetsToAdd(); + expect(optionalMembersToAdd).toHaveLength(6); + }); +}); @@ -19,84 +19,106 @@ */ import * as _ from "lodash"; -import { Component, Input, Output, EventEmitter, OnChanges, HostBinding, OnDestroy } from "@angular/core"; +import { Component, Input, Output, EventEmitter, OnChanges, HostBinding, OnDestroy, OnInit } from "@angular/core"; import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { Component as TopologyTemplate } from "app/models"; import { PoliciesService } from "../../../../../services/policies.service"; -import { PolicyInstance, PolicyTargetsMap } from './../../../../../../models/graph/zones/policy-instance'; -import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -import { IModalConfig } from "sdc-ui/lib/angular/modals/models/modal-config"; +import { PolicyInstance } from './../../../../../../models/graph/zones/policy-instance'; +import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular"; import { AddElementsComponent } from "../../../../../components/ui/modal/add-elements/add-elements.component"; import { TargetUiObject } from "../../../../../../models/ui-models/ui-target-object"; import { ComponentInstance } from "../../../../../../models/componentsInstances/componentInstance"; import { TargetOrMemberType } from "../../../../../../utils/constants"; import { GRAPH_EVENTS } from 'app/utils'; import { EventListenerService } from 'app/services/event-listener-service'; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { Store } from "@ngxs/store"; +import { Select } from "@ngxs/store"; +import { Observable } from "rxjs"; +import { tap } from "rxjs/operators"; +import {GraphState} from "../../../common/store/graph.state"; @Component({ selector: 'policy-targets-tab', templateUrl: './policy-targets-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'policy-targets-tab.component.less'] + styleUrls: ['policy-targets-tab.component.less'] }) + +export class PolicyTargetsTabComponent implements OnInit { -export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { + @Input() input:any; - private targets: Array<TargetUiObject>; // UI object to hold all targets with names. - @Input() policy: PolicyInstance; - @Input() topologyTemplate: TopologyTemplate; @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); @HostBinding('class') classes = 'component-details-panel-tab-policy-targets'; + @Select(GraphState.getSelectedComponent) policy$: Observable<PolicyInstance>; + public policy: PolicyInstance; + private subscription; + + private addModalInstance: SdcUiComponents.ModalComponent; + public targets: Array<TargetUiObject>; // UI object to hold all targets with names. + constructor(private translateService: TranslateService, private policiesService: PoliciesService, - private modalService: SdcUiComponents.ModalService, - private eventListenerService: EventListenerService - ) { - this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.initTargets) - } + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private store: Store + ) { } - ngOnChanges(changes:SimpleChanges):void { - this.initTargets(); + ngOnInit() { + this.subscription = this.policy$.pipe( + tap((policy) => { + if(policy instanceof PolicyInstance){ + this.policy = policy; + this.targets = this.policy.getTargetsAsUiObject(<ComponentInstance[]>this.compositionService.componentInstances, this.compositionService.groupInstances); + } + })).subscribe(); } - ngOnDestroy() { - this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE); + ngOnDestroy () { + if(this.subscription) + this.subscription.unsubscribe(); } deleteTarget(target: TargetUiObject): void { - this.isLoading.emit(true); - this.policiesService.deletePolicyTarget(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.policy, target.uniqueId, target.type).subscribe( + this.loaderService.activate(); + this.policiesService.deletePolicyTarget(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.policy, target.uniqueId, target.type).subscribe( (policyInstance:PolicyInstance) => { + this.targets = this.targets.filter(item => item.uniqueId !== target.uniqueId); this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, policyInstance); + // this.store.dispatch(new UpdateSelectedComponentAction({uniqueId: policyInstance.uniqueId, type:ComponentType.})); + }, + error => { + console.log("Error deleting target!"); + this.loaderService.deactivate(); }, - error => console.log("Error deleting target!"), - () => this.isLoading.emit(false) + () => this.loaderService.deactivate() ); } - private initTargets = (policyInstance?: PolicyInstance) => { - this.policy = policyInstance ? policyInstance : this.policy; - this.targets = this.policy.getTargetsAsUiObject(this.topologyTemplate.componentInstances, this.topologyTemplate.groupInstances); - } addTargets = ():void => { - var targetsToAdd:Array<TargetUiObject> = this.modalService.getCurrentInstance().innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data + var targetsToAdd:Array<TargetUiObject> = this.addModalInstance.innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data if(targetsToAdd.length > 0) { - this.modalService.closeModal(); - this.isLoading.emit(true); - var updatedTarget: Array<TargetUiObject> = _.union(this.targets, targetsToAdd); - this.policiesService.updateTargets(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.policy.uniqueId, updatedTarget).subscribe( + this.addModalInstance.closeModal(); + this.loaderService.activate(); + var updatedTargets: Array<TargetUiObject> = _.union(this.targets, targetsToAdd); + this.policiesService.updateTargets(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.policy.uniqueId, updatedTargets).subscribe( (updatedPolicyInstance:PolicyInstance) => { + this.targets = updatedTargets; this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, updatedPolicyInstance); + // this.store.dispatch(new UpdateSelectedComponentAction({component: updatedPolicyInstance})); }, error => { console.log("Error updating targets!"); + this.loaderService.deactivate(); }, - () => this.isLoading.emit(false) + () => this.loaderService.deactivate() ); } } @@ -104,7 +126,7 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { getOptionalsTargetsToAdd():Array<TargetUiObject> { let optionalsTargetsToAdd:Array<TargetUiObject> = []; // adding all instances as optional targets to add if not already exist - _.forEach(this.topologyTemplate.componentInstances, (instance:ComponentInstance) => { + _.forEach(this.compositionService.componentInstances, (instance:ComponentInstance) => { if (!_.some(this.targets, (target:TargetUiObject) => { return target.uniqueId === instance.uniqueId })) { @@ -113,7 +135,7 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { }); // adding all groups as optional targets to add if not already exist - _.forEach(this.topologyTemplate.groupInstances, (groupInstance:ComponentInstance) => { // adding all instances as optional targets to add if not already exist + _.forEach(this.compositionService.groupInstances, (groupInstance:ComponentInstance) => { // adding all instances as optional targets to add if not already exist if (!_.some(this.targets, (target:TargetUiObject) => { return target.uniqueId === groupInstance.uniqueId })) { @@ -125,21 +147,20 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { } openAddTargetModal(): void { - let addTargetModalConfig: IModalConfig = { + let addTargetModalConfig = { title: this.policy.name + " ADD TARGETS", size: "md", - type: "custom", + type: SdcUiCommon.ModalType.custom, testId: "addTargetsModal", buttons: [ {text: "ADD TARGETS", size: 'xsm', callback: this.addTargets, closeModal: false}, {text: 'CANCEL', size: 'sm', type: "secondary", closeModal: true} ] - }; + } as SdcUiCommon.IModalConfig; var optionalTargetsToAdd = this.getOptionalsTargetsToAdd(); - this.modalService.openCustomModal(addTargetModalConfig, AddElementsComponent, { + this.addModalInstance = this.modalService.openCustomModal(addTargetModalConfig, AddElementsComponent, { elementsToAdd: optionalTargetsToAdd, elementName: "target" }); - } } @@ -0,0 +1,97 @@ +<ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header> + <content> + <div class="w-sdc-designer-sidebar-section"> + <div *ngIf="properties"> + <ng-container *ngFor="let key of objectKeys(properties); let idx = index"> + <sdc-accordion [title]="groupNameByKey(key) + ' Properties'" [css-class]="'properties-accordion'" [arrow-direction]="'right'" [testId]="groupNameByKey(key) + 'properties'" [open]="true"> + + <!--ng-show="isShowDetailsSection" --> + <div class="i-sdc-designer-sidebar-section-content-item" *ngIf="!groupPropertiesByInstance"> + <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" attr.data-tests-id="propertyRow" + *ngFor="let property of properties[key]"> + + <div class="property-details"> + <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label" + [ngClass]="{'hand enabled': !isViewOnly}" + sdc-tooltip tooltip-text="{{property.name}}" + (click)="!isViewOnly && updateProperty(property)" + attr.data-tests-id="{{property.name}}">{{property.name}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="isPropertyOwner()" + sdc-tooltip tooltip-text="{{property.defaultValue}}">{{property.defaultValue}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="!isPropertyOwner()" + sdc-tooltip tooltip-text="{{property.value}}" + attr.data-tests-id="value_{{property.name}}">{{property.value}}</span> + </div> + <div class="property-buttons"> + <svg-icon *ngIf="!isViewOnly && (isPropertyOwner() && !property.readonly)" name="trash-o" clickable="true" size="medium" mode="info" testId="delete_{{property.name}}" (click)="deleteProperty(property)"></svg-icon> + </div> + </div> + </div> + <div class="i-sdc-designer-sidebar-section-content-item" *ngIf="groupPropertiesByInstance"> + <ng-container *ngFor="let InstanceProperties of properties[key]; let propIndex = index"> + <div class="vfci-properties-group"> + <div class="second-level"> + <div class="expand-collapse-title-icon"></div> + <span class="w-sdc-designer-sidebar-section-title-text" sdc-tooltip tooltip-text="{{getComponentInstanceNameFromInstanceByKey(InstanceProperties.key)}} Properties" + attr.data-tests-id="vfci-properties">{{getComponentInstanceNameFromInstanceByKey(InstanceProperties.key) + ' Properties'}}</span> + </div> + </div> + <div class="w-sdc-designer-sidebar-section-content instance-properties {{propIndex}}"> + <div class="i-sdc-designer-sidebar-section-content-item"> + <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" attr.data-tests-id="propertyRow" + *ngFor="let instanceProperty of InstanceProperties.value"> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label" + [ngClass]="{'hand enabled': !isViewOnly}" + sdc-tooltip tooltip-text="{{instanceProperty.name}}" + attr.data-tests-id="vfci-property">{{instanceProperty.name}}</span> + </div> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" + sdc-tooltip tooltip-text="{{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}"> + {{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}</span> + </div> + </div> + </div> + </div> + </ng-container> + </div> + <!--<div class="w-sdc-designer-sidebar-section-footer" *ngIf="(!isViewOnly && isPropertyOwner()) || showAddPropertyButton">--> + <!--<button class="w-sdc-designer-sidebar-section-footer-action tlv-btn blue" attr.data-tests-id="addGrey" (click)="addProperty()" type="button">--> + <!--Add Property--> + <!--</button>--> + <!--</div>--> + </sdc-accordion> + </ng-container> + </div> + + <!--attributes--> + <div *ngIf="attributes"> + <ng-container *ngFor="let key of objectKeys(attributes); let attrIndex = index"> + <sdc-accordion [title]="groupNameByKey(key) + ' Attributes'" [arrow-direction]="'right'" [testId]="groupNameByKey(key) + 'attributes'" [css-class]="'attributes-accordion'"> + <!--ng-show="isShowDetailsSection" --> + <div class="i-sdc-designer-sidebar-section-content-item"> + <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" + *ngFor="let attribute of attributes[key]"> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label" + [ngClass]="{'hand enabled': !isViewOnly}" + sdc-tooltip tooltip-text="{{attribute.name}}" + (click)="!isViewOnly && viewAttribute(attribute)" + attr.data-tests-id="{{attribute.name}}-attr">{{attribute.name}}</span> + </div> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="isPropertyOwner()" + sdc-tooltip tooltip-text="{{attribute.defaultValue}}">{{attribute.defaultValue}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="!isPropertyOwner()" + sdc-tooltip tooltip-text="{{attribute.value}}" attr.data-tests-id="value-of-{{attribute.name}}">{{attribute.value}}</span> + </div> + </div> + </div> + </sdc-accordion> + </ng-container> + </div> + </div> + </content> +</ng2-expand-collapse> @@ -0,0 +1,66 @@ +.scroll-container { + display: flex; + overflow-y: auto; +} + +.i-sdc-designer-sidebar-section-content-item-property-and-attribute { + color: #666666; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + border-bottom: 1px solid #cdcdcd; + min-height: 72px; + padding: 15px 10px 10px 18px; + // position: relative; + display:flex; + + .property-details { + flex:1; + } + + .property-buttons { + flex: 0 0 auto; + align-self: center; + } +} + +.i-sdc-designer-sidebar-section-content-item-property-and-attribute-label { + display: block; + font-weight: bold; + &:hover { + color: #3b7b9b; + } +} + +.i-sdc-designer-sidebar-section-content-item-property-and-attribute-label, .i-sdc-designer-sidebar-section-content-item-property-value { + overflow: hidden; + text-overflow: ellipsis; + max-width: 245px; + white-space: nowrap; + display: block; +} + + + +/deep/ .expand-collapse-content { + max-height: max-content; + padding: 10px 0; + + .sdc-accordion .sdc-accordion-header { + + background-color: #e6f6fb; + border-left: solid #009fdb 4px; + box-shadow: 0 0px 3px -1px rgba(0, 0, 0, 0.3); + margin-bottom: 2px; + width: auto; + height: auto; + padding: 10px; + color: #666666; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + + } + + /deep/.sdc-accordion .sdc-accordion-body { + padding-left: 0; + } +}
\ No newline at end of file @@ -0,0 +1,212 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + AttributeModel, + AttributesGroup, + Component as TopologyTemplate, + ComponentMetadata, + FullComponentInstance, + PropertiesGroup, + PropertyModel +} from 'app/models'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { GroupByPipe } from 'app/ng2/pipes/groupBy.pipe'; +import { ResourceNamePipe } from 'app/ng2/pipes/resource-name.pipe'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response'; +import { TranslateService } from 'app/ng2/shared/translator/translate.service'; +import { ModalsHandler } from 'app/utils'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions"; + +@Component({ + selector: 'properties-tab', + templateUrl: './properties-tab.component.html', + styleUrls: ['./properties-tab.component.less'] +}) +export class PropertiesTabComponent implements OnInit { + attributes: AttributesGroup; + isComponentInstanceSelected: boolean; + properties: PropertiesGroup; + groupPropertiesByInstance: boolean; + propertiesMessage: string; + metadata: ComponentMetadata; + objectKeys = Object.keys; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: FullComponentInstance | TopologyTemplate; + @Input() input: {title: string}; + + constructor(private store: Store, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private modalsHandler: ModalsHandler, + private topologyTemplateService: TopologyTemplateService, + private modalService: SdcUiServices.ModalService, + private translateService: TranslateService, + private groupByPipe: GroupByPipe) { + } + + ngOnInit() { + this.metadata = this.workspaceService.metadata; + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.getComponentInstancesPropertiesAndAttributes(); + } + + public isPropertyOwner = (): boolean => { + return this.component instanceof TopologyTemplate && this.component.isResource(); + } + + public updateProperty = (property: PropertyModel): void => { + this.openEditPropertyModal(property); + } + + public deleteProperty = (property: PropertyModel): void => { + + const onOk: Function = (): void => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + this.topologyTemplateService.deleteProperty(this.component.componentType, this.component.uniqueId, property.uniqueId) + .subscribe((response) => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.component.properties = this.component.properties.filter((prop) => prop.uniqueId !== property.uniqueId); + this.initComponentProperties(); + }, () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }); + }; + + const title: string = this.translateService.translate('PROPERTY_VIEW_DELETE_MODAL_TITLE'); + const message: string = this.translateService.translate('PROPERTY_VIEW_DELETE_MODAL_TEXT', {name: property.name}); + const okButton = { + testId: 'OK', + text: 'OK', + type: SdcUiCommon.ButtonType.info, + callback: onOk, + closeModal: true} as SdcUiComponents.ModalButtonComponent; + this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]); + } + + public groupNameByKey = (key: string): string => { + switch (key) { + case 'derived': + return 'Derived'; + + case this.metadata.uniqueId: + return ResourceNamePipe.getDisplayName(this.metadata.name); + + default: + return this.getComponentInstanceNameFromInstanceByKey(key); + } + } + + public getComponentInstanceNameFromInstanceByKey = (key: string): string => { + let instanceName: string = ''; + const componentInstance = this.compositionService.getComponentInstances().find((item) => item.uniqueId === key); + if (key !== undefined && componentInstance) { + + instanceName = ResourceNamePipe.getDisplayName(componentInstance.name); + } + return instanceName; + } + + private getComponentInstancesPropertiesAndAttributes = () => { + this.topologyTemplateService.getComponentInstanceAttributesAndProperties( + this.workspaceService.metadata.uniqueId, + this.workspaceService.metadata.componentType) + .subscribe((genericResponse: ComponentGenericResponse) => { + this.compositionService.componentInstancesAttributes = genericResponse.componentInstancesAttributes || new AttributesGroup(); + this.compositionService.componentInstancesProperties = genericResponse.componentInstancesProperties; + this.initPropertiesAndAttributes(); + }); + } + + private initComponentProperties = (): void => { + let result: PropertiesGroup = {}; + + this.propertiesMessage = undefined; + this.groupPropertiesByInstance = false; + if (this.component instanceof FullComponentInstance) { + result[this.component.uniqueId] = _.orderBy(this.compositionService.componentInstancesProperties[this.component.uniqueId], ['name']); + if (this.component.originType === 'VF') { + this.groupPropertiesByInstance = true; + result[this.component.uniqueId] = Array.from(this.groupByPipe.transform(result[this.component.uniqueId], 'path')); + } + } else if (this.metadata.isService()) { + // Temporally fix to hide properties for service (UI stack when there are many properties) + result = this.compositionService.componentInstancesProperties; + this.propertiesMessage = 'Note: properties for service are disabled'; + } else { + const componentUid = this.component.uniqueId; + result[componentUid] = Array<PropertyModel>(); + const derived = Array<PropertyModel>(); + _.forEach(this.component.properties, (property: PropertyModel) => { + if (componentUid === property.parentUniqueId) { + result[componentUid].push(property); + } else { + property.readonly = true; + derived.push(property); + } + }); + if (derived.length) { + result['derived'] = derived; + } + this.objectKeys(result).forEach((key) => { result[key] = _.orderBy(result[key], ['name']); }); + } + this.properties = result; + } + + private initComponentAttributes = (): void => { + let result: AttributesGroup = {}; + + if (this.component) { + if (this.component instanceof FullComponentInstance) { + result[this.component.uniqueId] = this.compositionService.componentInstancesAttributes[this.component.uniqueId] || []; + } else if (this.metadata.isService()) { + result = this.compositionService.componentInstancesAttributes; + } else { + result[this.component.uniqueId] = (this.component as TopologyTemplate).attributes; + } + this.attributes = result; + this.objectKeys(this.attributes).forEach((key) => { + this.attributes[key] = _.orderBy(this.attributes[key], ['name']); + }); + + } + } + + /** + * This function is checking if the component is the value owner of the current property + * in order to notify the edit property modal which fields to disable + */ + private isPropertyValueOwner = (): boolean => { + return this.metadata.isService() || !!this.component; + } + + /** + * The function opens the edit property modal. + * It checks if the property is from the VF or from one of it's resource instances and sends the needed property list. + * For create property reasons an empty array is transferd + * + * @param property the wanted property to edit/create + */ + private openEditPropertyModal = (property: PropertyModel): void => { + this.modalsHandler.newOpenEditPropertyModal(property, + (this.isPropertyOwner() ? + this.properties[property.parentUniqueId] : + this.properties[property.resourceInstanceUniqueId]) || [], + this.isPropertyValueOwner(), 'component', property.resourceInstanceUniqueId).then((updatedProperty: PropertyModel) => { + if (updatedProperty) { + const oldProp = _.find(this.properties[updatedProperty.resourceInstanceUniqueId], + (prop: PropertyModel) => prop.uniqueId === updatedProperty.uniqueId); + oldProp.value = updatedProperty.value; + } + }); + } + + private initPropertiesAndAttributes = (): void => { + this.initComponentProperties(); + this.initComponentAttributes(); + } +} @@ -0,0 +1,36 @@ +<div class="w-sdc-designer-sidebar-tab-content sdc-general-tab relations"> + <div *ngIf="!isCurrentDisplayComponentIsComplex(); else complexComponentTemplate"> + <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations"> + <sdc-accordion [title]="'Capabilities'" [arrow-direction]="'right'" [testId]="'Capabilities-accordion'"> + <div *ngFor="let capability of capabilities" class="relations-details-container"> + <div class="relations-name">{{capability.name}} </div> + <div class="relations-desc"> {{capability.type}} </div> + </div> + </sdc-accordion> + </div> + <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations"> + <sdc-accordion [title]="'Requirements'" [arrow-direction]="'right'" [testId]="'Requirements-accordion'"> + <requirement-list [component]='component' [requirements]="requirements" [isInstanceSelected]="isComponentInstanceSelected"></requirement-list> + </sdc-accordion> + + </div> + </div> + + <ng-template #complexComponentTemplate> + <sdc-accordion *ngIf="capabilitiesInstancesMap" [title]="'Capabilities'" [arrow-direction]="'right'" [testId]="'Capabilities-accordion'"> + <sdc-accordion *ngFor="let key of objectKeys(capabilitiesInstancesMap); let i = index" [title]="key"> + <div *ngFor="let capability of capabilitiesInstancesMap[key]" class="relations-details-container"> + <div class="relations-name">{{capability.name}} </div> + <div class="relations-desc"> {{capability.type}} </div> + </div> + </sdc-accordion> + </sdc-accordion> + + <sdc-accordion *ngIf="requirementsInstancesMap" [title]="'Requirements'" [arrow-direction]="'right'" [testId]="'Requirements-accordion'"> + <sdc-accordion *ngFor="let key of objectKeys(requirementsInstancesMap); let i = index" [title]="key"> + <requirement-list [component]='component' [requirements]="requirementsInstancesMap[key]" [isInstanceSelected]="isComponentInstanceSelected"></requirement-list> + </sdc-accordion> + </sdc-accordion> + + </ng-template> +</div> @@ -0,0 +1,57 @@ + +/deep/.sdc-accordion { + margin-bottom: 0; + display: grid; + + .sdc-accordion-header { + background-color: #e6f6fb; + border-left: solid #009fdb 4px; + box-shadow: 0 0px 3px -1px rgba(0, 0, 0, 0.3); + margin-bottom: 2px; + width: auto; + height: auto; + padding: 10px; + color: #666666; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + } + + .sdc-accordion-body.open { + padding-left: 0; + padding-top: 0; + .sdc-accordion-header { /*Second level - nested accordion */ + background-color: #f8f8f8; + padding: 4px 20px 4px 37px; + border-bottom: 1px solid #d2d2d2; + border-left:none; + height: 30px; + } + } +} + + +.relations-details-container { + border-bottom: 1px solid #cdcdcd; + padding: 10px 10px 10px 18px; + + font-size: 14px; + font-family: OpenSans-Regular, sans-serif; + + .relations-name { + color: #666666; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: capitalize; + max-width: 240px; + display: inline-block; + } + + .relations-desc { + color: #8c8c8c; + word-wrap: break-word; + white-space: normal; + max-width: 265px; + } +}
\ No newline at end of file @@ -0,0 +1,165 @@ +import { Component, OnInit, Input, OnDestroy } from '@angular/core'; +import { Component as TopologyTemplate, Capability, Requirement, CapabilitiesGroup, RequirementsGroup, ComponentInstance, FullComponentInstance } from "app/models"; +import { Store } from "@ngxs/store"; +import { GRAPH_EVENTS } from "app/utils"; +import { ComponentGenericResponse } from "app/ng2/services/responses/component-generic-response"; +import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service"; +import { EventListenerService } from "app/services"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions"; + + +export class InstanceCapabilitiesMap { + [key:string]:Array<Capability>; +} + +export class InstanceRequirementsMap { + [key:string]:Array<Requirement>; +} + +@Component({ + selector: 'req-capabilities-tab', + templateUrl: './req-capabilities-tab.component.html', + styleUrls: ['./req-capabilities-tab.component.less'] +}) +export class ReqAndCapabilitiesTabComponent implements OnInit, OnDestroy { + + isComponentInstanceSelected: boolean; + capabilities:Array<Capability>; + requirements:Array<Requirement>; + capabilitiesInstancesMap:InstanceCapabilitiesMap; + requirementsInstancesMap:InstanceRequirementsMap; + objectKeys = Object.keys; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: TopologyTemplate | FullComponentInstance; + @Input() input: any; + + + constructor(private store: Store, + private topologyTemplateService:TopologyTemplateService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private eventListenerService:EventListenerService) { } + + ngOnInit(): void { + + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + + this.requirements = []; + this.capabilities = []; + this.initEvents(); + this.initRequirementsAndCapabilities(); + + } + + private initEvents = ():void => { + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_NODE_SELECTED, this.initRequirementsAndCapabilities); + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.updateRequirementCapabilities); + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE, this.updateRequirementCapabilities); + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, this.updateRequirementCapabilities); + } + + ngOnDestroy(): void { + this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_NODE_SELECTED, this.initRequirementsAndCapabilities); + this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.updateRequirementCapabilities); + this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE, this.updateRequirementCapabilities); + this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, this.updateRequirementCapabilities); + } + + public isCurrentDisplayComponentIsComplex = ():boolean => { + + if (this.component instanceof FullComponentInstance) { + if (this.component.originType === 'VF') { + return true; + } + return false; + } else { + return this.component.isComplex(); + } + } + + private loadComplexComponentData = () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + this.topologyTemplateService.getCapabilitiesAndRequirements(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response:ComponentGenericResponse) => { + this.workspaceService.metadata.capabilities = response.capabilities; + this.workspaceService.metadata.requirements = response.requirements; + this.setScopeCapabilitiesRequirements(response.capabilities, response.requirements); + this.initInstancesMap(); + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }, (error) => { this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); }); + } + + + private extractValuesFromMap = (map:CapabilitiesGroup | RequirementsGroup):Array<any> => { + let values = []; + _.forEach(map, (capabilitiesOrRequirements:Array<Capability> | Array<Requirement>, key) => { + values = values.concat(capabilitiesOrRequirements) + } + ); + return values; + } + + private setScopeCapabilitiesRequirements = (capabilities:CapabilitiesGroup, requirements:RequirementsGroup) => { + this.capabilities = this.extractValuesFromMap(capabilities); + this.requirements = this.extractValuesFromMap(requirements); + } + + + private initInstancesMap = ():void => { + + this.capabilitiesInstancesMap = new InstanceCapabilitiesMap(); + _.forEach(this.capabilities, (capability:Capability) => { + if (this.capabilitiesInstancesMap[capability.ownerName]) { + this.capabilitiesInstancesMap[capability.ownerName] = this.capabilitiesInstancesMap[capability.ownerName].concat(capability); + } else { + this.capabilitiesInstancesMap[capability.ownerName] = new Array<Capability>(capability); + } + }); + + this.requirementsInstancesMap = new InstanceRequirementsMap(); + _.forEach(this.requirements, (requirement:Requirement) => { + if (this.requirementsInstancesMap[requirement.ownerName]) { + this.requirementsInstancesMap[requirement.ownerName] = this.requirementsInstancesMap[requirement.ownerName].concat(requirement); + } else { + this.requirementsInstancesMap[requirement.ownerName] = new Array<Requirement>(requirement); + } + }); + } + + private initRequirementsAndCapabilities = (needUpdate?: boolean) => { + + // if instance selected, we take the requirement and capabilities of the instance - always exist because we load them with the graph + if (this.component instanceof FullComponentInstance) { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.setScopeCapabilitiesRequirements(this.component.capabilities, this.component.requirements); + if (this.component.originType === 'VF') { + this.initInstancesMap(); + } + } else { + // if instance not selected, we take the requirement and capabilities of the VF/SERVICE, if not exist we call api + if (needUpdate || !this.component.capabilities || !this.component.requirements) { + this.loadComplexComponentData(); + + } else { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.setScopeCapabilitiesRequirements(this.component.capabilities, this.component.requirements); + this.initInstancesMap(); + } + } + } + + private updateRequirementCapabilities = () => { + if (!this.isComponentInstanceSelected) { + this.loadComplexComponentData(); + } + } + + + + +} + @@ -0,0 +1,20 @@ +<div class="i-sdc-designer-sidebar-capabilities-requirements"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-group"> + <div class="i-sdc-designer-sidebar-section-content-item-relations" + *ngFor="let requirement of requirements"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-name">{{requirement.name}} </div> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-desc">{{requirement.node}} + <div *ngIf="getRelation(requirement) != null"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-indent-box"></div> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-child"> + <span class="i-sdc-designer-sidebar-section-content-item-relations-details-desc">{{getRelation(requirement).type}} <br/></span> + <span class="i-sdc-designer-sidebar-section-content-item-relations-details-name">{{getRelation(requirement).requirementName}}</span> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +
\ No newline at end of file @@ -0,0 +1,40 @@ +import { Component, Input } from '@angular/core'; +import { Component as TopologyTemplate, RelationshipModel, Relationship, Requirement } from "app/models"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { ResourceNamePipe } from "app/ng2/pipes/resource-name.pipe"; + +@Component({ + selector: 'requirement-list', + templateUrl: './requirement-list.component.html' +}) +export class RequirementListComponent { + @Input() component: TopologyTemplate; + @Input() requirements: Array<Requirement>; + @Input() isInstanceSelected:boolean; + + + constructor(private compositionService: CompositionService) { } + + + public getRelation = (requirement:any):any => { + if (this.isInstanceSelected && this.component.componentInstancesRelations) { + let relationItem:Array<RelationshipModel> = _.filter(this.component.componentInstancesRelations, (relation:RelationshipModel) => { + return relation.fromNode === this.component.uniqueId && + _.filter(relation.relationships, (relationship:Relationship) => { + return relationship.relation.requirement == requirement.name && relationship.relation.requirementOwnerId == requirement.ownerId; + }).length; + }); + + if (relationItem && relationItem.length) { + return { + type: requirement.relationship.split('.').pop(), + requirementName: ResourceNamePipe.getDisplayName(this.compositionService.componentInstances[_.map + (this.compositionService.componentInstances, "uniqueId").indexOf(relationItem[0].toNode)].name) + }; + } + } + return null; + }; + +}; + @@ -0,0 +1,15 @@ +<ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header> + <content> + <service-consumption + [parentService]="metadata" + [selectedService]="component" + [selectedServiceInstanceId]="component.uniqueId" + [instancesMappedList]="instancesMappedList" + [parentServiceInputs]="componentInputs" + [instancesCapabilitiesMap]="instancesCapabilitiesMap" + [readonly]="isViewOnly"> + </service-consumption> + </content> +</ng2-expand-collapse> + @@ -0,0 +1,89 @@ + +import { Component, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + CapabilitiesGroup, + Capability, + Component as TopologyTemplate, + ComponentInstance, + FullComponentInstance, + InputBEModel, + InputsGroup, + InterfaceModel, + PropertiesGroup +} from 'app/models'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { SelectedComponentType } from '../../../common/store/graph.actions'; +import { CompositionService } from '../../../composition.service'; + +@Component({ + selector: 'service-consumption-tab', + templateUrl: './service-consumption-tab.component.html', + styleUrls: ['./service-consumption-tab.component.less'], +}) +export class ServiceConsumptionTabComponent { + isComponentInstanceSelected: boolean; + + instancesMappedList: ServiceInstanceObject[]; + componentInstancesProperties: PropertiesGroup; + componentInstancesInputs: InputsGroup; + componentInstancesInterfaces: Map<string, InterfaceModel[]>; + componentInputs: InputBEModel[]; + componentCapabilities: Capability[]; + instancesCapabilitiesMap: Map<string, Capability[]>; + metadata: ComponentMetadata; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: TopologyTemplate | FullComponentInstance; + @Input() input: any; + + constructor(private store: Store, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private eventListenerService: EventListenerService ) {} + ngOnInit() { + this.metadata = this.workspaceService.metadata; + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.initInstances(); + } + + private initInstances = (): void => { + this.topologyTemplateService.getServiceConsumptionData(this.metadata.componentType, this.metadata.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => { + this.componentInstancesProperties = genericResponse.componentInstancesProperties; + this.componentInstancesInputs = genericResponse.componentInstancesInputs; + this.componentInstancesInterfaces = genericResponse.componentInstancesInterfaces; + this.componentInputs = genericResponse.inputs; + this.buildInstancesCapabilitiesMap(genericResponse.componentInstances); + this.updateInstanceAttributes(); + }); + } + + private buildInstancesCapabilitiesMap = (componentInstances: Array<ComponentInstance>): void => { + this.instancesCapabilitiesMap = new Map(); + let flattenCapabilities = []; + _.forEach(componentInstances, (componentInstance) => { + flattenCapabilities = CapabilitiesGroup.getFlattenedCapabilities(componentInstance.capabilities); + this.instancesCapabilitiesMap[componentInstance.uniqueId] = _.filter(flattenCapabilities, cap => cap.properties && cap.ownerId === componentInstance.uniqueId); + }); + } + + private updateInstanceAttributes = (): void => { + if (this.isComponentInstanceSelected && this.componentInstancesProperties) { + this.instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({ + id: coInstance.uniqueId, + name: coInstance.name, + properties: this.componentInstancesProperties[coInstance.uniqueId] || [], + inputs: this.componentInstancesInputs[coInstance.uniqueId] || [], + interfaces: this.componentInstancesInterfaces[coInstance.uniqueId] || [] + })); + } + } + +} @@ -0,0 +1,18 @@ +<ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header> + <content> + <div *ngIf="isComponentInstanceSelected"> + <service-dependencies + [compositeService]="metaData" + [currentServiceInstance]="component" + [selectedInstanceProperties]="selectedInstanceProperties" + [selectedInstanceSiblings]="selectedInstanceSiblings" + [selectedInstanceConstraints]="selectedInstanceConstraints" + [readonly]="isViewOnly" + (dependencyStatus)="notifyDependencyEventsObserver($event)" + (updateRulesListEvent)="updateSelectedInstanceConstraints($event)" + (loadRulesListEvent)="loadConstraints()"> + </service-dependencies> + </div> + </content> +</ng2-expand-collapse> @@ -0,0 +1,3 @@ +:host /deep/ .expand-collapse-content { + padding: 0 0 10px; +}
\ No newline at end of file @@ -0,0 +1,95 @@ + +import { Component, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + CapabilitiesGroup, + Capability, + Component as TopologyTemplate, + ComponentInstance, + FullComponentInstance, + InputBEModel, + InputsGroup, + InterfaceModel, + PropertiesGroup, + PropertyBEModel, +} from 'app/models'; +import { DEPENDENCY_EVENTS } from 'app/utils/constants'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { ConstraintObject } from '../../../../../components/logic/service-dependencies/service-dependencies.component'; +import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { SelectedComponentType } from '../../../common/store/graph.actions'; +import { CompositionService } from '../../../composition.service'; + +@Component({ + selector: 'service-dependencies-tab', + templateUrl: 'service-dependencies-tab.component.html', + styleUrls: ['service-dependencies-tab.component.less'] +}) +export class ServiceDependenciesTabComponent { + isComponentInstanceSelected: boolean; + + selectedInstanceSiblings: ServiceInstanceObject[]; + componentInstancesConstraints: any[]; + selectedInstanceConstraints: ConstraintObject[]; + selectedInstanceProperties: PropertyBEModel[]; + componentInstanceProperties: PropertiesGroup; + metaData: ComponentMetadata; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: FullComponentInstance | TopologyTemplate; + @Input() input: any; + + constructor(private store: Store, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private eventListenerService: EventListenerService) { + } + + ngOnInit() { + this.metaData = this.workspaceService.metadata; + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.initInstancesWithProperties(); + this.loadConstraints(); + this.initInstancesWithProperties(); + } + + public loadConstraints = (): void => { + this.topologyTemplateService.getServiceFilterConstraints(this.metaData.componentType, this.metaData.uniqueId).subscribe((response) => { + this.componentInstancesConstraints = response.nodeFilterData; + }); + } + + public notifyDependencyEventsObserver = (isChecked: boolean): void => { + this.eventListenerService.notifyObservers(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, isChecked); + } + + public updateSelectedInstanceConstraints = (constraintsList:Array<ConstraintObject>):void => { + this.componentInstancesConstraints[this.component.uniqueId].properties = constraintsList; + this.selectedInstanceConstraints = this.componentInstancesConstraints[this.component.uniqueId].properties; + } + + private initInstancesWithProperties = (): void => { + this.topologyTemplateService.getComponentInstanceProperties(this.metaData.componentType, this.metaData.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => { + this.componentInstanceProperties = genericResponse.componentInstancesProperties; + this.updateInstanceAttributes(); + }); + } + + private updateInstanceAttributes = (): void => { + if (this.isComponentInstanceSelected && this.componentInstanceProperties) { + const instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({ + id: coInstance.uniqueId, + name: coInstance.name, + properties: this.componentInstanceProperties[coInstance.uniqueId] || [] + })); + this.selectedInstanceProperties = this.componentInstanceProperties[this.component.uniqueId]; + this.selectedInstanceSiblings = instancesMappedList.filter((coInstance) => coInstance.id !== this.component.uniqueId); + } + } +} @@ -1,50 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. - --> - -<ng2-composition-panel-header - [name]="selectedZoneInstanceName" - [topologyTemplate]="topologyTemplate" - [selectedZoneInstanceType]="selectedZoneInstanceType" - [selectedZoneInstanceId]="selectedZoneInstanceId" - [nonCertified]="nonCertified" - [isViewOnly]="isViewOnly" - [isLoading]="isLoading" -></ng2-composition-panel-header> - -<div class="component-details-panel-tabs"> - <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader> - - <div *ngIf="selectedZoneInstanceType === zoneInstanceType.POLICY"> - <policy-tabs - [topologyTemplate]="topologyTemplate" - [selectedZoneInstanceType]="selectedZoneInstanceType" - [selectedZoneInstanceId]="selectedZoneInstanceId" - [isViewOnly]="isViewOnly" - (isLoading)="setIsLoading($event)" - ></policy-tabs> - </div> - - <div *ngIf="selectedZoneInstanceType === zoneInstanceType.GROUP"> - <group-tabs - [topologyTemplate]="topologyTemplate" - [selectedZoneInstanceType]="selectedZoneInstanceType" - [selectedZoneInstanceId]="selectedZoneInstanceId" - [isViewOnly]="isViewOnly" - (isLoading)="setIsLoading($event)" - ></group-tabs> - </div> - -</div> @@ -1,11 +0,0 @@ -/deep/ -.component-details-panel { - - color: #666666; - font-family: OpenSans-Regular, sans-serif; - font-size: 14px; - - .component-details-panel-tabs { - - } -} @@ -1,60 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, AfterViewInit, SimpleChanges, HostBinding } from "@angular/core"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; -import { TranslateService } from 'app/ng2/shared/translator/translate.service'; -import { ZoneInstanceType } from "app/models/graph/zones/zone-instance"; -import { GroupsService } from "../../../services/groups.service"; -import { PoliciesService } from "../../../services/policies.service"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -import { IZoneService } from "../../../../models/graph/zones/zone"; - -@Component({ - selector: 'ng2-composition-panel', - templateUrl: './panel.component.html', - styleUrls: ['./panel.component.less'], - providers: [TranslateService] -}) -export class CompositionPanelComponent { - - @Input() topologyTemplate: TopologyTemplate; - @Input() selectedZoneInstanceType: ZoneInstanceType; - @Input() selectedZoneInstanceId: string; - @Input() selectedZoneInstanceName: string; - @Input() nonCertified: boolean; - @Input() isViewOnly: boolean; - @Input() isLoading: boolean; - - - @HostBinding('class') classes = 'component-details-panel'; - - private zoneInstanceType = ZoneInstanceType; // Expose ZoneInstanceType to use in template. - - constructor(){ - } - - private setIsLoading = (value):void => { - this.isLoading = value; - } - -} @@ -1,54 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ -import {NgModule} from "@angular/core"; -import {HttpModule} from "@angular/http"; -import {FormsModule} from "@angular/forms"; -import {BrowserModule} from "@angular/platform-browser"; -import {CompositionPanelComponent} from "./panel.component"; -import {CompositionPanelHeaderModule} from "app/ng2/pages/composition/panel/panel-header/panel-header.module"; -import {GroupTabsModule} from "./panel-tabs/groups/group-tabs.module"; -import {PolicyTabsModule} from "./panel-tabs/policies/policy-tabs.module"; -import {SdcUiComponents} from "sdc-ui/lib/angular"; -import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module'; -import {AddElementsModule} from "../../../components/ui/modal/add-elements/add-elements.module"; - -@NgModule({ - declarations: [ - CompositionPanelComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpModule, - CompositionPanelHeaderModule, - PolicyTabsModule, - GroupTabsModule, - UiElementsModule, - AddElementsModule - ], - entryComponents: [ - CompositionPanelComponent - ], - exports: [], - providers: [SdcUiComponents.ModalService] -}) -export class CompositionPanelModule { - -} @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`home component should match current snapshot 1`] = ` +<home-page + $state={[Function Object]} + authService={[Function Object]} + cacheService={[Function Object]} + componentShouldReload={[Function Function]} + homeService={[Function Object]} + importVSPService={[Function Object]} + initFolders={[Function Function]} + isDefaultFilter={[Function Function]} + loaderService={[Function Object]} + modalService={[Function Object]} + modalsHandler={[Function Object]} + sdcConfig={[Function Object]} + sdcMenu={[Function Object]} + translateService={[Function Object]} + updateFilter={[Function Function]} +> + <div + class="sdc-catalog-container" + > + + <top-nav /> + </div> +</home-page> +`; @@ -0,0 +1,93 @@ + +export interface IItemMenu { + +} + +export interface IMenuItemProperties { + text:string; + group:string; + state:string; + dist:string; + groupname:string; + states:Array<any>; +} + +export class FoldersMenu { + private _folders:Array<FoldersItemsMenu> = []; + + constructor(folders:Array<IMenuItemProperties>) { + let self = this; + folders.forEach(function (folder:IMenuItemProperties) { + if (folder.groupname) { + self._folders.push(new FoldersItemsMenuGroup(folder)); + } else { + self._folders.push(new FoldersItemsMenu(folder)); + } + }); + self._folders[0].setSelected(true); + } + + public getFolders():Array<FoldersItemsMenu> { + return this._folders; + } + + public getCurrentFolder():FoldersItemsMenu { + let menuItem:FoldersItemsMenu = undefined; + this.getFolders().forEach(function (tmpFolder:FoldersItemsMenu) { + if (tmpFolder.isSelected()) { + menuItem = tmpFolder; + } + }); + return menuItem; + } + + public setSelected(folder:FoldersItemsMenu):void { + this.getFolders().forEach(function (tmpFolder:FoldersItemsMenu) { + tmpFolder.setSelected(false); + }); + folder.setSelected(true); + } +} + +export class FoldersItemsMenu implements IItemMenu { + public text:string; + public group:string; + public state:string; + public dist:string; + public states:Array<any>; + + private selected:boolean = false; + + constructor(menuProperties:IMenuItemProperties) { + this.text = menuProperties.text; + this.group = menuProperties.group; + this.state = menuProperties.state; + this.states = menuProperties.states; + this.dist = menuProperties.dist; + } + + public isSelected():boolean { + return this.selected; + } + + public setSelected(value:boolean):void { + this.selected = value; + } + + public isGroup():boolean { + return false; + } +} + +export class FoldersItemsMenuGroup extends FoldersItemsMenu { + public groupname:string; + + constructor(menuProperties:IMenuItemProperties) { + super(menuProperties); + this.groupname = menuProperties.groupname; + } + + public isGroup():boolean { + return true; + } +} @@ -0,0 +1,88 @@ +<div class="sdc-catalog-container"> + <div class="w-sdc-main-container" *ngIf="user"> + + <div id="dashboard-main-scroll" infiniteScroll class="w-sdc-main-right-container" (infiniteScroll)="raiseNumberOfElementToDisplay()" [infiniteScrollDistance]="100"> + + <div class='w-sdc-row-flex-items'> + + <!-- ADD Component --> + <div *ngIf="user.role === 'DESIGNER'" class="w-sdc-dashboard-card-new" + (mouseleave)="setDisplayActions(false)" + (mouseover)="setDisplayActions(true)"> + <div class="w-sdc-dashboard-card-new-content" data-tests-id="AddButtonsArea"> + <div class="w-sdc-dashboard-card-new-content-plus" [hidden]="displayActions"></div> + <div class="sdc-dashboard-create-element-container" [hidden]="!displayActions"> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createResourceButton" size="medium" type="secondary" text="Add VF" (click)="openCreateModal('RESOURCE')"></sdc-button> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createCRButton" size="medium" type="secondary" text="Add CR" (click)="createCR()"></sdc-button> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createPNFButton" size="medium" type="secondary" text="Add PNF" (click)="createPNF()"></sdc-button> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createServiceButton" size="medium" type="secondary" text="Add Service" (click)="openCreateModal('SERVICE')"></sdc-button> + </div> + </div> + </div> + + <!-- Import Component --> + <div *ngIf="user.role === 'DESIGNER'" class="w-sdc-dashboard-card-new" + (mouseleave)="setDisplayActions(false)" + (mouseover)="setDisplayActions(true)"> + <div class="w-sdc-dashboard-card-new-content" data-tests-id="importButtonsArea" > + <div class="w-sdc-dashboard-card-import-content-plus" [hidden]="displayActions"></div> + <div class="sdc-dashboard-import-element-container" [hidden]="!displayActions"> + <sdc-button-file-opener + *ngIf="roles[user.role].dashboard.showCreateNew" + size="medium" + type="secondary" + text="Import VFC" + testId="importVFCbutton" + [extensions]="sdcConfig.toscaFileExtension" + (fileUpload)="onImportVfc($event)" + [convertToBase64]="true" + ></sdc-button-file-opener> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" data-tests-id="importButtonsVSP" size="medium" type="secondary" text="Import VSP" (click)="notificationIconCallback()"></sdc-button> + <sdc-button-file-opener + *ngIf="roles[user.role].dashboard.showCreateNew" + size="medium" + type="secondary" + text="Import DCAE" + testId="importDCAE" + [extensions]="sdcConfig.csarFileExtension" + (fileUpload)="onImportVf($event)" + [convertToBase64]="true" + ></sdc-button-file-opener> + </div> + </div> + </div> + + <!-- Tile new --> + <ui-tile *ngFor="let item of homeFilteredSlicedItems" + [component]="item" (onTileClick)="goToComponent(item)"></ui-tile> + <!-- Tile new --> + + </div> + + </div> + + <div class="w-sdc-left-sidebar"> + <div class="i-sdc-left-sidebar-item " + *ngFor="let folder of folders.getFolders()" + [ngClass]="{'category-title': folder.isGroup(), 'selectedLink': folder.isSelected()}"> + + <span *ngIf="folder.isGroup()" class="title-text">{{folder.text}}</span> + <sdc-checkbox *ngIf="!folder.isGroup() && !folder.dist" + [label]="folder.text" + [attr.data-tests-id]="'filter-' + folder.state" + [checked]="homeFilter.selectedStatuses.indexOf(folder.state) !== -1" + (checkedChange)="changeCheckboxesFilter(homeFilter.selectedStatuses, folder.state, $event)"></sdc-checkbox> + + <sdc-checkbox *ngIf="!folder.isGroup() && folder.dist" + [label]="folder.text" + [checked]="homeFilter.distributed.indexOf(folder.dist) !== -1" + (checkedChange)="changeCheckboxesFilter(homeFilter.distributed, folder.dist, $event)"></sdc-checkbox> + <span class="i-sdc-left-sidebar-item-state-count" [attr.data-tests-id]="'count-' + folder.state">{{entitiesCount(folder)}}</span> + </div> + </div> + + </div> + + <top-nav [topLvlSelectedIndex]="0" [version]="version" [searchTerm]="homeFilter.search.filterTerm" (searchTermChange)="changeFilterTerm($event)" [notificationIconCallback]="notificationIconCallback"></top-nav> + +</div> @@ -0,0 +1,126 @@ +@import '../../../../assets/styles/mixins_old'; +@import '../../../../assets/styles/sprite'; +.w-sdc-left-sidebar-nav { + margin-top: 46px; +} + +.w-sdc-main-right-container { + height: 100%; + overflow-y: scroll; +} + +.w-sdc-main-right-container-element { + float: left; + height: 217px; + width: 217px; + margin: 10px; + position: relative; +} + +.w-sdc-main-right-container-element-details-container { + position: absolute; + top: 165px; + left: 50px; +} + +.w-sdc-main-right-container-element-name { + font-weight: bold; +} + +.i-sdc-left-sidebar-item{ + display: flex; + &.category-title .title-text, sdc-checkbox{ + flex-grow: 1; + } + &:not(.category-title).i-sdc-left-sidebar-item-state-count { + line-height: 14px; + } +} + + +//////////////////////////////Cards//////////////////// +.w-sdc-dashboard-card-new { + border: 2px dashed @color_m; + .border-radius(2px); + cursor: pointer; + display: inline-block; + height: 198px; + margin: 11px; + position: relative; + vertical-align: middle; + width: 202px; +} + +.w-sdc-dashboard-card-new-content { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 100%; +} + +.w-sdc-dashboard-card-new-content-plus { + .sprite-new; + .add-icon; + position: relative; + margin-bottom: 20px; + + &:after { + .n_14_m; + content: 'ADD'; + position: absolute; + top: 25px; + left: -3px; + vertical-align: -50%; + } +} + +.w-sdc-dashboard-card-import-content-plus { + .sprite-new; + .import-icon; + position: relative; + margin-bottom: 20px; + + &:after { + .n_14_m; + content: 'IMPORT'; + position: absolute; + top: 25px; + left: -16px; + vertical-align: -50%; + } +} + +.sdc-dashboard-create-element-container, +.sdc-dashboard-import-element-container { + + width: 140px; + + sdc-button, + sdc-button-file-opener { + padding-bottom: 5px; + &:last-child{ + padding-bottom: 0; + } + } + + .import-file{ + position: relative; + file-opener{ + position: absolute; + top: 0; + /deep/ input[type="file"] { + .hand; + filter: alpha(opacity=0); + opacity: 0; + position: absolute; + top: 0; + left: 0; + width: 140px; + height: 36px; + } + } + } +} + + @@ -0,0 +1,270 @@ + +import { SdcConfigToken, ISdcConfig } from "../../config/sdc-config.config"; +import { SdcMenuToken, IAppMenu } from "../../config/sdc-menu.config"; + + +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { HomeComponent } from "./home.component"; +import {ConfigureFn, configureTests} from "../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import { TranslateService } from "../../shared/translator/translate.service"; +import { HomeService, CacheService, AuthenticationService, ImportVSPService } from '../../../../app/services-ng2'; +import { ModalsHandler } from "../../../../app/utils"; +import { SdcUiServices } from "onap-ui-angular"; +import {ComponentType, ResourceType} from "../../../utils/constants"; +import { FoldersMenu, FoldersItemsMenu, FoldersItemsMenuGroup } from './folders'; +import { HomeFilter } from "../../../../app/models/home-filter"; +import {Component} from "../../../models/components/component"; + + + + +describe('home component', () => { + + // const mockedEvent = <MouseEvent>{ target: {} } + let fixture: ComponentFixture<HomeComponent>; + // let eventServiceMock: Partial<EventListenerService>; + + let importVspService: Partial<ImportVSPService>; + let mockStateService; + let modalServiceMock :Partial<SdcUiServices.ModalService>; + let translateServiceMock : Partial<TranslateService>; + let foldersItemsMenuMock; + let homeFilterMock :Partial<HomeFilter>; + let foldersMock; + let loaderServiceMock; + + + beforeEach( + async(() => { + modalServiceMock = { + openWarningModal: jest.fn() + } + + mockStateService = { + // go: jest.fn().mockReturnValue( new Promise.resolve((resolve, reject )=> resolve())) + go: jest.fn() + } + + translateServiceMock = { + translate: jest.fn() + } + + homeFilterMock = { + search: jest.fn, + toUrlParam: jest.fn() + } + + foldersMock = { + setSelected: jest.fn() + } + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [HomeComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: SdcConfigToken, useValue: {"csarFileExtension":"csar", "toscaFileExtension":"yaml,yml"}}, + {provide: SdcMenuToken, useValue: {}}, + {provide: "$state", useValue: mockStateService}, + {provide: HomeService, useValue: {}}, + {provide: AuthenticationService, useValue: {}}, + {provide: CacheService, useValue: {}}, + {provide: TranslateService, useValue: translateServiceMock}, + {provide: ModalsHandler, useValue: {}}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}, + {provide: ImportVSPService, useValue: {}} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(HomeComponent); + }); + }) + ); + + + it('should match current snapshot', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should call on home component openCreateModal with null imported file', () => { + const component = TestBed.createComponent(HomeComponent); + let componentType:string = 'test'; + let importedFile:any = null; + component.componentInstance.openCreateModal(componentType, importedFile); + expect(mockStateService.go).toBeCalledWith('workspace.general', {type: componentType.toLowerCase()}); + }); + + + it('should call on home component openCreateModal with imported file', () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.initEntities = jest.fn(); + let componentType:string = 'test'; + let importedFile:any = 'importedFile'; + component.componentInstance.openCreateModal(componentType, importedFile); + expect(component.componentInstance.initEntities).toBeCalledWith(true); + }); + + + it ('should call on home component onImportVf without file without extension', () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test'}; + let expectedTitle:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS_TITLE"); + let expectedMessage:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS", {"csarFileExtension":"csar"}); + component.componentInstance.onImportVf(file); + expect(modalServiceMock.openWarningModal).toBeCalledWith(expectedTitle, expectedMessage , 'error-invalid-csar-ext'); + }); + + + it ('should call on home component onImportVf with file without extension' , () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test.csar'}; + component.componentInstance.onImportVf(file); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VF + }); + }); + + + it ('should call on home component onImportVfc without file without extension', () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test'}; + let expectedTitle:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS_TITLE"); + let expectedMessage:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS", {"toscaFileExtension":"yaml,yml"}); + component.componentInstance.onImportVfc(file); + expect(modalServiceMock.openWarningModal).toBeCalledWith(expectedTitle, expectedMessage , 'error-invalid-tosca-ext'); + }); + + it ('should call on home component onImportVfc with file without extension' , () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test.yml'}; + component.componentInstance.onImportVfc(file); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VFC + }); + }); + + it ('should call on home component createPNF' , () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.createPNF(); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.PNF + }); + }); + + it ('should call on home component createCR' , () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.createCR(); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.CR + }); + }); + + + it ('should call on home component updateFilter' , () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.homeFilter = homeFilterMock; + component.componentInstance.filterHomeItems = jest.fn(); + component.componentInstance.updateFilter(); + + expect(mockStateService.go).toBeCalledWith('.', homeFilterMock.toUrlParam(), {location: 'replace', notify: false}); + // expect(spy).toHaveBeenCalledTimes(1); + + // let spy = spyOn(homeFilterMock, 'toUrlParam').and.returnValue({ + // 'filter.term': '', + // 'filter.distributed': '', + // 'filter.status':'' + // }); + }); + + // it ('should call on home component setSelectedFolder' , () => { + // const component = TestBed.createComponent(HomeComponent); + // let folderItem:Partial<FoldersItemsMenu> = { text:'someThing'}; + // let folderItem1:number; + // component.componentInstance.folders = foldersMock; + // expect(foldersMock.setSelected).toBeCalledWith(folderItem); + // }); + + // it ('should call on home component goToComponent' , () => { + // const component = TestBed.createComponent(HomeComponent); + // let componentParam:Partial<Component> = { uuid:'someThing', uniqueId:'uniqueID', componentType:'componentType'}; + // component.componentInstance.goToComponent(componentParam); + // expect(loaderServiceMock.activate).toHaveBeenCalled(); + // // expect(mockStateService.go).toBeCalledWith('workspace.general', {id: componentParam.uniqueId, type: componentParam.componentType.toLowerCase()}).then(function(){ + // // loaderServiceMock.deactivate(); + // // }); + // expect(mockStateService.go).toBeCalled(); + // }); + + // it ('should call on home component raiseNumberOfElementToDisplay so numberOfItemToDisplay will be 0' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.raiseNumberOfElementToDisplay(); + // expect(component.componentInstance.numberOfItemToDisplay).toEqual(0); + // }); + // + // it ('should call on home component raiseNumberOfElementToDisplay with min(2,70) so numberOfItemToDisplay will be 2' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.homeItems = ['item1', 'item2']; + // component.componentInstance.numberOfItemToDisplay = 70; + // component.componentInstance.raiseNumberOfElementToDisplay(true); + // expect(component.componentInstance.numberOfItemToDisplay).toEqual(2); + // }); + // + // it ('should call on home component raiseNumberOfElementToDisplay with min(3,35) so numberOfItemToDisplay will be 2 after fullPagesAmount is calculated' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.homeItems = ['item1', 'item2', 'item3']; + // component.componentInstance.numberOfItemToDisplay = 70; + // component.componentInstance.numberOfItemToDisplay = 0; + // component.componentInstance.raiseNumberOfElementToDisplay(false); + // expect(component.componentInstance.numberOfItemToDisplay).toEqual(3); + // }); + // + // + // it ('should call on home component changeFilterTerm' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.changeFilterTerm("testStr"); + // // expect ( "testStr" ).toEqual(homeFilterMock.search.) + // }); + + + + + + + // it ('should call on home component entitiesCount' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.entitiesCount("aaa"); + // expect(mockStateService.go).toBeCalledWith('workspace.general', { + // type: ComponentType.RESOURCE.toLowerCase(), + // resourceType: ResourceType.CR + // }); + // }); + + + // it('should call on home component notificationIconCallback', () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.initEntities = jest.fn(); + // component.componentInstance.notificationIconCallback(); + // expect(mockStateService.go).toBeCalledWith('workspace.general', {}); + // }); + + + + + +});
\ No newline at end of file @@ -0,0 +1,358 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +'use strict'; +import { Component as NgComponent, Inject, OnInit } from '@angular/core'; +import { Component, IConfigRoles, IUserProperties, Resource } from 'app/models'; +import { HomeFilter } from 'app/models/home-filter'; +import { AuthenticationService, CacheService, HomeService } from 'app/services-ng2'; +import { ModalsHandler } from 'app/utils'; +import { SdcUiServices } from 'onap-ui-angular'; +import { CHANGE_COMPONENT_CSAR_VERSION_FLAG, ComponentType, ResourceType } from '../../../utils/constants'; +import { ImportVSPService } from '../../components/modals/onboarding-modal/import-vsp.service'; +import { ISdcConfig, SdcConfigToken } from '../../config/sdc-config.config'; +import { IAppMenu, SdcMenuToken } from '../../config/sdc-menu.config'; +import { EntityFilterPipe } from '../../pipes/entity-filter.pipe'; +import { TranslateService } from '../../shared/translator/translate.service'; +import { FoldersItemsMenu, FoldersItemsMenuGroup, FoldersMenu } from './folders'; + +@NgComponent({ + selector: 'home-page', + templateUrl: './home.component.html', + styleUrls: ['./home.component.less'] +}) +export class HomeComponent implements OnInit { + public numberOfItemToDisplay: number; + public homeItems: Component[]; + public homeFilteredItems: Component[]; + public homeFilteredSlicedItems: Component[]; + public folders: FoldersMenu; + public roles: IConfigRoles; + public user: IUserProperties; + public showTutorial: boolean; + public isFirstTime: boolean; + public version: string; + public homeFilter: HomeFilter; + public vfcmtType: string; + public displayActions: boolean; + + constructor( + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig, + @Inject(SdcMenuToken) public sdcMenu: IAppMenu, + @Inject('$state') private $state: ng.ui.IStateService, + private homeService: HomeService, + private authService: AuthenticationService, + private cacheService: CacheService, + private translateService: TranslateService, + private modalsHandler: ModalsHandler, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private importVSPService: ImportVSPService + ) {} + + ngOnInit(): void { + this.initHomeComponentVars(); + this.initFolders(); + this.initEntities(); + + if (this.$state.params) { + if (this.$state.params.folder) { + const folderName = this.$state.params.folder.replaceAll('_', ' '); + + const selectedFolder = this.folders.getFolders().find((tmpFolder: FoldersItemsMenu) => tmpFolder.text === folderName); + if (selectedFolder) { + this.setSelectedFolder(selectedFolder); + } + // Show the tutorial if needed when the dashboard page is opened.<script src="bower_components/angular-filter/dist/angular-filter.min.js"></script> + // This is called from the welcome page. + } else if (this.$state.params.show === 'tutorial') { + this.showTutorial = true; + this.isFirstTime = true; + } + } + } + + // Open onboarding modal + public notificationIconCallback(): void { + this.importVSPService.openOnboardingModal().subscribe((result) => { + if (!result.previousComponent || result.previousComponent.csarVersion !== result.componentCsar.csarVersion) { + this.cacheService.set(CHANGE_COMPONENT_CSAR_VERSION_FLAG, result.componentCsar.csarVersion); + } + this.$state.go('workspace.general', { + id: result.previousComponent && result.previousComponent.uniqueId, + componentCsar: result.componentCsar, + type: result.type + }); + }); + } + + public onImportVf(file: any): void { + if (file && file.filename) { + // Check that the file has valid extension. + const fileExtension: string = file.filename.split('.').pop(); + if (this.sdcConfig.csarFileExtension.indexOf(fileExtension.toLowerCase()) !== -1) { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VF + }); + } else { + const title: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS_TITLE'); + const message: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS', {extensions: this.sdcConfig.csarFileExtension}); + this.modalService.openWarningModal(title, message, 'error-invalid-csar-ext'); + } + } + } + + public onImportVfc(file: any): void { + if (file && file.filename) { + // Check that the file has valid extension. + const fileExtension: string = file.filename.split('.').pop(); + if (this.sdcConfig.toscaFileExtension.indexOf(fileExtension.toLowerCase()) !== -1) { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VFC + }); + } else { + const title: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS_TITLE'); + const message: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS', {extensions: this.sdcConfig.toscaFileExtension}); + this.modalService.openWarningModal(title, message, 'error-invalid-tosca-ext'); + } + } + } + + public openCreateModal(componentType: string, importedFile: any): void { + if (importedFile) { + this.initEntities(true); // Return from import + } else { + this.$state.go('workspace.general', {type: componentType.toLowerCase()}); + } + } + + public createPNF(): void { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.PNF + }); + } + + public createCR(): void { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.CR + }); + } + + public entitiesCount(folderItem: FoldersItemsMenu): any { + let total: number = 0; + if (folderItem.isGroup()) { + this.folders.getFolders().forEach((tmpFolder: FoldersItemsMenu) => { + if (tmpFolder.group && tmpFolder.group === (folderItem as FoldersItemsMenuGroup).groupname) { + total = total + this._getTotalCounts(tmpFolder); + } + }); + } else { + total = total + this._getTotalCounts(folderItem); + } + return total; + } + + public updateFilter = () => { + this.$state.go('.', this.homeFilter.toUrlParam(), {location: 'replace', notify: false}); + this.filterHomeItems(); + } + + public getCurrentFolderDistributed(): any[] { + const states = []; + if (this.folders) { + const folderItem: FoldersItemsMenu = this.folders.getCurrentFolder(); + if (folderItem.isGroup()) { + this.folders.getFolders().forEach((tmpFolder: FoldersItemsMenu) => { + if (tmpFolder.group && tmpFolder.group === (folderItem as FoldersItemsMenuGroup).groupname) { + this._setStates(tmpFolder, states); + } + }); + } else { + this._setStates(folderItem, states); + } + } + return states; + } + + public setSelectedFolder(folderItem: FoldersItemsMenu): void { + this.folders.setSelected(folderItem); + } + + public goToComponent(component: Component): void { + const loaderService = this.loaderService; + loaderService.activate(); + this.$state.go('workspace.general', {id: component.uniqueId, type: component.componentType.toLowerCase()}).then(() => { + loaderService.deactivate(); + }); + } + + public raiseNumberOfElementToDisplay(recalculate: boolean = false) { + const scrollPageAmount = 35; + if (!this.homeItems) { + this.numberOfItemToDisplay = 0; + } else if (this.homeItems.length > this.numberOfItemToDisplay || recalculate) { + let fullPagesAmount = Math.ceil(this.numberOfItemToDisplay / scrollPageAmount) * scrollPageAmount; + if (!recalculate || fullPagesAmount === 0) { // TODO trigger infiniteScroll to check bottom and fire onBottomHit by itself (sdc-ui) + fullPagesAmount += scrollPageAmount; + } + this.numberOfItemToDisplay = Math.min(this.homeItems.length, fullPagesAmount); + this.homeFilteredSlicedItems = this.homeFilteredItems.slice(0, this.numberOfItemToDisplay); + } + } + + public changeCheckboxesFilter(checkboxesFilterArray: string[], checkboxValue: string, checked?: boolean) { + const checkboxIdx = checkboxesFilterArray.indexOf(checkboxValue); + + checked = (checked !== undefined) ? checked : checkboxIdx === -1; + if (checked && checkboxIdx === -1) { + checkboxesFilterArray.push(checkboxValue); + } else if (!checked && checkboxIdx !== -1) { + checkboxesFilterArray.splice(checkboxIdx, 1); + } + this.updateFilter(); + } + + public changeFilterTerm(filterTerm: string): void { + this.homeFilter.search = { filterTerm }; + this.updateFilter(); + } + + public setDisplayActions(display?: boolean) { + this.displayActions = display !== undefined ? display : !this.displayActions; + } + + private _getTotalCounts(tmpFolder): number { + let total: number = 0; + if (tmpFolder.dist !== undefined) { + const distributions = tmpFolder.dist.split(','); + distributions.forEach((item: any) => { + total = total + this.getEntitiesByStateDist(tmpFolder.state, item).length; + }); + } else { + total = total + this.getEntitiesByStateDist(tmpFolder.state, tmpFolder.dist).length; + } + return total; + } + + private _setStates(tmpFolder, states) { + if (tmpFolder.states !== undefined) { + tmpFolder.states.forEach((item: any) => { + states.push({state: item.state, dist: item.dist}); + }); + } else { + states.push({state: tmpFolder.state, dist: tmpFolder.dist}); + } + } + + private initEntities(reload?: boolean) { + if (reload || this.componentShouldReload()) { + this.loaderService.activate(); + this.homeService.getAllComponents(true).subscribe( + (components: Component[]) => { + this.cacheService.set('breadcrumbsComponentsState', this.$state.current.name); // dashboard + this.cacheService.set('breadcrumbsComponents', components); + this.homeItems = components; + this.loaderService.deactivate(); + this.filterHomeItems(); + }, (error) => { this.loaderService.deactivate(); }); + } else { + this.homeItems = this.cacheService.get('breadcrumbsComponents'); + this.filterHomeItems(); + } + } + + private isDefaultFilter = (): boolean => { + const defaultFilter = new HomeFilter(); + return angular.equals(defaultFilter, this.homeFilter); + } + + private componentShouldReload = (): boolean => { + const breadcrumbsValid: boolean = (this.$state.current.name === this.cacheService.get('breadcrumbsComponentsState') && this.cacheService.contains('breadcrumbsComponents')); + return !breadcrumbsValid || this.isDefaultFilter(); + } + + private getEntitiesByStateDist(state: string, dist: string): Component[] { + let gObj: Component[]; + if (this.homeItems && (state || dist)) { + gObj = this.homeItems.filter((obj: Component) => { + if (dist !== undefined && obj.distributionStatus === dist && obj.lifecycleState === state) { + return true; + } else if (dist === undefined && (obj.lifecycleState === state || obj.distributionStatus === state)) { + return true; + } + return false; + }); + } else { + gObj = []; + } + return gObj; + } + + private filterHomeItems() { + this.homeFilteredItems = this.makeFilteredItems(this.homeItems, this.homeFilter); + this.raiseNumberOfElementToDisplay(true); + this.homeFilteredSlicedItems = this.homeFilteredItems.slice(0, this.numberOfItemToDisplay); + } + + private makeFilteredItems(homeItems: Component[], filter: HomeFilter) { + let filteredComponents: Component[] = homeItems; + + // filter: exclude all resources of type 'vfcmtType': + filteredComponents = filteredComponents.filter((c) => + !c.isResource() || (c as Resource).resourceType.indexOf(this.vfcmtType) === -1); + + // common entity filter + // -------------------------------------------------------------------------- + filteredComponents = EntityFilterPipe.transform(filteredComponents, filter); + + return filteredComponents; + } + + private initFolders = (): void => { + // Note: Do not use SdcUi.ChecklistComponent for folders checkboxes, since from the data structure + // it is not determined that all checkboxes under the same group are managed by the same selectedValues array. + if (this.user) { + this.folders = new FoldersMenu(this.roles[this.user.role].folder); + } + } + + private initHomeComponentVars(): void { + this.version = this.cacheService.get('version'); + this.numberOfItemToDisplay = 0; + this.displayActions = false; + this.user = this.authService.getLoggedinUser(); + this.roles = this.sdcMenu.roles; + this.showTutorial = false; + this.isFirstTime = false; + this.vfcmtType = ResourceType.VFCMT; + + // Checkboxes filter init + this.homeFilter = new HomeFilter(this.$state.params); + + // bind callbacks that are transferred as inputs + this.notificationIconCallback = this.notificationIconCallback.bind(this); + } + +} @@ -0,0 +1,31 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { HomeComponent } from "./home.component"; +import { LayoutModule } from "../../components/layout/layout.module"; +import { UiElementsModule } from "../../components/ui/ui-elements.module"; +import { GlobalPipesModule } from "../../pipes/global-pipes.module"; +import { TranslateModule } from "../../shared/translator/translate.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; + +@NgModule({ + declarations: [ + HomeComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + LayoutModule, + UiElementsModule, + GlobalPipesModule, + TranslateModule + ], + exports: [ + HomeComponent + ], + entryComponents: [ + HomeComponent + ], + providers: [] +}) +export class HomeModule { +} @@ -1,9 +1,9 @@ import {NgModule} from "@angular/core"; import {CommonModule} from "@angular/common"; import {InterfaceOperationComponent} from "./interface-operation.page.component"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; import {TranslateModule} from "app/ng2/shared/translator/translate.module"; +import { SdcUiComponentsModule } from 'onap-ui-angular'; @NgModule({ declarations: [ @@ -1,5 +1,5 @@ <!-- - ~ Copyright © 2016-2018 European Support Limited + ~ Copyright � 2016-2018 European Support Limited ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -1,14 +1,14 @@ import * as _ from "lodash"; -import {Component, Input, Output, ComponentRef, Inject} from '@angular/core'; -import {Component as IComponent} from 'app/models/components/component'; +import { Component, Input, Output, ComponentRef, Inject } from '@angular/core'; +import {Component as IComponent } from 'app/models/components/component'; -import {SdcConfigToken, ISdcConfig} from "app/ng2/config/sdc-config.config"; -import {TranslateService} from "app/ng2/shared/translator/translate.service"; +import { SdcConfigToken, ISdcConfig } from "app/ng2/config/sdc-config.config"; +import {TranslateService } from "app/ng2/shared/translator/translate.service"; -import {Observable} from "rxjs/Observable"; +import {Observable } from "rxjs/Observable"; -import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component'; -import {ModalService} from 'app/ng2/services/modal.service'; +import {ModalComponent } from 'app/ng2/components/ui/modal/modal.component'; +import {ModalService } from 'app/ng2/services/modal.service'; import { InputBEModel, OperationModel, @@ -18,15 +18,19 @@ import { Capability } from 'app/models'; -import {IModalConfig, IModalButtonComponent} from "sdc-ui/lib/angular/modals/models/modal-config"; -import {SdcUiComponents} from "sdc-ui/lib/angular"; -import {ModalButtonComponent} from "sdc-ui/lib/angular/components"; +// import {SdcUiComponents } from 'sdc-ui/lib/angular'; +// import {ModalButtonComponent } from 'sdc-ui/lib/angular/components'; +// import { IModalButtonComponent, IModalConfig } from 'sdc-ui/lib/angular/modals/models/modal-config'; -import {ComponentServiceNg2} from 'app/ng2/services/component-services/component.service'; -import {WorkflowServiceNg2} from 'app/ng2/services/workflow.service'; -import {PluginsService} from "app/ng2/services/plugins.service"; +import {ComponentServiceNg2 } from 'app/ng2/services/component-services/component.service'; +import {PluginsService } from 'app/ng2/services/plugins.service'; +import {WorkflowServiceNg2 } from 'app/ng2/services/workflow.service'; -import {OperationCreatorComponent, OperationCreatorInput} from 'app/ng2/pages/interface-operation/operation-creator/operation-creator.component'; +import { OperationCreatorComponent, OperationCreatorInput } from 'app/ng2/pages/interface-operation/operation-creator/operation-creator.component'; +import { IModalButtonComponent } from 'onap-ui-angular'; +import { ModalButtonComponent } from 'onap-ui-angular'; +import { IModalConfig } from 'onap-ui-angular'; +import { SdcUiServices } from 'onap-ui-angular'; export class UIOperationModel extends OperationModel { isCollapsed: boolean = true; @@ -61,6 +65,7 @@ export class UIOperationModel extends OperationModel { } } +// tslint:disable-next-line:max-classes-per-file class ModalTranslation { CREATE_TITLE: string; EDIT_TITLE: string; @@ -74,7 +79,7 @@ class ModalTranslation { constructor(private TranslateService: TranslateService) { this.TranslateService.languageChangedObservable.subscribe(lang => { this.CREATE_TITLE = this.TranslateService.translate("INTERFACE_CREATE_TITLE"); - this.EDIT_TITLE = this.TranslateService.translate("INTERFACE_EDIT_TITLE"); + this.EDIT_TITLE = this.TranslateService.translate('INTERFACE_EDIT_TITLE'); this.DELETE_TITLE = this.TranslateService.translate("INTERFACE_DELETE_TITLE"); this.CANCEL_BUTTON = this.TranslateService.translate("INTERFACE_CANCEL_BUTTON"); this.SAVE_BUTTON = this.TranslateService.translate("INTERFACE_SAVE_BUTTON"); @@ -85,6 +90,7 @@ class ModalTranslation { } } +// tslint:disable-next-line:max-classes-per-file export class UIInterfaceModel extends InterfaceModel { isCollapsed: boolean = false; @@ -92,7 +98,7 @@ export class UIInterfaceModel extends InterfaceModel { super(interf); this.operations = _.map( this.operations, - operation => new UIOperationModel(operation) + (operation) => new UIOperationModel(operation) ); } @@ -101,6 +107,7 @@ export class UIInterfaceModel extends InterfaceModel { } } +// tslint:disable-next-line:max-classes-per-file @Component({ selector: 'interface-operation', templateUrl: './interface-operation.page.component.html', @@ -110,16 +117,16 @@ export class UIInterfaceModel extends InterfaceModel { export class InterfaceOperationComponent { - interfaces: Array<UIInterfaceModel>; + interfaces: UIInterfaceModel[]; modalInstance: ComponentRef<ModalComponent>; openOperation: OperationModel; enableWorkflowAssociation: boolean; - inputs: Array<InputBEModel>; + inputs: InputBEModel[]; isLoading: boolean; - interfaceTypes:{ [interfaceType: string]: Array<string> }; + interfaceTypes: { [interfaceType: string]: string[] }; modalTranslation: ModalTranslation; workflowIsOnline: boolean; - workflows: Array<any>; + workflows: any[]; capabilities: CapabilitiesGroup; @Input() component: IComponent; @@ -135,7 +142,8 @@ export class InterfaceOperationComponent { private ComponentServiceNg2: ComponentServiceNg2, private WorkflowServiceNg2: WorkflowServiceNg2, private ModalServiceNg2: ModalService, - private ModalServiceSdcUI: SdcUiComponents.ModalService + private ModalServiceSdcUI: SdcUiServices.ModalService + ) { this.enableWorkflowAssociation = sdcConfig.enableWorkflowAssociation; this.modalTranslation = new ModalTranslation(TranslateService); @@ -146,11 +154,11 @@ export class InterfaceOperationComponent { this.workflowIsOnline = !_.isUndefined(this.PluginsService.getPluginByStateUrl('workflowDesigner')); Observable.forkJoin( - this.ComponentServiceNg2.getInterfaces(this.component), + this.ComponentServiceNg2.getInterfaceOperations(this.component), this.ComponentServiceNg2.getComponentInputs(this.component), this.ComponentServiceNg2.getInterfaceTypes(this.component), this.ComponentServiceNg2.getCapabilitiesAndRequirements(this.component.componentType, this.component.uniqueId) - ).subscribe((response: Array<any>) => { + ).subscribe((response: any[]) => { const callback = (workflows) => { this.isLoading = false; this.initInterfaces(response[0].interfaces); @@ -174,36 +182,36 @@ export class InterfaceOperationComponent { }); } - initInterfaces(interfaces: Array<InterfaceModel>): void { - this.interfaces = _.map(interfaces, interf => new UIInterfaceModel(interf)); + initInterfaces(interfaces: InterfaceModel[]): void { + this.interfaces = _.map(interfaces, (interf) => new UIInterfaceModel(interf)); } sortInterfaces(): void { - this.interfaces = _.filter(this.interfaces, interf => interf.operations && interf.operations.length > 0); // remove empty interfaces + this.interfaces = _.filter(this.interfaces, (interf) => interf.operations && interf.operations.length > 0); // remove empty interfaces this.interfaces.sort((a, b) => a.type.localeCompare(b.type)); // sort interfaces alphabetically - _.forEach(this.interfaces, interf => { + _.forEach(this.interfaces, (interf) => { interf.operations.sort((a, b) => a.name.localeCompare(b.name)); // sort operations alphabetically }); } collapseAll(value: boolean = true): void { - _.forEach(this.interfaces, interf => { + _.forEach(this.interfaces, (interf) => { interf.isCollapsed = value; }); } isAllCollapsed(): boolean { - return _.every(this.interfaces, interf => interf.isCollapsed); + return _.every(this.interfaces, (interf) => interf.isCollapsed); } isAllExpanded(): boolean { - return _.every(this.interfaces, interf => !interf.isCollapsed); + return _.every(this.interfaces, (interf) => !interf.isCollapsed); } isListEmpty(): boolean { return _.filter( this.interfaces, - interf => interf.operations && interf.operations.length > 0 + (interf) => interf.operations && interf.operations.length > 0 ).length === 0; } @@ -291,11 +299,6 @@ export class InterfaceOperationComponent { } - private enableOrDisableSaveButton = (shouldEnable: boolean): void => { - let saveButton: ModalButtonComponent = this.ModalServiceSdcUI.getCurrentInstance().getButtonById('saveButton'); - saveButton.disabled = !shouldEnable; - } - onRemoveOperation = (event: Event, operation: OperationModel): void => { event.stopPropagation(); @@ -303,11 +306,11 @@ export class InterfaceOperationComponent { this.ComponentServiceNg2 .deleteInterfaceOperation(this.component, operation) .subscribe(() => { - const curInterf = _.find(this.interfaces, interf => interf.type === operation.interfaceType); - const index = _.findIndex(curInterf.operations, el => el.uniqueId === operation.uniqueId); + const curInterf = _.find(this.interfaces, (interf) => interf.type === operation.interfaceType); + const index = _.findIndex(curInterf.operations, (el) => el.uniqueId === operation.uniqueId); curInterf.operations.splice(index, 1); if (!curInterf.operations.length) { - const interfIndex = _.findIndex(this.interfaces, interf => interf.type === operation.interfaceType); + const interfIndex = _.findIndex(this.interfaces, (interf) => interf.type === operation.interfaceType); this.interfaces.splice(interfIndex, 1); } }); @@ -322,13 +325,18 @@ export class InterfaceOperationComponent { ); } + private enableOrDisableSaveButton = (shouldEnable: boolean): void => { + const saveButton: ModalButtonComponent = this.ModalServiceSdcUI.getCurrentInstance().getButtonById('saveButton'); + saveButton.disabled = !shouldEnable; + } + private createOperation = (operation: OperationModel): void => { this.ComponentServiceNg2.createInterfaceOperation(this.component, operation).subscribe((response: OperationModel) => { this.openOperation = null; let curInterf = _.find( this.interfaces, - interf => interf.type === operation.interfaceType + (interf) => interf.type === operation.interfaceType ); if (!curInterf) { @@ -358,18 +366,19 @@ export class InterfaceOperationComponent { this.ComponentServiceNg2.updateInterfaceOperation(this.component, operation).subscribe((newOperation: OperationModel) => { this.openOperation = null; - let oldOpIndex, oldInterf; - _.forEach(this.interfaces, interf => { - _.forEach(interf.operations, op => { + let oldOpIndex; + let oldInterf; + _.forEach(this.interfaces, (interf) => { + _.forEach(interf.operations, (op) => { if (op.uniqueId === newOperation.uniqueId) { oldInterf = interf; - oldOpIndex = _.findIndex(interf.operations, el => el.uniqueId === op.uniqueId); + oldOpIndex = _.findIndex(interf.operations, (el) => el.uniqueId === op.uniqueId); } }) }); oldInterf.operations.splice(oldOpIndex, 1); - const newInterf = _.find(this.interfaces, interf => interf.type === operation.interfaceType); + const newInterf = _.find(this.interfaces, (interf) => interf.type === operation.interfaceType); const newOpModel = new UIOperationModel(newOperation); newInterf.operations.push(newOpModel); this.sortInterfaces(); @@ -180,6 +180,9 @@ <span class="bold-message">{{ 'EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_1' | translate }}</span> <span>{{ 'EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_2' | translate }}</span> </div> + <div *ngIf="!workflows.length"> + Only <span class="bold-message">certified</span> workflow versions can be assigned to an operation + </div> </div> </div> @@ -11,7 +11,7 @@ font-size: 12px; } - .w-sdc-form .form-item { + .w-sdc-form .i-sdc-form-item { margin-bottom: 15px; } @@ -15,9 +15,9 @@ import { Capability } from 'app/models'; -import {IDropDownOption} from "sdc-ui/lib/angular/form-elements/dropdown/dropdown-models"; import {Tabs, Tab} from "app/ng2/components/ui/tabs/tabs.component"; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import { IDropDownOption } from 'onap-ui-angular'; export class DropDownOption implements IDropDownOption { value: string; @@ -3,10 +3,10 @@ import {CommonModule} from "@angular/common"; import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; import {TranslateModule} from "app/ng2/shared/translator/translate.module"; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { UiElementsModule } from '../../../components/ui/ui-elements.module'; import {OperationCreatorComponent} from "./operation-creator.component"; import {ParamRowComponent} from './param-row/param-row.component'; @@ -1,5 +1,5 @@ <!-- - ~ Copyright © 2016-2018 European Support Limited + ~ Copyright © 2016-2018 European Support Limited ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + <div class="cell field-name"> <ui-element-input *ngIf="!isAssociateWorkflow" @@ -32,6 +32,7 @@ input { height: 30px; + border: none; padding-left: 10px; } @@ -1,4 +1,5 @@ import {Component, Input} from '@angular/core'; +import {PROPERTY_DATA} from "app/utils"; import {DataTypeService} from "app/ng2/services/data-type.service"; import {OperationModel, OperationParameter, InputBEModel, DataTypeModel, Capability} from 'app/models'; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; @@ -36,7 +37,7 @@ export class ParamRowComponent { filteredInputProps: Array<DropdownValue> = []; filteredCapabilitiesProps: Array<{capabilityName: string, properties: Array<DropdownValueType>}> = []; - constructor(private dataTypeService: DataTypeService) {} + constructor(private dataTypeService:DataTypeService) {} ngOnInit() { if (this.isInputParam) { @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="page404"> Page404 </div> @@ -13,8 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <div class="plugin-not-connected"> <div class="plugin-error-message"> <div class="icon-wrapper"> @@ -0,0 +1,4 @@ +<div class="workspace-plugins"> + <plugin-frame (onLoadingDone)="onLoadingDone(plugin)" [plugin]="plugin" [queryParams]="queryParams"></plugin-frame> + <loader [display]="isLoading && plugin.isOnline" ></loader> +</div>
\ No newline at end of file @@ -0,0 +1,2 @@ +.workspace-plugins { +} @@ -0,0 +1,58 @@ +import {Component, Inject} from "@angular/core"; +import {Component as ComponentData, IUserProperties, Plugin} from "app/models"; +import {CacheService, PluginsService} from "app/services-ng2"; + + +@Component({ + selector: 'plugin-context-view', + templateUrl: './plugin-context-view.page.component.html', + styleUrls: ['./plugin-context-view.page.component.less'] +}) + +export class PluginContextViewPageComponent { + plugin: Plugin; + user: IUserProperties; + queryParams: Object; + isLoading: boolean; + show: boolean; + component: ComponentData; + + constructor(@Inject("$stateParams") private _stateParams, + private cacheService: CacheService, + private pluginsService: PluginsService) { + + this.show = false; + this.component = this._stateParams.component; + this.plugin = this.pluginsService.getPluginByStateUrl(_stateParams.path); + this.user = this.cacheService.get('user'); + } + + ngOnInit() { + this.isLoading = true; + + this.queryParams = { + userId: this.user.userId, + userRole: this.user.role, + displayType: "context", + contextType: this.component.getComponentSubType(), + uuid: this.component.uuid, + lifecycleState: this.component.lifecycleState, + isOwner: this.component.lastUpdaterUserId === this.user.userId, + version: this.component.version, + parentUrl: window.location.origin, + eventsClientId: this.plugin.pluginId + }; + + if (this._stateParams.queryParams) { + _.assign(this.queryParams, this._stateParams.queryParams); + } + } + + onLoadingDone(plugin: Plugin) { + if (plugin.pluginId == this.plugin.pluginId) { + this.isLoading = false; + } + } + + +} @@ -12,16 +12,9 @@ ~ 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. - --> - -<sdc-tabs> - <sdc-tab titleIcon="info-circle"> - <group-information-tab [group]="group" [isViewOnly]="isViewOnly" *ngIf="group"></group-information-tab> - </sdc-tab> - <sdc-tab titleIcon="inputs-o"> - <group-members-tab [group]="group" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" (isLoading)="setIsLoading($event)" *ngIf="group"></group-members-tab> - </sdc-tab> - <sdc-tab titleIcon="settings-o"> - <group-properties-tab [group]="group" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" *ngIf="group"></group-properties-tab> - </sdc-tab> -</sdc-tabs> +--> +<div class="sdc-catalog-container plugins-tab-container"> + <top-nav [version]="version" [hideSearch]="true"></top-nav> + <plugin-frame (onLoadingDone)="onLoadingDone(plugin)" [plugin]="plugin" [queryParams]="queryParams"></plugin-frame> + <loader [display]="isLoading"></loader> +</div> @@ -0,0 +1,2 @@ +.plugins-tab-container { +} @@ -0,0 +1,45 @@ +import {Component, Inject} from "@angular/core"; +import {IUserProperties, Plugin} from "app/models"; +import {CacheService, PluginsService} from "app/services-ng2"; + +@Component({ + selector: 'plugin-tab-view', + templateUrl: './plugin-tab-view.page.component.html', + styleUrls: ['./plugin-tab-view.page.component.less'] +}) + +export class PluginTabViewPageComponent { + plugin: Plugin; + user: IUserProperties; + version: string; + queryParams: Object; + isLoading: boolean; + + constructor(@Inject("$stateParams") private _stateParams, + private cacheService: CacheService, + private pluginsService: PluginsService) { + + this.plugin = this.pluginsService.getPluginByStateUrl(_stateParams.path); + this.version = this.cacheService.get('version'); + this.user = this.cacheService.get('user'); + } + + ngOnInit() { + this.isLoading = true; + + this.queryParams = { + userId: this.user.userId, + userRole: this.user.role, + displayType: "tab", + parentUrl: window.location.origin, + eventsClientId: this.plugin.pluginId + }; + + } + + onLoadingDone(plugin: Plugin) { + if (plugin.pluginId == this.plugin.pluginId) { + this.isLoading = false; + } + } +}
\ No newline at end of file @@ -0,0 +1,34 @@ +import {NgModule} from "@angular/core"; +import {PluginContextViewPageComponent} from "./plugin-context-view/plugin-context-view.page.component"; +import {PluginFrameModule} from "../../components/ui/plugin/plugin-frame.module"; +import {CommonModule} from "@angular/common"; +import {UiElementsModule} from "../../components/ui/ui-elements.module"; +import {PluginTabViewPageComponent} from "./plugin-tab-view/plugin-tab-view.page.component"; +import {LayoutModule} from "../../components/layout/layout.module"; +import {HttpModule} from "@angular/http"; + +@NgModule({ + declarations: [ + PluginContextViewPageComponent, + PluginTabViewPageComponent + ], + imports: [ + CommonModule, + PluginFrameModule, + UiElementsModule, + LayoutModule, + HttpModule + ], + exports: [ + PluginContextViewPageComponent, + PluginTabViewPageComponent + ], + entryComponents: [ + PluginContextViewPageComponent, + PluginTabViewPageComponent + ] +}) +export class PluginsModule { + +} + @@ -18,97 +18,95 @@ * ============LICENSE_END========================================================= */ -import * as _ from "lodash"; -import {Component} from '@angular/core'; -import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; -import { DataTypeService } from "app/ng2/services/data-type.service"; -import {PropertyBEModel, DataTypesMap} from "app/models"; -import {PROPERTY_DATA} from "app/utils"; -import {PROPERTY_TYPES} from "../../../../utils"; -import { ModalService } from "app/ng2/services/modal.service"; -import { InstancePropertiesAPIMap } from "app/models/properties-inputs/property-fe-map"; -import { ModalModel } from "app/models/modal"; -import { DataTypeModel } from "app/models/data-types"; - - +import { Component } from '@angular/core'; +import { DataTypesMap, PropertyBEModel } from 'app/models'; +import { DataTypeModel } from 'app/models/data-types'; +import { ModalModel } from 'app/models/modal'; +import { InstancePropertiesAPIMap } from 'app/models/properties-inputs/property-fe-map'; +import { DropdownValue } from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { DataTypeService } from 'app/ng2/services/data-type.service'; +import { ModalService } from 'app/ng2/services/modal.service'; +import { PROPERTY_DATA } from 'app/utils'; +import * as _ from 'lodash'; +import { PROPERTY_TYPES } from '../../../../utils'; @Component({ selector: 'declare-list', templateUrl: './declare-list.component.html', - styleUrls:['./declare-list.component.less'], + styleUrls: ['./declare-list.component.less'], }) export class DeclareListComponent { - typesProperties: Array<DropdownValue>; - typesSchemaProperties: Array<DropdownValue>; + typesProperties: DropdownValue[]; + typesSchemaProperties: DropdownValue[]; propertyModel: PropertyBEModel; - //propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; - //commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; - //types:Array<string>; - dataTypes:DataTypesMap; - isLoading:boolean; - inputsToCreate:InstancePropertiesAPIMap; - propertiesListString:string; + // propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; + // commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; + // types:Array<string>; + dataTypes: DataTypesMap; + isLoading: boolean; + inputsToCreate: InstancePropertiesAPIMap; + propertiesListString: string; privateDataType: DataTypeModel; - constructor(protected dataTypeService:DataTypeService, private modalService:ModalService) {} + constructor(protected dataTypeService: DataTypeService, private modalService: ModalService) {} ngOnInit() { console.log('DeclareListComponent.ngOnInit() - enter'); this.propertyModel = new PropertyBEModel(); this.propertyModel.type = ''; this.propertyModel.schema.property.type = ''; - const types: Array<string> = PROPERTY_DATA.TYPES; //All types - simple type + map + list - this.dataTypes = this.dataTypeService.getAllDataTypes(); //Get all data types in service - const nonPrimitiveTypes :Array<string> = _.filter(Object.keys(this.dataTypes), (type:string)=> { - return types.indexOf(type) == -1; + const types: string[] = PROPERTY_DATA.TYPES; // All types - simple type + map + list + this.dataTypes = this.dataTypeService.getAllDataTypes(); // Get all data types in service + const nonPrimitiveTypes: string[] = _.filter(Object.keys(this.dataTypes), (type: string) => { + return types.indexOf(type) === -1; }); this.typesProperties = _.map(PROPERTY_DATA.TYPES, (type: string) => new DropdownValue(type, type) ); - let typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, + const typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, (type: string) => new DropdownValue(type, type) ); - let nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, + const nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, (type: string) => new DropdownValue(type, - type.replace("org.openecomp.datatypes.heat.","")) + type.replace('org.openecomp.datatypes.heat.',"")) ); - this.typesProperties = _.concat(this.typesProperties,nonPrimitiveTypesValues); - this.typesSchemaProperties = _.concat(typesSimpleProperties,nonPrimitiveTypesValues); - this.typesProperties.unshift(new DropdownValue('','Select Type...')); - this.typesSchemaProperties.unshift(new DropdownValue('','Select Schema Type...')); + this.typesProperties = _.concat(this.typesProperties, nonPrimitiveTypesValues); + this.typesSchemaProperties = _.concat(typesSimpleProperties, nonPrimitiveTypesValues); + this.typesProperties.unshift(new DropdownValue('', 'Select Type...')); + this.typesSchemaProperties.unshift(new DropdownValue('', 'Select Schema Type...')); this.inputsToCreate = this.modalService.currentModal.instance.dynamicContent.instance.input.properties; this.propertiesListString = this.modalService.currentModal.instance.dynamicContent.instance.input.propertyNameList.join(", "); this.privateDataType = new DataTypeModel(null); - this.privateDataType.name = "datatype"; + this.privateDataType.name = 'datatype'; console.log('DeclareListComponent.ngOnInit() - leave'); } - checkFormValidForSubmit(){ - const showSchema:boolean = this.showSchema(); - let isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type)? false : true; - if (!showSchema){ + checkFormValidForSubmit() { + const showSchema: boolean = this.showSchema(); + const isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type) ? false : true; + if (!showSchema) { this.propertyModel.schema.property.type = ''; } return this.propertyModel.name && this.propertyModel.type && isSchemaValid; } - showSchema():boolean { + showSchema(): boolean { return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.propertyModel.type) > -1; - }; + } - onSchemaTypeChange():void { - if (this.propertyModel.type == PROPERTY_TYPES.MAP) { + onSchemaTypeChange(): void { + if (this.propertyModel.type === PROPERTY_TYPES.MAP) { this.propertyModel.value = JSON.stringify({'': null}); - } else if (this.propertyModel.type == PROPERTY_TYPES.LIST) { + } else if (this.propertyModel.type === PROPERTY_TYPES.LIST) { this.propertyModel.value = JSON.stringify([]); } - }; + } } @@ -18,13 +18,13 @@ * ============LICENSE_END========================================================= */ -import {NgModule} from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {DeclareListComponent} from "./declare-list.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {TranslateModule} from "../../../shared/translator/translate.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { DeclareListComponent } from './declare-list.component'; @NgModule({ declarations: [ @@ -19,7 +19,6 @@ */ import { NgModule } from "@angular/core"; import {HierarchyNavigationComponent} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.component"; -import {HttpModule} from "@angular/http"; import {FormsModule} from "@angular/forms"; import {PropertyTableModule} from "../../components/logic/properties-table/property-table.module"; import {UiElementsModule} from "../../components/ui/ui-elements.module"; @@ -46,12 +45,11 @@ import {ComponentModeService} from "../../services/component-services/component- imports: [ BrowserModule, FormsModule, - HttpModule, GlobalPipesModule, PropertyTableModule, PoliciesTableModule, UiElementsModule], - + entryComponents: [PropertiesAssignmentComponent], exports: [ PropertiesAssignmentComponent @@ -18,7 +18,7 @@ <div class="main-content"> <div class="left-column"> <div class="main-tabs-section"> - <tabs #propertyInputTabs tabStyle="round-tabs" (tabChanged)="tabChanged($event)" [hideIndicationOnTabChange]="true"> + <tabs #propertyInputTabs tabStyle="round-tabs" (tabChanged)="tabChanged($event)" [hideIndicationOnTabChange]="true" > <tab tabTitle="Properties"> <properties-table class="properties-table" [fePropertiesMap]="instanceFePropertiesMap" @@ -42,12 +42,13 @@ </tab> <tab tabTitle="Inputs"> <inputs-table class="properties-table" - [readonly]="isReadonly" - [inputs]="inputs | searchFilter:'name':searchQuery" - [instanceNamesMap]="componentInstanceNamesMap" - [isLoading]="loadingInputs" - (deleteInput)="deleteInput($event)" - (inputChanged)="dataChanged($event)"> + [fePropertiesMap]="instanceFePropertiesMap" + [readonly]="isReadonly" + [inputs]="inputs | searchFilter:'name':searchQuery" + [instanceNamesMap]="componentInstanceNamesMap" + [isLoading]="loadingInputs" + (deleteInput)="deleteInput($event)" + (inputChanged)="dataChanged($event)"> </inputs-table> </tab> <tab tabTitle="Policies"> @@ -133,13 +133,12 @@ flex-direction:column; margin: 0px 0 0 1em; overflow-x:auto; - .add-btn{ + .add-btn{ align-self: flex-end; margin-top: 10px; margin-bottom: 19px; } - /deep/ .tabs { border-bottom: solid 1px #d0d0d0; } @@ -19,58 +19,34 @@ */ import * as _ from "lodash"; -import {Component, ViewChild, Inject, TemplateRef} from "@angular/core"; +import { Component, ViewChild, Inject, TemplateRef } from "@angular/core"; import { PropertiesService } from "../../services/properties.service"; -import { - PropertyFEModel, - InstanceFePropertiesMap, - InstanceBePropertiesMap, - InstancePropertiesAPIMap, - Component as ComponentData, - FilterPropertiesAssignmentData, - ModalModel, - ButtonModel, - Capability, - ToscaPresentationData -} from "app/models"; +import { PropertyFEModel, InstanceFePropertiesMap, InstanceBePropertiesMap, InstancePropertiesAPIMap, Component as ComponentData, FilterPropertiesAssignmentData, ModalModel, ButtonModel } from "app/models"; import { ResourceType } from "app/utils"; -import {ComponentServiceNg2} from "../../services/component-services/component.service"; -import {ComponentInstanceServiceNg2} from "../../services/component-instance-services/component-instance.service" -import { - InputBEModel, - InputFEModel, - ComponentInstance, - GroupInstance, - PolicyInstance, - PropertyBEModel, - DerivedFEProperty, - SimpleFlatProperty, - CapabilitiesGroup -} from "app/models"; +import { ComponentServiceNg2 } from "../../services/component-services/component.service"; +import { TopologyTemplateService } from "../../services/component-services/topology-template.service"; +import { ComponentInstanceServiceNg2 } from "../../services/component-instance-services/component-instance.service" +import { InputBEModel, InputFEModel, ComponentInstance, GroupInstance, PolicyInstance, PropertyBEModel, DerivedFEProperty, SimpleFlatProperty } from "app/models"; import { KeysPipe } from 'app/ng2/pipes/keys.pipe'; -import {WorkspaceMode, EVENTS} from "../../../utils/constants"; -import {EventListenerService} from "app/services/event-listener-service" -import {HierarchyDisplayOptions} from "../../components/logic/hierarchy-navigtion/hierarchy-display-options"; -import {FilterPropertiesAssignmentComponent} from "../../components/logic/filter-properties-assignment/filter-properties-assignment.component"; -import {PropertyRowSelectedEvent} from "../../components/logic/properties-table/properties-table.component"; -import {HierarchyNavService} from "./services/hierarchy-nav.service"; -import {PropertiesUtils} from "./services/properties.utils"; -import {ComponentModeService} from "../../services/component-services/component-mode.service"; -import {ModalService} from "../../services/modal.service"; -import {Tabs, Tab} from "../../components/ui/tabs/tabs.component"; -import {InputsUtils} from "./services/inputs.utils"; -import {PropertyCreatorComponent} from "./property-creator/property-creator.component"; -import {DeclareListComponent} from "./declare-list/declare-list.component"; +import { WorkspaceMode, EVENTS, PROPERTY_TYPES } from "../../../utils/constants"; +import { EventListenerService } from "app/services/event-listener-service" +import { HierarchyDisplayOptions } from "../../components/logic/hierarchy-navigtion/hierarchy-display-options"; +import { FilterPropertiesAssignmentComponent } from "../../components/logic/filter-properties-assignment/filter-properties-assignment.component"; +import { PropertyRowSelectedEvent } from "../../components/logic/properties-table/properties-table.component"; +import { HierarchyNavService } from "./services/hierarchy-nav.service"; +import { PropertiesUtils } from "./services/properties.utils"; +import { ComponentModeService } from "../../services/component-services/component-mode.service"; +import { Tabs, Tab } from "../../components/ui/tabs/tabs.component"; +import { InputsUtils } from "./services/inputs.utils"; import { InstanceFeDetails } from "../../../models/instance-fe-details"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -//import { ModalService as ModalServiceSdcUI} from "sdc-ui/lib/angular/modals/modal.service"; -import { IModalButtonComponent } from "sdc-ui/lib/angular/modals/models/modal-config"; +import { SdcUiServices, SdcUiCommon } from "onap-ui-angular"; import { UnsavedChangesComponent } from "app/ng2/components/ui/forms/unsaved-changes/unsaved-changes.component"; -import {Observable} from "rxjs"; -import { DataTypeService } from "app/ng2/services/data-type.service"; -import { DataTypeModel } from "app/models"; -import { PROPERTY_DATA, PROPERTY_TYPES } from "app/utils"; -import { PropertyDeclareAPIModel} from "app/models"; +import {PropertyCreatorComponent} from "./property-creator/property-creator.component"; +import {ModalService} from "../../services/modal.service"; +import { DeclareListComponent } from "./declare-list/declare-list.component"; +import { CapabilitiesGroup, Capability } from "../../../models/capability"; +import { ToscaPresentationData } from "../../../models/tosca-presentation"; +import { Observable } from "rxjs"; const SERVICE_SELF_TITLE = "SELF"; @Component({ @@ -119,7 +95,7 @@ export class PropertiesAssignmentComponent { stateChangeStartUnregister:Function; serviceBePropertiesMap: InstanceBePropertiesMap; serviceBeCapabilitiesPropertiesMap: InstanceBePropertiesMap; - selectedInstance_FlattenCapabilitiesList: Array<Capability>; + selectedInstance_FlattenCapabilitiesList: Capability[]; @ViewChild('hierarchyNavTabs') hierarchyNavTabs: Tabs; @ViewChild('propertyInputTabs') propertyInputTabs: Tabs; @@ -136,12 +112,13 @@ export class PropertiesAssignmentComponent { @Inject("$state") private $state:ng.ui.IStateService, @Inject("Notification") private Notification:any, private componentModeService:ComponentModeService, - private ModalService:ModalService, private EventListenerService:EventListenerService, - private ModalServiceSdcUI: SdcUiComponents.ModalService) { + private ModalServiceSdcUI: SdcUiServices.ModalService, + private ModalService: ModalService, + private keysPipe:KeysPipe, + private topologyTemplateService: TopologyTemplateService) { this.instanceFePropertiesMap = new InstanceFePropertiesMap(); - /* This is the way you can access the component data, please do not use any data except metadata, all other data should be received from the new api calls on the first time than if the data is already exist, no need to call the api again - Ask orit if you have any questions*/ this.component = _stateParams.component; @@ -159,8 +136,8 @@ export class PropertiesAssignmentComponent { this.loadingPolicies = true; this.loadingInstances = true; this.loadingProperties = true; - this.componentServiceNg2 - .getComponentInputsWithProperties(this.component) + this.topologyTemplateService + .getComponentInputsWithProperties(this.component.componentType, this.component.uniqueId) .subscribe(response => { _.forEach(response.inputs, (input: InputBEModel) => { const newInput: InputFEModel = new InputFEModel(input); @@ -169,7 +146,7 @@ export class PropertiesAssignmentComponent { }); this.loadingInputs = false; - }); + }, error => {}); //ignore error this.componentServiceNg2 .getComponentResourcePropertiesData(this.component) .subscribe(response => { @@ -177,6 +154,7 @@ export class PropertiesAssignmentComponent { this.instances = []; this.instances.push(...response.componentInstances); this.instances.push(...response.groupInstances); + this.instances.push(...response.policies); _.forEach(response.policies, (policy: any) => { const newPolicy: InputFEModel = new InputFEModel(policy); @@ -199,7 +177,7 @@ export class PropertiesAssignmentComponent { this.loadingProperties = false; } this.selectFirstInstanceByDefault(); - }); + }, error => { this.loadingInstances = false; }); //ignore error this.stateChangeStartUnregister = this.$scope.$on('$stateChangeStart', (event, toState, toParams) => { // stop if has changed properties @@ -238,14 +216,14 @@ export class PropertiesAssignmentComponent { getServiceProperties(){ this.loadingProperties = false; - this.componentServiceNg2 - .getServiceProperties(this.component) - .subscribe(response => { + this.topologyTemplateService + .getServiceProperties(this.component.uniqueId) + .subscribe((response) => { this.serviceBePropertiesMap = new InstanceBePropertiesMap(); this.serviceBePropertiesMap[this.component.uniqueId] = response; this.processInstancePropertiesResponse(this.serviceBePropertiesMap, false); this.loadingProperties = false; - }, error => { + }, (error) => { this.loadingProperties = false; }); } @@ -267,14 +245,12 @@ export class PropertiesAssignmentComponent { this.loadingProperties = true; if (instance instanceof ComponentInstance) { let instanceBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); - this.selectedInstance_FlattenCapabilitiesList = instance.capabilities ? CapabilitiesGroup.getFlattenedCapabilities(instance.capabilities) : []; if (this.isInput(instance.originType)) { this.componentInstanceServiceNg2 .getComponentInstanceInputs(this.component, instance) .subscribe(response => { instanceBePropertiesMap[instance.uniqueId] = response; this.processInstancePropertiesResponse(instanceBePropertiesMap, true); - this.processInstanceCapabilitiesPropertiesResponse(false); this.loadingProperties = false; }, error => { }); //ignore error @@ -286,7 +262,6 @@ export class PropertiesAssignmentComponent { .subscribe(response => { instanceBePropertiesMap[instance.uniqueId] = response; this.processInstancePropertiesResponse(instanceBePropertiesMap, false); - this.processInstanceCapabilitiesPropertiesResponse(false); this.loadingProperties = false; }, error => { }); //ignore error @@ -305,7 +280,7 @@ export class PropertiesAssignmentComponent { } else if (instance instanceof PolicyInstance) { let instanceBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); this.componentInstanceServiceNg2 - .getComponentPolicyInstanceProperties(this.component, this.selectedInstanceData.uniqueId) + .getComponentPolicyInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId) .subscribe((response) => { instanceBePropertiesMap[instance.uniqueId] = response; this.processInstancePropertiesResponse(instanceBePropertiesMap, false); @@ -480,7 +455,7 @@ export class PropertiesAssignmentComponent { let selectedGroupInstancesProperties: InstanceBePropertiesMap = new InstanceBePropertiesMap(); let selectedPolicyInstancesProperties: InstanceBePropertiesMap = new InstanceBePropertiesMap(); let selectedComponentInstancesInputs: InstanceBePropertiesMap = new InstanceBePropertiesMap(); - let instancesIds = new KeysPipe().transform(this.instanceFePropertiesMap, []); + let instancesIds = this.keysPipe.transform(this.instanceFePropertiesMap, []); angular.forEach(instancesIds, (instanceId: string): void => { let selectedInstanceData: any = this.instances.find(instance => instance.uniqueId == instanceId); @@ -500,7 +475,7 @@ export class PropertiesAssignmentComponent { let inputsToCreate: InstancePropertiesAPIMap = new InstancePropertiesAPIMap(selectedComponentInstancesInputs, selectedComponentInstancesProperties, selectedGroupInstancesProperties, selectedPolicyInstancesProperties); - //move changed capabilities properties from componentInstanceInputsMap obj to componentInstanceProperties + //move changed capabilities properties from componentInstanceInputsMap obj to componentInstanceProperties inputsToCreate.componentInstanceProperties[this.selectedInstanceData.uniqueId] = (inputsToCreate.componentInstanceProperties[this.selectedInstanceData.uniqueId] || []).concat( _.filter( @@ -526,15 +501,14 @@ export class PropertiesAssignmentComponent { } } ); - - this.componentServiceNg2 + this.topologyTemplateService .createInput(this.component, inputsToCreate, this.isSelf()) - .subscribe(response => { + .subscribe((response) => { this.setInputTabIndication(response.length); this.checkedPropertiesCount = 0; this.checkedChildPropertiesCount = 0; _.forEach(response, (input: InputBEModel) => { - let newInput: InputFEModel = new InputFEModel(input); + const newInput: InputFEModel = new InputFEModel(input); this.inputsUtils.resetInputDefaultValue(newInput, input.defaultValue); this.inputs.push(newInput); this.updatePropertyValueAfterDeclare(newInput); @@ -628,8 +602,8 @@ export class PropertiesAssignmentComponent { }; console.log("save button clicked. input=", input); - this.componentServiceNg2 - .createListInput(this.component, input, this.isSelf()) + this.topologyTemplateService + .createListInput(this.component.uniqueId, input, this.isSelf()) .subscribe(response => { this.setInputTabIndication(response.length); this.checkedPropertiesCount = 0; @@ -662,8 +636,8 @@ export class PropertiesAssignmentComponent { console.log('declareListProperties() - leave'); }; - /*** DECLARE PROPERTIES/POLICIES ***/ - declarePropertiesToPolicies = (): void => { + /*** DECLARE PROPERTIES/POLICIES ***/ + declarePropertiesToPolicies = (): void => { let selectedComponentInstancesProperties: InstanceBePropertiesMap = new InstanceBePropertiesMap(); let instancesIds = new KeysPipe().transform(this.instanceFePropertiesMap, []); @@ -679,7 +653,7 @@ export class PropertiesAssignmentComponent { let policiesToCreate: InstancePropertiesAPIMap = new InstancePropertiesAPIMap(null, selectedComponentInstancesProperties, null, null); this.loadingPolicies = true; - this.componentServiceNg2 + this.topologyTemplateService .createPolicy(this.component, policiesToCreate, this.isSelf()) .subscribe(response => { this.setPolicyTabIndication(response.length); @@ -688,7 +662,7 @@ export class PropertiesAssignmentComponent { this.loadingPolicies = false; }); //ignore error - }; + } displayPoliciesAsDeclared = (policies) => { _.forEach(policies, (policy: any) => { @@ -699,8 +673,7 @@ export class PropertiesAssignmentComponent { this.updatePropertyValueAfterDeclare(newPolicy); this.policies.push(policy); }); - }; - + } saveChangedData = ():Promise<(PropertyBEModel|InputBEModel)[]> => { return new Promise((resolve, reject) => { @@ -736,7 +709,8 @@ export class PropertiesAssignmentComponent { if (changedInputsProperties.length && changedCapabilitiesProperties.length) { request = Observable.forkJoin( this.componentInstanceServiceNg2.updateInstanceInputs(this.component, this.selectedInstanceData.uniqueId, changedInputsProperties), - this.componentInstanceServiceNg2.updateInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedCapabilitiesProperties) + this.componentInstanceServiceNg2.updateInstanceProperties(this.component.componentType, this.component.uniqueId, + this.selectedInstanceData.uniqueId, changedCapabilitiesProperties) ); } else if (changedInputsProperties.length) { @@ -745,7 +719,7 @@ export class PropertiesAssignmentComponent { } else if (changedCapabilitiesProperties.length) { request = this.componentInstanceServiceNg2 - .updateInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedCapabilitiesProperties); + .updateInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedCapabilitiesProperties); } handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list @@ -757,19 +731,18 @@ export class PropertiesAssignmentComponent { }; } else { if (this.isSelf()) { - request = this.componentServiceNg2.updateServiceProperties(this.component, _.map(changedProperties, cp => { + request = this.topologyTemplateService.updateServiceProperties(this.component.uniqueId, _.map(changedProperties, cp => { delete cp.constraints; return cp; })); } else { request = this.componentInstanceServiceNg2 - .updateInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedProperties); + .updateInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedProperties); } handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list response.forEach((resProp) => { - const changedProp = <PropertyFEModel>_.find(this.changedData, changedDataObject => changedDataObject.uniqueId === resProp.uniqueId); - this.changedData = _.filter(this.changedData, changedDataObject => changedDataObject.uniqueId !== resProp.uniqueId); + const changedProp = <PropertyFEModel>this.changedData.shift(); this.propertiesUtils.resetPropertyValue(changedProp, resProp.value); }); resolve(response); @@ -778,7 +751,7 @@ export class PropertiesAssignmentComponent { } } else if (this.selectedInstanceData instanceof GroupInstance) { request = this.componentInstanceServiceNg2 - .updateComponentGroupInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedProperties); + .updateComponentGroupInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedProperties); handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list response.forEach((resProp) => { @@ -790,7 +763,7 @@ export class PropertiesAssignmentComponent { }; } else if (this.selectedInstanceData instanceof PolicyInstance) { request = this.componentInstanceServiceNg2 - .updateComponentPolicyInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedProperties); + .updateComponentPolicyInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedProperties); handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list response.forEach((resProp) => { @@ -802,6 +775,7 @@ export class PropertiesAssignmentComponent { }; } } else if (this.isInputsTabSelected) { + const changedInputs: InputBEModel[] = this.changedData.map((changedInput) => { changedInput = <InputFEModel>changedInput; const inputBE = new InputBEModel(changedInput); @@ -925,27 +899,27 @@ export class PropertiesAssignmentComponent { { title: modalTitle, size: 'sm', - type: 'custom', - testId: "id", - + type: SdcUiCommon.ModalType.custom, + testId: "navigate-modal", + buttons: [ - {id: 'cancelButton', text: 'Cancel', type: 'secondary', size: 'xsm', closeModal: true, callback: () => reject()}, - {id: 'discardButton', text: 'Discard', type: 'secondary', size: 'xsm', closeModal: true, callback: () => { this.reverseChangedData(); resolve()}}, - {id: 'saveButton', text: 'Save', type: 'primary', size: 'xsm', closeModal: true, disabled: !this.isValidChangedData, callback: () => this.doSaveChangedData(resolve, reject)} - ] as IModalButtonComponent[] - }, UnsavedChangesComponent, {isValidChangedData: this.isValidChangedData}); + {id: 'cancelButton', text: 'Cancel', type: SdcUiCommon.ButtonType.secondary, size: 'xsm', closeModal: true, callback: () => reject()}, + {id: 'discardButton', text: 'Discard', type: SdcUiCommon.ButtonType.secondary, size: 'xsm', closeModal: true, callback: () => { this.reverseChangedData(); resolve()}}, + {id: 'saveButton', text: 'Save', type: SdcUiCommon.ButtonType.primary, size: 'xsm', closeModal: true, disabled: !this.isValidChangedData, callback: () => this.doSaveChangedData(resolve, reject)} + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig, UnsavedChangesComponent, {isValidChangedData: this.isValidChangedData}); }); } updatePropertyValueAfterDeclare = (input: InputFEModel) => { if (this.instanceFePropertiesMap[input.instanceUniqueId]) { - let instanceName = input.instanceUniqueId.slice(input.instanceUniqueId.lastIndexOf('.') + 1); - let propertyForUpdatindVal = _.find(this.instanceFePropertiesMap[input.instanceUniqueId], (feProperty: PropertyFEModel) => { - return feProperty.uniqueId === input.propertyId && + const instanceName = input.instanceUniqueId.slice(input.instanceUniqueId.lastIndexOf('.') + 1); + const propertyForUpdatindVal = _.find(this.instanceFePropertiesMap[input.instanceUniqueId], (feProperty: PropertyFEModel) => { + return feProperty.name == input.relatedPropertyName && (feProperty.name == input.relatedPropertyName || input.name === instanceName.concat('_').concat(feProperty.name.replace(/[.]/g, '_'))); }); - let inputPath = (input.inputPath && input.inputPath != propertyForUpdatindVal.name) ? input.inputPath : undefined; + const inputPath = (input.inputPath && input.inputPath != propertyForUpdatindVal.name) ? input.inputPath : undefined; propertyForUpdatindVal.setAsDeclared(inputPath); //set prop as declared before assigning value this.propertiesService.disableRelatedProperties(propertyForUpdatindVal, inputPath); this.propertiesUtils.resetPropertyValue(propertyForUpdatindVal, input.relatedPropertyValue, inputPath); @@ -968,7 +942,7 @@ export class PropertiesAssignmentComponent { setPolicyTabIndication = (numPolicies: number): void => { this.propertyInputTabs.setTabIndication('Policies', numPolicies); - }; + } resetUnsavedChangesForInput = (input:InputFEModel) => { this.inputsUtils.resetInputDefaultValue(input, input.defaultValue); @@ -1009,9 +983,9 @@ export class PropertiesAssignmentComponent { deletePolicy = (policy: PolicyInstance) => { this.loadingPolicies = true; - this.componentServiceNg2 + this.topologyTemplateService .deletePolicy(this.component, policy) - .subscribe(response => { + .subscribe((response) => { this.policies = this.policies.filter(policy => policy.uniqueId !== response.uniqueId); //Reload the whole instance for now - TODO: CHANGE THIS after the BE starts returning properties within the response, use commented code below instead! this.changeSelectedInstance(this.selectedInstanceData); @@ -1020,25 +994,25 @@ export class PropertiesAssignmentComponent { }; deleteProperty = (property: PropertyFEModel) => { - let propertyToDelete = new PropertyFEModel(property); + const propertyToDelete = new PropertyFEModel(property); this.loadingProperties = true; - let feMap = this.instanceFePropertiesMap; - this.componentServiceNg2 - .deleteServiceProperty(this.component, propertyToDelete) - .subscribe(response => { + const feMap = this.instanceFePropertiesMap; + this.topologyTemplateService + .deleteServiceProperty(this.component.uniqueId, propertyToDelete) + .subscribe((response) => { const props = feMap[this.component.uniqueId]; props.splice(props.findIndex(p => p.uniqueId === response),1); this.loadingProperties = false; - }, error => { + }, (error) => { this.loadingProperties = false; console.error(error); }); - }; + } /*** addProperty ***/ addProperty = () => { let modalTitle = 'Add Property'; - const modal = this.ModalService.createCustomModal(new ModalModel( + let modal = this.ModalService.createCustomModal(new ModalModel( 'sm', modalTitle, null, @@ -1046,10 +1020,10 @@ export class PropertiesAssignmentComponent { new ButtonModel('Save', 'blue', () => { modal.instance.dynamicContent.instance.isLoading = true; const newProperty: PropertyBEModel = modal.instance.dynamicContent.instance.propertyModel; - this.componentServiceNg2.createServiceProperty(this.component, newProperty) - .subscribe(response => { + this.topologyTemplateService.createServiceProperty(this.component.uniqueId, newProperty) + .subscribe((response) => { modal.instance.dynamicContent.instance.isLoading = false; - let newProp: PropertyFEModel = this.propertiesUtils.convertAddPropertyBAToPropertyFE(response); + const newProp: PropertyFEModel = this.propertiesUtils.convertAddPropertyBAToPropertyFE(response); this.instanceFePropertiesMap[this.component.uniqueId].push(newProp); modal.instance.close(); }, (error) => { @@ -1059,7 +1033,6 @@ export class PropertiesAssignmentComponent { title: 'Failure' }); }); - }, () => !modal.instance.dynamicContent.instance.checkFormValidForSubmit()), new ButtonModel('Cancel', 'outline grey', () => { modal.instance.close(); @@ -1069,24 +1042,23 @@ export class PropertiesAssignmentComponent { )); this.ModalService.addDynamicContentToModal(modal, PropertyCreatorComponent, {}); modal.instance.open(); - }; + } /*** SEARCH RELATED FUNCTIONS ***/ searchPropertiesInstances = (filterData:FilterPropertiesAssignmentData) => { let instanceBePropertiesMap:InstanceBePropertiesMap; this.componentServiceNg2 .filterComponentInstanceProperties(this.component, filterData) - .subscribe(response => { - + .subscribe((response) => { this.processInstancePropertiesResponse(response, false); this.hierarchyPropertiesDisplayOptions.searchText = filterData.propertyName;//mark results in tree this.searchPropertyName = filterData.propertyName;//mark in table this.hierarchyNavTabs.triggerTabChange('Composition'); this.propertiesNavigationData = []; this.displayClearSearch = true; - }, error => {}); //ignore error + }, (error) => {}); //ignore error - }; + } clearSearch = () => { this.instancesNavigationData = this.instances; @@ -1106,5 +1078,6 @@ export class PropertiesAssignmentComponent { private isInput = (instanceType:string):boolean =>{ return instanceType === ResourceType.VF || instanceType === ResourceType.PNF || instanceType === ResourceType.CVFC || instanceType === ResourceType.CR; } + } @@ -1,79 +1,78 @@ -import * as _ from "lodash"; -import {Component} from '@angular/core'; -import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; -import { DataTypeService } from "app/ng2/services/data-type.service"; -import {PropertyBEModel, DataTypesMap} from "app/models"; -import {PROPERTY_DATA} from "app/utils"; -import {PROPERTY_TYPES} from "../../../../utils"; - +import { Component } from '@angular/core'; +import { DataTypesMap, PropertyBEModel } from 'app/models'; +import { DropdownValue } from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { DataTypeService } from 'app/ng2/services/data-type.service'; +import { PROPERTY_DATA } from 'app/utils'; +import * as _ from 'lodash'; +import { PROPERTY_TYPES } from '../../../../utils'; @Component({ selector: 'property-creator', templateUrl: './property-creator.component.html', - styleUrls:['./property-creator.component.less'], + styleUrls: ['./property-creator.component.less'], }) export class PropertyCreatorComponent { - typesProperties: Array<DropdownValue>; - typesSchemaProperties: Array<DropdownValue>; + typesProperties: DropdownValue[]; + typesSchemaProperties: DropdownValue[]; propertyModel: PropertyBEModel; - //propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; - //commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; - //types:Array<string>; - dataTypes:DataTypesMap; - isLoading:boolean; + // propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; + // commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; + // types:Array<string>; + dataTypes: DataTypesMap; + isLoading: boolean; - constructor(protected dataTypeService:DataTypeService) {} + constructor(protected dataTypeService: DataTypeService) {} ngOnInit() { this.propertyModel = new PropertyBEModel(); this.propertyModel.type = ''; this.propertyModel.schema.property.type = ''; - const types: Array<string> = PROPERTY_DATA.TYPES; //All types - simple type + map + list - this.dataTypes = this.dataTypeService.getAllDataTypes(); //Get all data types in service - const nonPrimitiveTypes :Array<string> = _.filter(Object.keys(this.dataTypes), (type:string)=> { - return types.indexOf(type) == -1; + const types: string[] = PROPERTY_DATA.TYPES; // All types - simple type + map + list + this.dataTypes = this.dataTypeService.getAllDataTypes(); // Get all data types in service + const nonPrimitiveTypes: string[] = _.filter(Object.keys(this.dataTypes), (type: string) => { + return types.indexOf(type) === -1; }); this.typesProperties = _.map(PROPERTY_DATA.TYPES, (type: string) => new DropdownValue(type, type) ); - let typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, + const typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, (type: string) => new DropdownValue(type, type) ); - let nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, + const nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, (type: string) => new DropdownValue(type, - type.replace("org.openecomp.datatypes.heat.","")) + type.replace('org.openecomp.datatypes.heat.', '')) ) .sort((a, b) => a.label.localeCompare(b.label)); - this.typesProperties = _.concat(this.typesProperties,nonPrimitiveTypesValues); - this.typesSchemaProperties = _.concat(typesSimpleProperties,nonPrimitiveTypesValues); - this.typesProperties.unshift(new DropdownValue('','Select Type...')); - this.typesSchemaProperties.unshift(new DropdownValue('','Select Schema Type...')); + this.typesProperties = _.concat(this.typesProperties, nonPrimitiveTypesValues); + this.typesSchemaProperties = _.concat(typesSimpleProperties, nonPrimitiveTypesValues); + this.typesProperties.unshift(new DropdownValue('', 'Select Type...')); + this.typesSchemaProperties.unshift(new DropdownValue('', 'Select Schema Type...')); } - checkFormValidForSubmit(){ - const showSchema:boolean = this.showSchema(); - let isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type)? false : true; - if (!showSchema){ + checkFormValidForSubmit() { + const showSchema: boolean = this.showSchema(); + const isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type) ? false : true; + if (!showSchema) { this.propertyModel.schema.property.type = ''; } return this.propertyModel.name && this.propertyModel.type && isSchemaValid; } - showSchema():boolean { + showSchema(): boolean { return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.propertyModel.type) > -1; - }; + } - onSchemaTypeChange():void { - if (this.propertyModel.type == PROPERTY_TYPES.MAP) { + onSchemaTypeChange(): void { + if (this.propertyModel.type === PROPERTY_TYPES.MAP) { this.propertyModel.value = JSON.stringify({'': null}); - } else if (this.propertyModel.type == PROPERTY_TYPES.LIST) { + } else if (this.propertyModel.type === PROPERTY_TYPES.LIST) { this.propertyModel.value = JSON.stringify([]); } - }; + } } @@ -1,10 +1,10 @@ -import {NgModule} from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {PropertyCreatorComponent} from "./property-creator.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {TranslateModule} from "../../../shared/translator/translate.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { PropertyCreatorComponent } from './property-creator.component'; @NgModule({ declarations: [ @@ -48,7 +48,7 @@ export class PropertiesUtils { let newFEProp: PropertyFEModel = new PropertyFEModel(property); //Convert property to FE - this.initValueObjectRef(newFEProp); //initialize valueObj. + this.initValueObjectRef(newFEProp); //initialize valueObj AND creates flattened children propertyFeArray.push(newFEProp); newFEProp.updateExpandedChildPropertyId(newFEProp.name); //display only the first level of children this.dataTypeService.checkForCustomBehavior(newFEProp); @@ -79,8 +79,8 @@ export class PropertiesUtils { return instanceFePropertiesMap; } - public convertAddPropertyBAToPropertyFE = (property: PropertyBEModel):PropertyFEModel => { - let newFEProp: PropertyFEModel = new PropertyFEModel(property); //Convert property to FE + public convertAddPropertyBAToPropertyFE = (property: PropertyBEModel): PropertyFEModel => { + const newFEProp: PropertyFEModel = new PropertyFEModel(property); //Convert property to FE this.initValueObjectRef(newFEProp); newFEProp.updateExpandedChildPropertyId(newFEProp.name); //display only the first level of children this.dataTypeService.checkForCustomBehavior(newFEProp); @@ -108,7 +108,7 @@ export class PropertiesUtils { let tempProps: Array<DerivedFEProperty> = []; let dataTypeObj: DataTypeModel = this.dataTypeService.getDataTypeByTypeName(type); this.dataTypeService.getDerivedDataTypeProperties(dataTypeObj, tempProps, parentName); - return tempProps; + return _.sortBy(tempProps, ['propertiesName']); } /* Sets the valueObj of parent property and its children. @@ -5,7 +5,8 @@ import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; +import { SdcUiComponentsModule } from 'onap-ui-angular'; + @NgModule({ declarations: [ @@ -4,7 +4,7 @@ import {RequirementsEditorComponent} from "./requirements-editor.component"; import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "../../../components/ui/form-components/form-elements.module"; import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; +import { SdcUiComponentsModule } from "onap-ui-angular"; @NgModule({ declarations: [ @@ -14,29 +14,29 @@ * permissions and limitations under the License. */ -import * as _ from "lodash"; import { Component } from '@angular/core'; -import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; import { - Service, - ServiceInstanceObject, - InstanceFePropertiesMap, - InstanceBePropertiesMap, - PropertyBEModel, + Capability, InputBEModel, - OperationModel, + InstanceBePropertiesMap, + InstanceFePropertiesMap, InterfaceModel, - Capability + OperationModel, + PropertyBEModel, + Service } from 'app/models'; -import {ConsumptionInput, ConsumptionInputDetails, ServiceOperation} from 'app/ng2/components/logic/service-consumption/service-consumption.component'; -import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils"; +import { ConsumptionInput, ConsumptionInputDetails, ServiceOperation } from 'app/ng2/components/logic/service-consumption/service-consumption.component'; +import { PropertiesUtils } from 'app/ng2/pages/properties-assignment/services/properties.utils'; +import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service'; import { PROPERTY_DATA } from 'app/utils'; - +import * as _ from 'lodash'; +import { ServiceInstanceObject } from '../../../models/service-instance-properties-and-interfaces'; +import { TopologyTemplateService } from '../../services/component-services/topology-template.service'; @Component({ selector: 'service-consumption-editor', templateUrl: './service-consumption-editor.component.html', - styleUrls:['./service-consumption-editor.component.less'], + styleUrls: ['./service-consumption-editor.component.less'], providers: [] }) @@ -45,27 +45,27 @@ export class ServiceConsumptionCreatorComponent { input: { interfaceId: string, serviceOperationIndex: number, - serviceOperations: Array<ServiceOperation>, + serviceOperations: ServiceOperation[], parentService: Service, selectedService: Service, - parentServiceInputs: Array<InputBEModel>, - selectedServiceProperties: Array<PropertyBEModel>, - selectedServiceInstanceId: String, - selectedInstanceSiblings: Array<ServiceInstanceObject>, - selectedInstanceCapabilitisList: Array<Capability>, - siblingsCapabilitiesList: Map<string, Array<Capability>> + parentServiceInputs: InputBEModel[], + selectedServiceProperties: PropertyBEModel[], + selectedServiceInstanceId: string, + selectedInstanceSiblings: ServiceInstanceObject[], + selectedInstanceCapabilitisList: Capability[], + siblingsCapabilitiesList: Map<string, Capability[]> }; - sourceTypes: Array<any> = []; - serviceOperationsList: Array<ServiceOperation>; + sourceTypes: any[] = []; + serviceOperationsList: ServiceOperation[]; serviceOperation: ServiceOperation; currentIndex: number; isLoading: boolean = false; parentService: Service; selectedService: Service; - selectedServiceInstanceId: String; - parentServiceInputs: Array<InputBEModel>; - selectedServiceProperties: Array<PropertyBEModel>; - changedData: Array<ConsumptionInputDetails> = []; + selectedServiceInstanceId: string; + parentServiceInputs: InputBEModel[]; + selectedServiceProperties: PropertyBEModel[]; + changedData: ConsumptionInputDetails[] = []; inputFePropertiesMap: any = []; SOURCE_TYPES = { @@ -75,7 +75,7 @@ export class ServiceConsumptionCreatorComponent { SERVICE_INPUT_LABEL: 'Service Input' }; - constructor(private serviceServiceNg2: ServiceServiceNg2, private propertiesUtils:PropertiesUtils) {} + constructor(private topologyTemplateService: TopologyTemplateService, private propertiesUtils: PropertiesUtils) {} ngOnInit() { this.serviceOperationsList = this.input.serviceOperations; @@ -112,7 +112,7 @@ export class ServiceConsumptionCreatorComponent { capabilities: [] } ]; - _.forEach(this.input.selectedInstanceSiblings, sib => + _.forEach(this.input.selectedInstanceSiblings, (sib) => this.sourceTypes.push({ label: sib.name, value: sib.id, @@ -128,56 +128,84 @@ export class ServiceConsumptionCreatorComponent { } onExpandAll() { - _.forEach(this.serviceOperation.consumptionInputs, coInput => { + _.forEach(this.serviceOperation.consumptionInputs, (coInput) => { coInput.expanded = true; - }) + }); } onCollapseAll() { - _.forEach(this.serviceOperation.consumptionInputs, coInput => { + _.forEach(this.serviceOperation.consumptionInputs, (coInput) => { coInput.expanded = false; - }) + }); } isAllInputExpanded() { - return _.every(this.serviceOperation.consumptionInputs, coInput => coInput.expanded === true); + return _.every(this.serviceOperation.consumptionInputs, (coInput) => coInput.expanded === true); } isAllInputCollapsed() { - return _.every(this.serviceOperation.consumptionInputs, coInput => coInput.expanded === false); + return _.every(this.serviceOperation.consumptionInputs, (coInput) => coInput.expanded === false); } onChangePage(newIndex) { if (newIndex >= 0 && newIndex < this.serviceOperationsList.length) { this.currentIndex = newIndex; this.serviceOperation = this.serviceOperationsList[newIndex]; - if(!this.serviceOperation.consumptionInputs || this.serviceOperation.consumptionInputs.length === 0) { + if (!this.serviceOperation.consumptionInputs || this.serviceOperation.consumptionInputs.length === 0) { this.initConsumptionInputs(); } this.getComplexPropertiesForCurrentInputsOfOperation(this.serviceOperation.consumptionInputs); } } + checkFormValidForSubmit(): boolean { + return this.isValidInputsValues() && this.isMandatoryFieldsValid(); + } + + checkFormValidForNavigation(): boolean { + return this.isMandatoryFieldsValid() && (this.changedData.length === 0 || this.isValidInputsValues()); + } + + onChange(value: any, isValid: boolean, consumptionInput: ConsumptionInputDetails) { + consumptionInput.updateValidity(isValid); + const dataChangedIndex = this.changedData.findIndex((changedItem) => changedItem.inputId === consumptionInput.inputId); + if (value !== consumptionInput.origVal) { + if (dataChangedIndex === -1) { + this.changedData.push(consumptionInput); + } + } else { + if (dataChangedIndex !== -1) { + this.changedData.splice(dataChangedIndex, 1); + } + } + } + + onComplexPropertyChanged(property, consumptionInput) { + consumptionInput.value = JSON.stringify(property.valueObj); + this.onChange(property.valueObj, property.valueObjIsValid , consumptionInput); + } + private initConsumptionInputs() { this.isLoading = true; - this.serviceServiceNg2.getServiceConsumptionInputs(this.parentService, this.selectedServiceInstanceId, this.input.interfaceId, this.serviceOperation.operation).subscribe((result: Array<ConsumptionInput>) => { + this.topologyTemplateService.getServiceConsumptionInputs(this.parentService.uniqueId, this.selectedServiceInstanceId, + this.input.interfaceId, this.serviceOperation.operation).subscribe((result: ConsumptionInput[]) => { this.isLoading = false; this.serviceOperation.consumptionInputs = this.analyzeCurrentConsumptionInputs(result); this.getComplexPropertiesForCurrentInputsOfOperation(this.serviceOperation.consumptionInputs); - }, err=> { + }, (err) => { this.isLoading = false; }); } - private analyzeCurrentConsumptionInputs(result: Array<any>): Array<ConsumptionInputDetails> { - let inputsResult: Array<ConsumptionInputDetails> = []; - let currentOp = this.serviceOperation.operation; - if(currentOp) { - inputsResult = _.map(result, input => { - let sourceVal = input.source || this.SOURCE_TYPES.STATIC; - let consumptionInputDetails: ConsumptionInputDetails = _.cloneDeep(input); + private analyzeCurrentConsumptionInputs(result: any[]): ConsumptionInputDetails[] { + let inputsResult: ConsumptionInputDetails[] = []; + const currentOp = this.serviceOperation.operation; + if (currentOp) { + inputsResult = _.map(result, (input) => { + const sourceVal = input.source || this.SOURCE_TYPES.STATIC; + const consumptionInputDetails: ConsumptionInputDetails = _.cloneDeep(input); consumptionInputDetails.source = sourceVal; consumptionInputDetails.isValid = true; consumptionInputDetails.expanded = false; - let filteredListsObj = this.getFilteredProps(sourceVal, input.type); + const filteredListsObj = this.getFilteredProps(sourceVal, input.type); consumptionInputDetails.assignValueLabel = this.getAssignValueLabel(sourceVal); consumptionInputDetails.associatedProps = filteredListsObj.associatedPropsList; consumptionInputDetails.associatedInterfaces = filteredListsObj.associatedInterfacesList; @@ -190,15 +218,14 @@ export class ServiceConsumptionCreatorComponent { private onSourceChanged(consumptionInput: ConsumptionInputDetails): void { consumptionInput.assignValueLabel = this.getAssignValueLabel(consumptionInput.source); - let filteredListsObj = this.getFilteredProps(consumptionInput.source, consumptionInput.type); + const filteredListsObj = this.getFilteredProps(consumptionInput.source, consumptionInput.type); consumptionInput.associatedProps = filteredListsObj.associatedPropsList; consumptionInput.associatedInterfaces = filteredListsObj.associatedInterfacesList; consumptionInput.associatedCapabilities = filteredListsObj.associatedCapabilitiesList; - if(consumptionInput.source === this.SOURCE_TYPES.STATIC) { - if(PROPERTY_DATA.SIMPLE_TYPES.indexOf(consumptionInput.type) !== -1) { - consumptionInput.value = consumptionInput.defaultValue || ""; - } - else { + if (consumptionInput.source === this.SOURCE_TYPES.STATIC) { + if (PROPERTY_DATA.SIMPLE_TYPES.indexOf(consumptionInput.type) !== -1) { + consumptionInput.value = consumptionInput.defaultValue || ''; + } else { consumptionInput.value = null; Object.assign(this.inputFePropertiesMap, this.processPropertiesOfComplexTypeInput(consumptionInput)); } @@ -206,9 +233,11 @@ export class ServiceConsumptionCreatorComponent { } private getFilteredProps(sourceVal, inputType) { - let currentSourceObj = this.sourceTypes.find(s => s.value === sourceVal); - let associatedInterfacesList = [], associatedPropsList = [], associatedCapabilitiesPropsList: Array<Capability> = []; - if(currentSourceObj) { + const currentSourceObj = this.sourceTypes.find((s) => s.value === sourceVal); + let associatedInterfacesList = []; + let associatedPropsList = []; + let associatedCapabilitiesPropsList: Capability[] = []; + if (currentSourceObj) { if (currentSourceObj.interfaces) { associatedInterfacesList = this.getFilteredInterfaceOutputs(currentSourceObj, inputType); } @@ -221,31 +250,31 @@ export class ServiceConsumptionCreatorComponent { associatedCapabilitiesPropsList = _.reduce(currentSourceObj.capabilities, (filteredCapsList, capability: Capability) => { - let filteredProps = _.filter(capability.properties, prop => prop.type === inputType); + const filteredProps = _.filter(capability.properties, (prop) => prop.type === inputType); if (filteredProps.length) { - let cap = new Capability(capability); + const cap = new Capability(capability); cap.properties = filteredProps; filteredCapsList.push(cap); } - return filteredCapsList + return filteredCapsList; }, []); } return { - associatedPropsList: associatedPropsList, - associatedInterfacesList: associatedInterfacesList, + associatedPropsList, + associatedInterfacesList, associatedCapabilitiesList: associatedCapabilitiesPropsList - } + }; } private getFilteredInterfaceOutputs(currentSourceObj, inputType) { - let currentServiceOperationId = this.serviceOperation.operation.uniqueId; - let filteredInterfacesList = []; - Object.keys(currentSourceObj.interfaces).map(interfId => { - let interfaceObj: InterfaceModel = new InterfaceModel(currentSourceObj.interfaces[interfId]); - Object.keys(interfaceObj.operations).map(opId => { - if(currentServiceOperationId !== opId) { - let operationObj: OperationModel = interfaceObj.operations[opId]; - let filteredOutputsList = _.filter(operationObj.outputs.listToscaDataDefinition, output => output.type === inputType); + const currentServiceOperationId = this.serviceOperation.operation.uniqueId; + const filteredInterfacesList = []; + Object.keys(currentSourceObj.interfaces).map((interfId) => { + const interfaceObj: InterfaceModel = new InterfaceModel(currentSourceObj.interfaces[interfId]); + Object.keys(interfaceObj.operations).map((opId) => { + if (currentServiceOperationId !== opId) { + const operationObj: OperationModel = interfaceObj.operations[opId]; + const filteredOutputsList = _.filter(operationObj.outputs.listToscaDataDefinition, (output) => output.type === inputType); if (filteredOutputsList.length) { filteredInterfacesList.push({ name: `${interfaceObj.type}.${operationObj.name}`, @@ -259,25 +288,23 @@ export class ServiceConsumptionCreatorComponent { return filteredInterfacesList; } - getAssignValueLabel(selectedSource: string): string { - if(selectedSource === this.SOURCE_TYPES.STATIC || selectedSource === "") { + private getAssignValueLabel(selectedSource: string): string { + if (selectedSource === this.SOURCE_TYPES.STATIC || selectedSource === '') { return this.SOURCE_TYPES.STATIC; - } - else { - if(selectedSource === this.parentService.uniqueId) { //parent is the source + } else { + if (selectedSource === this.parentService.uniqueId) { // parent is the source return this.SOURCE_TYPES.SERVICE_INPUT_LABEL; } return this.SOURCE_TYPES.SERVICE_PROPERTY_LABEL; } } - private isValidInputsValues(): boolean { return this.changedData.length > 0 && this.changedData.every((changedItem) => changedItem.isValid); } private isMandatoryFieldsValid(): boolean { - const invalid: Array<ConsumptionInputDetails> = this.serviceOperation.consumptionInputs.filter(item => + const invalid: ConsumptionInputDetails[] = this.serviceOperation.consumptionInputs.filter((item) => item.required && (item.value === null || typeof item.value === 'undefined' || item.value === '')); if (invalid.length > 0) { return false; @@ -285,45 +312,19 @@ export class ServiceConsumptionCreatorComponent { return true; } - checkFormValidForSubmit(): boolean { - return this.isValidInputsValues() && this.isMandatoryFieldsValid(); - } - - checkFormValidForNavigation(): boolean { - return this.isMandatoryFieldsValid() && (this.changedData.length === 0 || this.isValidInputsValues()); - } - - onChange(value: any, isValid: boolean, consumptionInput: ConsumptionInputDetails) { - consumptionInput.updateValidity(isValid); - const dataChangedIndex = this.changedData.findIndex((changedItem) => changedItem.inputId === consumptionInput.inputId); - if (value !== consumptionInput.origVal) { - if (dataChangedIndex === -1) { - this.changedData.push(consumptionInput); - } - } else { - if (dataChangedIndex !== -1) { - this.changedData.splice(dataChangedIndex, 1); - } - } - } - - private getComplexPropertiesForCurrentInputsOfOperation(opInputs: Array<ConsumptionInput>) { - _.forEach(opInputs, input => { - if(PROPERTY_DATA.SIMPLE_TYPES.indexOf(input.type) === -1 && input.source === this.SOURCE_TYPES.STATIC) { + private getComplexPropertiesForCurrentInputsOfOperation(opInputs: ConsumptionInput[]) { + _.forEach(opInputs, (input) => { + if (PROPERTY_DATA.SIMPLE_TYPES.indexOf(input.type) === -1 && input.source === this.SOURCE_TYPES.STATIC) { Object.assign(this.inputFePropertiesMap, this.processPropertiesOfComplexTypeInput(input)); } }); } private processPropertiesOfComplexTypeInput(input: ConsumptionInput): InstanceFePropertiesMap { - let inputBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); + const inputBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); inputBePropertiesMap[input.name] = [input]; - let originTypeIsVF = false; - return this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren(inputBePropertiesMap, originTypeIsVF); //create flattened children and init values + const originTypeIsVF = false; + return this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren(inputBePropertiesMap, originTypeIsVF); // create flattened children and init values } - onComplexPropertyChanged(property, consumptionInput) { - consumptionInput.value = JSON.stringify(property.valueObj); - this.onChange(property.valueObj, property.valueObjIsValid , consumptionInput); - } -}
\ No newline at end of file +} @@ -1,11 +1,11 @@ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServiceConsumptionCreatorComponent} from "./service-consumption-editor.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {PropertyTableModule} from 'app/ng2/components/logic/properties-table/property-table.module'; -import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { PropertyTableModule } from 'app/ng2/components/logic/properties-table/property-table.module'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from 'app/ng2/shared/translator/translate.module'; +import { ServiceConsumptionCreatorComponent } from './service-consumption-editor.component'; @NgModule({ declarations: [ @@ -25,4 +25,4 @@ import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; providers: [] }) export class ServiceConsumptionCreatorModule { -}
\ No newline at end of file +} @@ -14,20 +14,23 @@ * permissions and limitations under the License. */ import { Component } from '@angular/core'; -import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; -import {ConstraintObjectUI, OPERATOR_TYPES} from 'app/ng2/components/logic/service-dependencies/service-dependencies.component'; -import {ServiceInstanceObject, PropertyBEModel, InputBEModel} from 'app/models'; +import { InputBEModel, PropertyBEModel } from 'app/models'; +import { ConstraintObjectUI, OPERATOR_TYPES } from 'app/ng2/components/logic/service-dependencies/service-dependencies.component'; +import { DropdownValue } from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service'; import { PROPERTY_DATA } from 'app/utils'; -import {DropdownValue} from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { ServiceInstanceObject } from '../../../models/service-instance-properties-and-interfaces'; -export class UIDropDownSourceTypesElement extends DropdownValue{ - options: Array<any>; +export class UIDropDownSourceTypesElement extends DropdownValue { + options: any[]; assignedLabel: string; type: string; - constructor(input?: any){ - if(input) { - let value = input.value || ''; - let label = input.label || ''; + constructor(input?: any) { + if (input) { + const value = input.value || ''; + const label = input.label || ''; + // const hidden = input.hidden || ''; + // const selected = input.selected || ''; super(value, label); this.options = input.options; this.assignedLabel = input.assignedLabel; @@ -36,10 +39,11 @@ export class UIDropDownSourceTypesElement extends DropdownValue{ } } +// tslint:disable-next-line:max-classes-per-file @Component({ selector: 'service-dependencies-editor', templateUrl: './service-dependencies-editor.component.html', - styleUrls:['./service-dependencies-editor.component.less'], + styleUrls: ['./service-dependencies-editor.component.less'], providers: [ServiceServiceNg2] }) @@ -47,44 +51,42 @@ export class ServiceDependenciesEditorComponent { input: { serviceRuleIndex: number, - serviceRules: Array<ConstraintObjectUI>, + serviceRules: ConstraintObjectUI[], compositeServiceName: string, currentServiceName: string, - parentServiceInputs: Array<InputBEModel>, - selectedInstanceProperties: Array<PropertyBEModel>, - operatorTypes: Array<DropdownValue>, - selectedInstanceSiblings: Array<ServiceInstanceObject> + parentServiceInputs: InputBEModel[], + selectedInstanceProperties: PropertyBEModel[], + operatorTypes: DropdownValue[], + selectedInstanceSiblings: ServiceInstanceObject[] }; currentServiceName: string; - selectedServiceProperties: Array<PropertyBEModel>; + selectedServiceProperties: PropertyBEModel[]; selectedPropertyObj: PropertyBEModel; - ddValueSelectedServicePropertiesNames: Array<DropdownValue>; - operatorTypes: Array<DropdownValue>; - sourceTypes: Array<UIDropDownSourceTypesElement> = []; + ddValueSelectedServicePropertiesNames: DropdownValue[]; + operatorTypes: DropdownValue[]; + sourceTypes: UIDropDownSourceTypesElement[] = []; currentRule: ConstraintObjectUI; currentIndex: number; - listOfValuesToAssign: Array<DropdownValue>; - listOfSourceOptions: Array<PropertyBEModel>; + listOfValuesToAssign: DropdownValue[]; + listOfSourceOptions: PropertyBEModel[]; assignedValueLabel: string; - serviceRulesList: Array<ConstraintObjectUI>; - + serviceRulesList: ConstraintObjectUI[]; SOURCE_TYPES = { STATIC: {label: 'Static', value: 'static'}, SERVICE_PROPERTY: {label: 'Service Property', value: 'property'} }; - ngOnInit() { this.currentIndex = this.input.serviceRuleIndex; this.serviceRulesList = this.input.serviceRules; this.currentRule = this.serviceRulesList && this.input.serviceRuleIndex >= 0 ? - this.serviceRulesList[this.input.serviceRuleIndex]: - new ConstraintObjectUI({sourceName: this.SOURCE_TYPES.STATIC.value, sourceType: this.SOURCE_TYPES.STATIC.value, value: "", constraintOperator: OPERATOR_TYPES.EQUAL}); + this.serviceRulesList[this.input.serviceRuleIndex] : + new ConstraintObjectUI({sourceName: this.SOURCE_TYPES.STATIC.value, sourceType: this.SOURCE_TYPES.STATIC.value, value: '', constraintOperator: OPERATOR_TYPES.EQUAL}); this.currentServiceName = this.input.currentServiceName; this.operatorTypes = this.input.operatorTypes; this.selectedServiceProperties = this.input.selectedInstanceProperties; - this.ddValueSelectedServicePropertiesNames = _.map(this.input.selectedInstanceProperties, prop => new DropdownValue(prop.name, prop.name)); + this.ddValueSelectedServicePropertiesNames = _.map(this.input.selectedInstanceProperties, (prop) => new DropdownValue(prop.name, prop.name)); this.initSourceTypes(); this.syncRuleData(); this.updateSourceTypesRelatedValues(); @@ -100,7 +102,7 @@ export class ServiceDependenciesEditorComponent { type: this.SOURCE_TYPES.SERVICE_PROPERTY.value, options: this.input.parentServiceInputs }); - _.forEach(this.input.selectedInstanceSiblings, sib => + _.forEach(this.input.selectedInstanceSiblings, (sib) => this.sourceTypes.push({ label: sib.name, value: sib.name, @@ -112,28 +114,27 @@ export class ServiceDependenciesEditorComponent { } syncRuleData() { - if(!this.currentRule.sourceName && this.currentRule.sourceType === this.SOURCE_TYPES.STATIC.value) { + if (!this.currentRule.sourceName && this.currentRule.sourceType === this.SOURCE_TYPES.STATIC.value) { this.currentRule.sourceName = this.SOURCE_TYPES.STATIC.value; } - this.selectedPropertyObj = _.find(this.selectedServiceProperties, prop => prop.name === this.currentRule.servicePropertyName); + this.selectedPropertyObj = _.find(this.selectedServiceProperties, (prop) => prop.name === this.currentRule.servicePropertyName); this.updateOperatorTypesList(); this.updateSourceTypesRelatedValues(); } updateOperatorTypesList() { if (this.selectedPropertyObj && PROPERTY_DATA.SIMPLE_TYPES_COMPARABLE.indexOf(this.selectedPropertyObj.type) === -1) { - this.operatorTypes = [{label: "=", value: OPERATOR_TYPES.EQUAL}]; + this.operatorTypes = [{label: '=', value: OPERATOR_TYPES.EQUAL}]; this.currentRule.constraintOperator = OPERATOR_TYPES.EQUAL; - } - else { + } else { this.operatorTypes = this.input.operatorTypes; } } updateSourceTypesRelatedValues() { - if(this.currentRule.sourceName) { - let selectedSourceType: UIDropDownSourceTypesElement = this.sourceTypes.find( - t => t.value === this.currentRule.sourceName && t.type === this.currentRule.sourceType + if (this.currentRule.sourceName) { + const selectedSourceType: UIDropDownSourceTypesElement = this.sourceTypes.find( + (t) => t.value === this.currentRule.sourceName && t.type === this.currentRule.sourceType ); this.listOfSourceOptions = selectedSourceType.options || []; this.assignedValueLabel = selectedSourceType.assignedLabel || this.SOURCE_TYPES.STATIC.label; @@ -150,7 +151,7 @@ export class ServiceDependenciesEditorComponent { } onServicePropertyChanged() { - this.selectedPropertyObj = _.find(this.selectedServiceProperties, prop => prop.name === this.currentRule.servicePropertyName); + this.selectedPropertyObj = _.find(this.selectedServiceProperties, (prop) => prop.name === this.currentRule.servicePropertyName); this.updateOperatorTypesList(); this.filterOptionsByType(); this.currentRule.value = ''; @@ -165,11 +166,11 @@ export class ServiceDependenciesEditorComponent { } filterOptionsByType() { - if(!this.selectedPropertyObj) { + if (!this.selectedPropertyObj) { this.listOfValuesToAssign = []; return; } - this.listOfValuesToAssign = this.listOfSourceOptions.reduce((result, op:PropertyBEModel) => { + this.listOfValuesToAssign = this.listOfSourceOptions.reduce((result, op: PropertyBEModel) => { if (op.type === this.selectedPropertyObj.type && (!op.schemaType || op.schemaType === this.selectedPropertyObj.schemaType)) { result.push(new DropdownValue(op.name, op.name)); } @@ -182,11 +183,11 @@ export class ServiceDependenciesEditorComponent { } checkFormValidForSubmit() { - if(!this.serviceRulesList) { //for create modal - let isStatic = this.currentRule.sourceName === this.SOURCE_TYPES.STATIC.value; + if (!this.serviceRulesList) { // for create modal + const isStatic = this.currentRule.sourceName === this.SOURCE_TYPES.STATIC.value; return this.currentRule.isValidRule(isStatic); } - //for update all rules - return this.serviceRulesList.every(rule => rule.isValidRule(rule.sourceName === this.SOURCE_TYPES.STATIC.value)); + // for update all rules + return this.serviceRulesList.every((rule) => rule.isValidRule(rule.sourceName === this.SOURCE_TYPES.STATIC.value)); } -}
\ No newline at end of file +} @@ -1,9 +1,9 @@ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServiceDependenciesEditorComponent} from "./service-dependencies-editor.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { ServiceDependenciesEditorComponent } from './service-dependencies-editor.component'; @NgModule({ declarations: [ @@ -22,4 +22,4 @@ import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; providers: [] }) export class ServiceDependenciesEditorModule { -}
\ No newline at end of file +} @@ -0,0 +1,68 @@ +<!-- + ~ Copyright (C) 2018 AT&T Intellectual Property. 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. +--> +<div class="activity-log"> + <div class="sdc-filter-bar-wrapper"> + <sdc-filter-bar + [placeHolder]="'Search...'" + [testId]="activityLogSearchBar" + (keyup)="updateFilter($event)"> + </sdc-filter-bar> + </div> + <ngx-datatable + columnMode="flex" + [footerHeight]="0" + [limit]="50" + [headerHeight]="40" + [rowHeight]="35" + #activityLogTable + [rows]="activities"> + + <ngx-datatable-column name="Time" [flexGrow]="2" [prop]="'TIMESTAMP'"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.TIMESTAMP | date }} | {{row.TIMESTAMP | date:"HH:mm O"}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Action" [flexGrow]="3" [prop]="'ACTION'"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.ACTION}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Comment" [flexGrow]="5" [prop]="'COMMENT'"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span sdc-tooltip [tooltip-text]="row.COMMENT">{{ row.COMMENT }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Modifier" [flexGrow]="3" [prop]="'MODIFIER'"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.MODIFIER }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Status" [flexGrow]="1" [prop]="'STATUS'"> + <ng-template ngx-datatable-cell-template let-row="row"> + <svg-icon-label + [name]="row.STATUS <= 399 ? 'success' : 'icons_close'" + [mode]="row.STATUS <= 399 ? 'success' : 'error'" + [size]="'medium'" + [label]="row.STATUS" + [labelPlacement]="'left'" + [labelClassName]="'label'" + > + </svg-icon-label> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> + +</div>
\ No newline at end of file @@ -0,0 +1,8 @@ +.sdc-filter-bar-wrapper { + sdc-filter-bar { + flex: 0 0 30%; + } + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +}
\ No newline at end of file @@ -0,0 +1,84 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Observable'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ActivityLogService } from '../../../services/activity-log.service'; +import { WorkspaceService } from '../workspace.service'; +import { ActivityLogComponent } from './activity-log.component'; + +describe('activity log component', () => { + + let fixture: ComponentFixture<ActivityLogComponent>; + let activityLogServiceMock: Partial<ActivityLogService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let componentMetadataMock: ComponentMetadata; + + const mockLogs = '[' + + '{"MODIFIER":"Carlos Santana(m08740)","COMMENT":"comment","STATUS":"200","ACTION":"Checkout","TIMESTAMP":"2018-11-19 13:00:02.388 UTC"},' + + '{"MODIFIER":"John Doe(m08741)","COMMENT":"comment","STATUS":"200","ACTION":"Checkin","TIMESTAMP":"2018-11-20 13:00:02.388 UTC"},' + + '{"MODIFIER":"Jane Doe(m08742)","COMMENT":"comment","STATUS":"200","ACTION":"Checkout","TIMESTAMP":"2018-11-21 13:00:02.388 UTC"}' + + ']'; + + beforeEach( + async(() => { + + componentMetadataMock = new ComponentMetadata(); + componentMetadataMock.uniqueId = 'fake'; + componentMetadataMock.componentType = 'SERVICE'; + + activityLogServiceMock = { + getActivityLog : jest.fn().mockImplementation((type, id) => Observable.of(JSON.parse(mockLogs)) ) + }; + + workspaceServiceMock = { + metadata : componentMetadataMock + }; + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [ActivityLogComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: WorkspaceService, useValue: workspaceServiceMock }, + { provide: ActivityLogService, useValue: activityLogServiceMock }, + { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(ActivityLogComponent); + }); + }) + ); + + it('should see exactly 3 activity logs', () => { + fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.activities.length).toBe(3); + }); + + it('should filter out 1 element when searching', () => { + fixture.componentInstance.ngOnInit(); + + const event = { + target : { + value : 'Checkin' + } + }; + + expect(fixture.componentInstance.activities.length).toBe(3); + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.activities.length).toBe(1); + }); +}); @@ -0,0 +1,48 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Activity } from '../../../../models/activity'; +import { ActivityLogService } from '../../../services/activity-log.service'; +import { WorkspaceService } from '../workspace.service'; + +@Component({ + selector: 'activity-log', + templateUrl: './activity-log.component.html', + styleUrls: ['./activity-log.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class ActivityLogComponent implements OnInit { + + activities: Activity[] = []; + temp: Activity[] = []; + + constructor(private workspaceService: WorkspaceService, + private activityLogService: ActivityLogService, + private loaderService: SdcUiServices.LoaderService) { + } + + ngOnInit(): void { + this.loaderService.activate(); + const componentId: string = this.workspaceService.metadata.uniqueId; + const componentType: string = this.workspaceService.metadata.componentType; + this.activityLogService.getActivityLog(componentType, componentId).subscribe((logs) => { + this.activities = logs; + this.temp = [...logs]; + this.loaderService.deactivate(); + }, (error) => { this.loaderService.deactivate(); }); + } + + updateFilter(event) { + const val = event.target.value.toLowerCase(); + + // filter our data + const temp = this.temp.filter((activity: Activity) => { + return !val || + activity.COMMENT.toLowerCase().indexOf(val) !== -1 || + activity.STATUS.toLowerCase().indexOf(val) !== -1 || + activity.ACTION.toLowerCase().indexOf(val) !== -1 || + activity.MODIFIER.toLowerCase().indexOf(val) !== -1; + }); + + // update the rows + this.activities = temp; + } +} @@ -0,0 +1,28 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {ActivityLogComponent} from "./activity-log.component"; +import {ActivityLogService} from "../../../services/activity-log.service"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; + +@NgModule({ + declarations: [ + ActivityLogComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule + ], + exports: [ + ActivityLogComponent + ], + entryComponents: [ + ActivityLogComponent + ], + providers: [ ActivityLogService ] +}) +export class ActivityLogModule { +} @@ -0,0 +1,104 @@ +<form> + <div class="attr-container"> + + <div class="attr-col"> + <!-- ATTRIBUTE NAME - MANDATORY --> + <div> + <sdc-input + #attributeName + label="Name" + [required]="true" + [(value)]="attributeToEdit.name" + [disabled]="isEdit" + name="attributeName" + testId="attributeName" + [maxLength]="255"> + </sdc-input> + <sdc-validation [validateElement]="attributeName" (validityChanged)="onValidityChange($event, 'name')"> + <sdc-required-validator message="{{'VALIDATION_ERROR_REQUIRED' | translate : { 'field' : 'Name' } }}"></sdc-required-validator> + <sdc-regex-validator message="{{'VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED' | translate }}" [pattern]="validationPatterns.propertyName"></sdc-regex-validator> + </sdc-validation> + </div> + + <!-- ATTRIBUTE DESCRIPTION - OPTIONAL --> + <div> + <sdc-textarea #attributeDescription + [(value)]="attributeToEdit.description" + [required]="false" + testId="description" + [maxLength]="256" + label="Description"> + </sdc-textarea> + </div> + </div> + + <div class="attr-col"> + + <div class="attributeType"> + <!-- ATTRIBUTE TYPE - MANDATORY --> + <sdc-dropdown #attributeType [disabled]="false" label="Type" [required]="true" + [selectedOption]="toDropDownOption(this.attributeToEdit.type)" placeHolder="Choose Type" + [options]="types" (changed)="onTypeSelected($event)"> + <sdc-validation [validateElement]="attributeType" (validityChanged)="onValidityChange($event, 'type')"> + <sdc-required-validator message="'required field'"></sdc-required-validator> + </sdc-validation> + </sdc-dropdown> + </div> + + <!-- ATTRIBUTE DEFAULT VALUE TEXT - OPTIONAL --> + <div *ngIf="attributeToEdit.type != 'boolean'"> + <sdc-input + #defaultValue + [required]="false" + label="Default Value" + [(value)]="attributeToEdit.defaultValue" + [disabled]="false" + name="defaultValue" + testId="defaultValue" + [maxLength]="255" + (valueChange)="defaultValueChanged()"> + </sdc-input> + + <sdc-validation [validateElement]="defaultValue" (validityChanged)="onValidityChange($event, 'defaultValue')"> + <sdc-regex-validator *ngIf="this.attributeToEdit.defaultValue && this.attributeToEdit.defaultValue.length > 0" message="{{ this.defaultValueErrorMessage }}" + [pattern]="defaultValuePattern"></sdc-regex-validator> + <sdc-custom-validator *ngIf="this.attributeToEdit.type == 'map' && this.attributeToEdit.schema.property.type" message="{{ 'PROPERTY_EDIT_MAP_UNIQUE_KEYS' | translate }}" + [callback]="isMapUnique" [disabled]="false"></sdc-custom-validator> + </sdc-validation> + </div> + + <!-- ATTRIBUTE DEFAULT VALUE BOOLEAN- OPTIONAL --> + <div *ngIf="attributeToEdit.type == 'boolean'"> + <sdc-dropdown [disabled]="false" label="Default Value" + [required]="false" + [selectedOption]="toDropDownOption(this.attributeToEdit.defaultValue)" placeHolder="Choose Default Value" + [options]="booleanValues" (changed)="onBooleanDefaultValueSelected($event)"> + + </sdc-dropdown> + </div> + + <div *ngIf="attributeToEdit.type == 'list' || attributeToEdit.type == 'map'"> + <!-- ATTRIBUTE ENTRY SCHEMA - MANDATORY --> + <sdc-dropdown #entrySchema + [disabled]="false" label="Entry Schema" [required]="true" + [selectedOption]="toDropDownOption(this.attributeToEdit.schema.property.type)" placeHolder="Choose Schema Type" + [options]="entrySchemaValues" (changed)="onEntrySchemaTypeSelected($event)"> + <sdc-validation [validateElement]="entrySchema" (validityChanged)="onValidityChange($event, 'entrySchema')"> + <sdc-required-validator message="'required !TODO - CHANGE MESSAGE'"></sdc-required-validator> + </sdc-validation> + </sdc-dropdown> + </div> + + <!-- ATTRIBUTE HIDDEN - OPTIONAL --> + <sdc-checkbox + label="Hidden" + [checked]="attributeToEdit.hidden" + [disabled]="false" + testId="hidden" + (checkedChange)="this.onHiddenCheckboxClicked($event)" + > + </sdc-checkbox> + </div> + </div> + +</form>
\ No newline at end of file @@ -0,0 +1,138 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; +import { InputComponent } from 'onap-ui-angular/dist/form-elements/text-elements/input/input.component'; +import { Subject } from 'rxjs/Subject'; +import { AttributeModel } from '../../../../models/attributes'; +import { ValidationUtils } from '../../../../utils/validation-utils'; +import { CacheService } from '../../../services/cache.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { AttributeOptions } from './attributes-options'; + +@Component({ + selector: 'attribute-modal', + templateUrl: './attribute-modal.component.html', + styleUrls: ['./attributes.component.less'] +}) +export class AttributeModalComponent implements OnInit { + + @ViewChild('defaultValue') validatedInput: InputComponent; + + public readonly types = AttributeOptions.types; // integer, string, boolean etc. + + public readonly booleanValues = AttributeOptions.booleanValues; // true / false + + public readonly entrySchemaValues = AttributeOptions.entrySchemaValues; // integer, string, boolean, float + + public onValidationChange: Subject<boolean> = new Subject(); + + public validationPatterns: any; + public readonly listPattern = ValidationUtils.getPropertyListPatterns(); + public readonly mapPattern = ValidationUtils.getPropertyMapPatterns(); + + // The current effective default value pattern + public defaultValuePattern: string; + public defaultValueErrorMessage: string; + + // Attribute being Edited + public attributeToEdit: AttributeModel; + + constructor(private translateService: TranslateService, private cacheService: CacheService) { + this.validationPatterns = this.cacheService.get('validation').validationPatterns; + } + + ngOnInit() { + this.revalidateDefaultValue(); + } + + onHiddenCheckboxClicked(event: boolean) { + this.attributeToEdit.hidden = event; + } + + onTypeSelected(selectedElement: IDropDownOption) { + if (this.attributeToEdit.type !== selectedElement.value && selectedElement.value === 'boolean') { + this.attributeToEdit.defaultValue = ''; // Clean old value in case we choose change type to boolean + } + this.attributeToEdit.type = selectedElement.value; + this.revalidateDefaultValue(); + } + + onBooleanDefaultValueSelected(selectedElement: IDropDownOption) { + if (this.attributeToEdit.type === 'boolean') { + this.attributeToEdit.defaultValue = selectedElement.value; + } + } + + onEntrySchemaTypeSelected(selectedElement: IDropDownOption) { + this.attributeToEdit.schema.property.type = selectedElement.value; + this.revalidateDefaultValue(); + } + + onValidityChange(isValid: boolean, field: string) { + const typeIsValid = this.attributeToEdit.type && this.attributeToEdit.type.length > 0; // Make sure type is defined + + // Make sure name is defined when other fields are changed + let nameIsValid = true; + if (field !== 'name') { + nameIsValid = this.attributeToEdit.name && this.attributeToEdit.name.length > 0; + } + this.onValidationChange.next(isValid && nameIsValid && typeIsValid); + } + + defaultValueChanged() { + this.revalidateDefaultValue(); + } + + /** + * Utility function for UI that converts a simple value to IDropDownOption + * @param val + * @returns {{value: any; label: any}} + */ + toDropDownOption(val: string) { + return { value : val, label: val }; + } + + public isMapUnique = () => { + if (this.attributeToEdit && this.attributeToEdit.type === 'map' && this.attributeToEdit.defaultValue) { + return ValidationUtils.validateUniqueKeys(this.attributeToEdit.defaultValue); + } + return true; + } + + private revalidateDefaultValue() { + this.setDefaultValuePattern(this.attributeToEdit.type); + setTimeout(() => { + if (this.validatedInput) { + this.validatedInput.onKeyPress(this.attributeToEdit.defaultValue); + } }, 250); + } + + private setDefaultValuePattern(valueType: string) { + const selectedSchemaType = this.attributeToEdit.schema.property.type; + this.defaultValuePattern = '.*'; + switch (valueType) { + case 'float': + this.defaultValuePattern = this.validationPatterns.number; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : 'float' }); + break; + case 'integer': + this.defaultValuePattern = this.validationPatterns.integerNoLeadingZero; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : 'integer' }); + break; + case 'list': + if (selectedSchemaType != undefined) { + this.defaultValuePattern = this.listPattern[selectedSchemaType]; + const listTypeStr = `list of ${selectedSchemaType}s (v1, v2, ...) `; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : listTypeStr }); + } + break; + case 'map': + if (selectedSchemaType != undefined) { + this.defaultValuePattern = this.mapPattern[selectedSchemaType]; + const mapTypeStr = `map of ${selectedSchemaType}s (k1:v1, k2:v2, ...)`; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : mapTypeStr }); + } + break; + } + } + +} @@ -0,0 +1,128 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { AttributeModel } from '../../../../models/attributes'; +import { ValidationUtils } from '../../../../utils/validation-utils'; +import { CacheService } from '../../../services/cache.service'; +import { TranslatePipe } from '../../../shared/translator/translate.pipe'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { AttributeModalComponent } from './attribute-modal.component'; + +describe('attributes modal component', () => { + + let fixture: ComponentFixture<AttributeModalComponent>; + + // Mocks + let translateServiceMock: Partial<TranslateService>; + let cacheServiceMock: Partial<CacheService>; + + const validationPatterns = { + integerNoLeadingZero : 'int_regx', + number : 'number_regx' + }; + + const newAttribute = { + uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null + }; + + beforeEach( + async(() => { + + translateServiceMock = { + translate: jest.fn() + }; + + cacheServiceMock = { + get: jest.fn().mockImplementation((k) => { + return { validationPatterns}; + } ) + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [AttributeModalComponent, TranslatePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: translateServiceMock}, + {provide: CacheService, useValue: cacheServiceMock}, + ] + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributeModalComponent); + }); + }) + ); + + it('test that when hidden is clicked, hidden attribute is set', async () => { + fixture.componentInstance.attributeToEdit = new AttributeModel(); + const hidden = fixture.componentInstance.attributeToEdit.hidden; + fixture.componentInstance.ngOnInit(); + + expect(hidden).toBe(false); + fixture.componentInstance.onHiddenCheckboxClicked(true); + expect(fixture.componentInstance.attributeToEdit.hidden).toBe(true); + }); + + it('test that when type is set to boolean default value is cleared', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + + component.onTypeSelected({ value : 'string', label : 'string'}); + component.attributeToEdit.defaultValue = 'some_value'; + component.onTypeSelected({ value : 'boolean', label : 'boolean'}); + expect(component.attributeToEdit.defaultValue).toBe(''); + + component.onBooleanDefaultValueSelected({ value : 'true', label : 'true'}); + expect(component.attributeToEdit.defaultValue).toBe('true'); + }); + + it('test that when certain type is selected, the correct regex pattern is chosen', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + + // integer + component.onTypeSelected({ value : 'integer', label : 'integer'}); + expect(component.defaultValuePattern).toBe(validationPatterns.integerNoLeadingZero); + + // float + component.onTypeSelected({ value : 'float', label : 'float'}); + expect(component.defaultValuePattern).toBe(validationPatterns.number); + + // list is chosen with no schema, regex pattern is set to default + component.onTypeSelected({ value : 'list', label : 'list'}); + expect(component.defaultValuePattern).toEqual('.*'); + + // schema is set to list of int + component.onEntrySchemaTypeSelected({ value : 'integer', label : 'integer' }); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyListPatterns().integer); + + // schema is set to list of float + component.onEntrySchemaTypeSelected({ value : 'float', label : 'float' }); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyListPatterns().float); + + // map is selected (float schema is still selected from previous line) + component.onTypeSelected({ value : 'map', label : 'map'}); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyMapPatterns().float); + + // change schema type to boolean + component.onEntrySchemaTypeSelected({ value : 'boolean', label : 'boolean' }); + }); + + it('should detect map with non-unique keys', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + expect(component.isMapUnique()).toBe(true); // map is not selected so return true by default + component.onTypeSelected({ value : 'map', label : 'map'}); + component.onEntrySchemaTypeSelected({ value : 'boolean', label : 'boolean' }); + component.attributeToEdit.defaultValue = '"1":true,"2":false'; + expect(component.isMapUnique()).toBe(true); + component.attributeToEdit.defaultValue = '"1":true,"1":false'; + expect(component.isMapUnique()).toBe(false); + }); +}); @@ -0,0 +1,60 @@ +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; + +export class AttributeOptions { + public static readonly types: IDropDownOption[] = [ + { + label: 'integer', + value: 'integer', + }, + { + label: 'string', + value: 'string', + }, + { + label: 'float', + value: 'float' + }, + { + label: 'boolean', + value: 'boolean' + }, + { + label: 'list', + value: 'list' + }, + { + label: 'map', + value: 'map' + } + ]; + + public static readonly booleanValues: IDropDownOption[] = [ + { + label: 'true', + value: 'true', + }, + { + label: 'false', + value: 'false', + } + ]; + + public static readonly entrySchemaValues: IDropDownOption[] = [ + { + label: 'integer', + value: 'integer', + }, + { + label: 'string', + value: 'string', + }, + { + label: 'float', + value: 'float' + }, + { + label: 'boolean', + value: 'boolean' + } + ]; +} @@ -0,0 +1,93 @@ +<!-- + ~ Copyright (C) 2018 AT&T Intellectual Property. 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. +--> +<div class="workspace-attributes"> + + <div class="action-bar-wrapper"> + <svg-icon-label + *ngIf="!(this.isViewOnly$ | async)" + class="add-attr-icon" + [name]="'plus'" + [mode]="'primary'" + [size]="'medium'" + [label]="'Add'" + [labelPlacement]="'right'" + [labelClassName]="'externalActionLabel'" + (click)="onAddAttribute()"> + </svg-icon-label> + </div> + + <ngx-datatable + columnMode="flex" + [footerHeight]="0" + [limit]="50" + [headerHeight]="40" + [rowHeight]="35" + [rows]="attributes" + #componentAttributesTable + (activate)="onExpandRow($event)"> + + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div>{{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="2"> + + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span>{{ row.name }}</span> + </div> + </ng-template> + + </ngx-datatable-column> + + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.type}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column [resizeable]="false" name="Default Value" [flexGrow]="3"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.defaultValue}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column *ngIf="!(this.isViewOnly$ | async)" [resizeable]="false" name="Action" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row" let-rowIndex="rowIndex"> + <div class="actionColumn"> + <svg-icon [clickable]="true" + [mode]="'primary2'" + [name]="'edit-o'" + [size]="'medium'" + (click)="onEditAttribute($event, row)"> + </svg-icon> + <svg-icon [clickable]="true" + [mode]="'primary2'" + [name]="'trash-o'" + (click)="onDeleteAttribute($event, row)" + [size]="'medium'"> + </svg-icon> + </div> + </ng-template> + </ngx-datatable-column> + + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,36 @@ +.action-bar-wrapper { + flex: 0 0 30%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.add-attr-icon{ + cursor: pointer; +} + +.attr-container { + display: flex; + justify-content: space-between; + + .attr-col { + display: flex; + flex-direction: column; + max-width: 275px; + flex-grow: 1; + } + +} + +.attributeType { + margin-bottom: 10px; +} + +sdc-checkbox { + margin-top: 20px; +} + +.actionColumn { + text-align: center; + padding: 5px; +}
\ No newline at end of file @@ -0,0 +1,182 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Rx'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ModalsHandler } from '../../../../utils'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { WorkspaceService } from '../workspace.service'; +import { AttributesComponent } from './attributes.component'; + +describe('attributes component', () => { + + let fixture: ComponentFixture<AttributesComponent>; + + // Mocks + let workspaceServiceMock: Partial<WorkspaceService>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let componentMetadataMock: ComponentMetadata; + let modalServiceMock: Partial<SdcUiServices.ModalService>; + + const mockAttributesList = [ + { uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null }, + { uniqueId : '2', name : 'attr2', description: 'description2', type : 'int', hidden : false, defaultValue : 1, schema : null}, + { uniqueId : '3', name : 'attr3', description: 'description3', type : 'double', hidden : false, defaultValue : 1.0, schema : null}, + { uniqueId : '4', name : 'attr4', description: 'description4', type : 'boolean', hidden : false, defaultValue : true, schema : null}, + ]; + + const newAttribute = { + uniqueId : '5', name : 'attr5', description: 'description5', type : 'string', hidden : false, defaultValue : 'val5', schema : null + }; + const updatedAttribute = { + uniqueId : '2', name : 'attr2', description: 'description_new', type : 'string', hidden : false, defaultValue : 'new_val2', schema : null + }; + const errorAttribute = { + uniqueId : '99', name : 'attr99', description: 'description_error', type : 'string', hidden : false, defaultValue : 'error', schema : null + }; + + beforeEach( + async(() => { + + componentMetadataMock = new ComponentMetadata(); + componentMetadataMock.uniqueId = 'fake'; + componentMetadataMock.componentType = 'VL'; + + topologyTemplateServiceMock = { + getComponentAttributes: jest.fn().mockResolvedValue({ attributes : mockAttributesList }), + addAttributeAsync: jest.fn().mockImplementation( + (compType, cUid, attr) => { + if (attr === errorAttribute) { + return Observable.throwError('add_error').toPromise(); + } else { + return Observable.of(newAttribute).toPromise(); + } + } + ), + updateAttributeAsync: jest.fn().mockImplementation( + (compType, cUid, attr) => { + if (attr === errorAttribute) { + return Observable.throwError('update_error').toPromise(); + } else { + return Observable.of(updatedAttribute).toPromise(); + } + } + ), + deleteAttributeAsync: jest.fn().mockImplementation((cid, ctype, attr) => Observable.of(attr)) + }; + + workspaceServiceMock = { + metadata: componentMetadataMock + }; + + const customModalInstance = { innerModalContent: { instance: { onValidationChange: { subscribe: jest.fn()}}}}; + + modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => customModalInstance) + }; + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [AttributesComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: ModalsHandler, useValue: {}}, + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributesComponent); + }); + }) + ); + + it('should see exactly 1 attributes on init', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + }); + + it('should see exactly 5 attributes when adding', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + + await fixture.componentInstance.addOrUpdateAttribute(newAttribute, false); + expect(fixture.componentInstance.getAttributes().length).toEqual(5); + }); + + it('should see exactly 3 attributes when deleting', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + const attrToDelete = mockAttributesList[0]; + expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(1); + await fixture.componentInstance.deleteAttribute(attrToDelete); + expect(fixture.componentInstance.getAttributes().length).toEqual(3); + expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(0); + }); + + it('should see updated attribute', async () => { + await fixture.componentInstance.asyncInitComponent(); + + await fixture.componentInstance.addOrUpdateAttribute(updatedAttribute, true); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + const attribute = fixture.componentInstance.getAttributes().filter( (attr) => { + return attr.uniqueId === updatedAttribute.uniqueId; + })[0]; + expect(attribute.description).toEqual( 'description_new'); + }); + + it('Add fails, make sure loader is deactivated and attribute is not added', async () => { + await fixture.componentInstance.asyncInitComponent(); + const numAttributes = fixture.componentInstance.getAttributes().length; + await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, false); // Add + expect(loaderServiceMock.deactivate).toHaveBeenCalled(); + expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); + }); + + it('Update fails, make sure loader is deactivated', async () => { + await fixture.componentInstance.asyncInitComponent(); + const numAttributes = fixture.componentInstance.getAttributes().length; + await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, true); // Add + expect(loaderServiceMock.deactivate).toHaveBeenCalled(); + expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); + }); + + it('on delete modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + const event = { stopPropagation: jest.fn() }; + fixture.componentInstance.onDeleteAttribute(event, fixture.componentInstance.getAttributes()[0]); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(modalServiceMock.openInfoModal).toHaveBeenCalled(); + }); + + it('on add modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + fixture.componentInstance.onAddAttribute(); + expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); + }); + + it('on edit modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + const event = { stopPropagation: jest.fn() }; + fixture.componentInstance.onEditAttribute(event, fixture.componentInstance.getAttributes()[0]); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); + }); +}); @@ -0,0 +1,188 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { IAttributeModel } from 'app/models'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component'; +import { AttributeModel } from '../../../../models'; +import { Resource } from '../../../../models'; +import { ModalsHandler } from '../../../../utils'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { WorkspaceService } from '../workspace.service'; +import { AttributeModalComponent } from './attribute-modal.component'; + +@Component({ + selector: 'attributes', + templateUrl: './attributes.component.html', + styleUrls: ['./attributes.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class AttributesComponent implements OnInit { + + @Select(WorkspaceState.isViewOnly) + isViewOnly$: boolean; + + @ViewChild('componentAttributesTable') + private table: any; + + private componentType: string; + private componentUid: string; + + private attributes: IAttributeModel[] = []; + private temp: IAttributeModel[] = []; + private customModalInstance: ModalComponent; + + constructor(private workspaceService: WorkspaceService, + private topologyTemplateService: TopologyTemplateService, + private modalsHandler: ModalsHandler, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private translateService: TranslateService) { + + this.componentType = this.workspaceService.metadata.componentType; + this.componentUid = this.workspaceService.metadata.uniqueId; + } + + ngOnInit(): void { + this.asyncInitComponent(); + } + + async asyncInitComponent() { + this.loaderService.activate(); + const response = await this.topologyTemplateService.getComponentAttributes(this.componentType, this.componentUid); + this.attributes = response.attributes; + this.temp = [...response.attributes]; + this.loaderService.deactivate(); + } + + getAttributes(): IAttributeModel[] { + return this.attributes; + } + + addOrUpdateAttribute = async (attribute: AttributeModel, isEdit: boolean) => { + this.loaderService.activate(); + let attributeFromServer: AttributeModel; + this.temp = [...this.attributes]; + + const deactivateLoader = () => { + this.loaderService.deactivate(); + return undefined; + }; + + if (isEdit) { + attributeFromServer = await this.topologyTemplateService + .updateAttributeAsync(this.componentType, this.componentUid, attribute) + .catch(deactivateLoader); + if (attributeFromServer) { + const indexOfUpdatedAttribute = _.findIndex(this.temp, (e) => e.uniqueId === attributeFromServer.uniqueId); + this.temp[indexOfUpdatedAttribute] = attributeFromServer; + } + } else { + attributeFromServer = await this.topologyTemplateService + .addAttributeAsync(this.componentType, this.componentUid, attribute) + .catch(deactivateLoader); + if (attributeFromServer) { + this.temp.push(attributeFromServer); + } + } + this.attributes = this.temp; + this.loaderService.deactivate(); + } + + deleteAttribute = async (attributeToDelete: AttributeModel) => { + this.loaderService.activate(); + this.temp = [...this.attributes]; + const res = await this.topologyTemplateService.deleteAttributeAsync(this.componentType, this.componentUid, attributeToDelete); + _.remove(this.temp, (attr) => attr.uniqueId === attributeToDelete.uniqueId); + this.attributes = this.temp; + this.loaderService.deactivate(); + }; + + openAddEditModal(selectedRow: AttributeModel, isEdit: boolean) { + const component = new Resource(undefined, undefined, undefined); + component.componentType = this.componentType; + component.uniqueId = this.componentUid; + + const title: string = this.translateService.translate('ATTRIBUTE_DETAILS_MODAL_TITLE'); + const attributeModalConfig = { + title, + size: 'md', + type: SdcUiCommon.ModalType.custom, + buttons: [ + { + id: 'save', + text: 'Save', + // spinner_position: Placement.left, + size: 'sm', + callback: () => this.modalCallBack(isEdit), + closeModal: true, + disabled: false, + } + ] as SdcUiCommon.IModalButtonComponent[] + }; + + this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, AttributeModalComponent, { attributeToEdit: selectedRow }); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('save').disabled = !isValid); + } + + /*********************** + * Call Backs from UI * + ***********************/ + + /** + * Called when 'Add' is clicked + */ + onAddAttribute() { + this.openAddEditModal(new AttributeModel(), false); + } + + /** + * Called when 'Edit' button is clicked + */ + onEditAttribute(event, row) { + event.stopPropagation(); + + const attributeToEdit: AttributeModel = new AttributeModel(row); + this.openAddEditModal(attributeToEdit, true); + } + + /** + * Called when 'Delete' button is clicked + */ + onDeleteAttribute(event, row: AttributeModel) { + event.stopPropagation(); + const onOk = () => { + this.deleteAttribute(row); + }; + + const title: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TITLE'); + const message: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TEXT'); + const okButton = new SdcUiComponents.ModalButtonComponent(); + okButton.testId = 'OK'; + okButton.text = 'OK'; + okButton.type = SdcUiCommon.ButtonType.info; + okButton.closeModal = true; + okButton.callback = onOk; + + this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]); + } + + onExpandRow(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + /** + * Callback from Modal after "Save" is clicked + * + * @param {boolean} isEdit - Whether modal is edit or add attribute + */ + modalCallBack = (isEdit: boolean) => { + const attribute: AttributeModel = this.customModalInstance.innerModalContent.instance.attributeToEdit; + this.addOrUpdateAttribute(attribute, isEdit); + } + +} @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { GlobalPipesModule } from '../../../pipes/global-pipes.module'; +import { AttributesComponent } from './attributes.component'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { AttributeModalComponent } from './attribute-modal.component'; +import { TranslateModule } from '../../../shared/translator/translate.module'; + +@NgModule({ + declarations: [ + AttributesComponent, + AttributeModalComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule, + TranslateModule + ], + exports: [ + AttributesComponent + ], + entryComponents: [ + AttributesComponent, AttributeModalComponent + ], + providers: [TopologyTemplateService] +}) +export class AttributesModule { +} @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deployment artifacts page should match current snapshot of informational artifact pages component 1`] = ` +<deployment-artifact-page + addOrUpdateArtifact={[Function Function]} + artifactsService={[Function Object]} + cacheService={[Function Object]} + deleteArtifact={[Function Function]} + getEnvArtifact={[Function Function]} + modalService={[Function Object]} + openGenericArtifactBrowserModal={[Function Function]} + openPopOver={[Function Function]} + popoverContentComponent="undefined" + popoverService={[Function Object]} + sortArtifacts={[Function Function]} + store={[Function Store]} + table="undefined" + translateService={[Function Object]} + updateEnvParams={[Function Function]} + workspaceService={[Function Object]} +> + +</deployment-artifact-page> +`; @@ -0,0 +1,73 @@ +<div class="deployment-artifact-page" *ngIf="(workspaceState$ | async) as state"> + <svg-icon-label *ngIf="!state.isViewOnly" class="add-artifact-btn" [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'" + [label]="'Add'" [name]="'plus'" + (click)="addOrUpdateArtifact()"></svg-icon-label> + <ngx-datatable + columnMode="flex" + [headerHeight]="40" + [footerHeight]="'undefined'" + [reorderable]="false" + [swapColumns]="false" + [rows]="deploymentArtifacts$ | async" + #deploymentArtifactsTable> + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div *ngIf="row.generatedFromId" class="env-artifact-container"> + <div class="env-artifact"></div> + </div> + <span sdc-tooltip [tooltip-text]="row.artifactDisplayName" [tooltip-placement]="3" [attr.data-tests-id]="'artifactDisplayName_' + row.artifactDisplayName">{{row.artifactDisplayName}}</span> + <span *ngIf="row.description.length > 0" class="info"> + <svg-icon [clickable]="true" [name]="'comment'" [mode]="'primary2'" (click)="openPopOver('Description',row.description,{x:$event.pageX , y:$event.pageY },'bottom')"></svg-icon> + </span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="0.6"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span sdc-tooltip [tooltip-text]="row.artifactType" [tooltip-placement]="3" [attr.data-tests-id]="'artifactType_' + row.artifactDisplayName">{{row.artifactType}}</span> + </ng-template> + </ngx-datatable-column> exactly 2 tosca artifacts + <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="0.3"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span [attr.data-tests-id]="'artifactVersion_' + row.artifactDisplayName">{{ row.artifactVersion }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="UUID" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span sdc-tooltip [tooltip-text]="row.artifactUUID" [tooltip-placement]="3">{{ row.artifactUUID }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="0.6"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class="download-artifact-button"> + <svg-icon *ngIf="!row.heatParameters?.length && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'edit-o'" + testId="edit_{{row.artifactDisplayName}}" clickable="true" size="medium" + (click)="addOrUpdateArtifact(row, state.isViewOnly)"></svg-icon> + <svg-icon *ngIf="row.heatParameters?.length && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'indesign_status'" + testId="update_heat_params_{{row.artifactDisplayName}}" clickable="true" size="medium" + (click)="updateEnvParams(row, state.isViewOnly)"></svg-icon> + <svg-icon *ngIf="!row.isFromCsar && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'trash-o'" + testId="delete_{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="deleteArtifact(row)"></svg-icon> + <svg-icon *ngIf="row.isGenericBrowseable()" class="action-icon" [mode]="'primary2'" [name]="'search-o'" + testId="gab-{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="openGenericArtifactBrowserModal(row)"></svg-icon> + + <!--Download--> + </div> + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-footer> + <ng-template ngx-datatable-footer-template> + <div class="table-footer-container"> + <sdc-button *ngIf="!state.isViewOnly" [type]="'secondary'" + [testId]="'add_artifact_btn'" + [text]="'DEPLOYMENT_ARTIFACT_BUTTON_ADD_OTHER' | translate" + [icon_name]="'plus-circle-o'" + [icon_mode] = "'secondary'" + [icon_position]="'left'" + (click)="addOrUpdateArtifact()"> + </sdc-button> + </div> + </ng-template> + </ngx-datatable-footer> + </ngx-datatable> +</div> @@ -0,0 +1,55 @@ +.deployment-artifact-page { + + + .env-artifact-container { + margin-left: -25px; + margin-top: -21px; + padding-left: 10px; + position: absolute; + background-color: white; + .env-artifact { + border-left: 1px #848586 solid; + height: 33px; + + border-top: 1px #848586 solid; + border-bottom: 1px #848586 solid; + width: 10px; + float: left; + + } + } + .add-artifact-btn { + display: flex; + cursor: pointer; + justify-content: flex-end; + margin-bottom: 10px; + } + .download-artifact-button { + display: flex; + justify-content: center; + + .action-icon { + margin-right: 10px; + } + } + + .table-footer-container { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + margin: 20px 0px; + } +} + +:host ::ng-deep { + + .ngx-datatable { + //border: 1px solid red; + .datatable-body-cell { + .info { + float: right; + } + } + } +}
\ No newline at end of file @@ -0,0 +1,155 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { ArtifactModel } from 'app/models'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/index'; +import { map } from 'rxjs/operators'; +import { GabConfig } from '../../../../models/gab-config'; +import { PathsAndNamesDefinition } from '../../../../models/paths-and-names'; +import { GenericArtifactBrowserComponent } from '../../../../ng2/components/logic/generic-artifact-browser/generic-artifact-browser.component'; +import { ArtifactGroupType, ArtifactType } from '../../../../utils/constants'; +import { ArtifactsService } from '../../../components/forms/artifacts-form/artifacts.service'; +import { PopoverContentComponent } from '../../../components/ui/popover/popover-content.component'; +import { CacheService } from '../../../services/cache.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { GetArtifactsByTypeAction } from '../../../store/actions/artifacts.action'; +import { ArtifactsState } from '../../../store/states/artifacts.state'; +import { WorkspaceState, WorkspaceStateModel } from '../../../store/states/workspace.state'; +import { WorkspaceService } from '../workspace.service'; +import { ModalService } from 'app/ng2/services/modal.service'; + +export interface IPoint { + x: number; + y: number; +} + +@Component({ + selector: 'deployment-artifact-page', + templateUrl: './deployment-artifacts-page.component.html', + styleUrls: ['./deployment-artifacts-page.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class DeploymentArtifactsPageComponent implements OnInit { + + public componentId: string; + public componentType: string; + public deploymentArtifacts$: Observable<ArtifactModel[]>; + public isComponentInstanceSelected: boolean; + + @Select(WorkspaceState) workspaceState$: Observable<WorkspaceStateModel>; + @ViewChild('informationArtifactsTable') table: any; + @ViewChild('popoverForm') popoverContentComponent: PopoverContentComponent; + + constructor(private workspaceService: WorkspaceService, + private artifactsService: ArtifactsService, + private store: Store, + private popoverService: SdcUiServices.PopoverService, + private cacheService: CacheService, + private modalService: SdcUiServices.ModalService, + private translateService: TranslateService) { + } + + private getEnvArtifact = (heatArtifact: ArtifactModel, artifacts: ArtifactModel[]): ArtifactModel => { + return _.find(artifacts, (item: ArtifactModel) => { + return item.generatedFromId === heatArtifact.uniqueId; + }); + }; + + // we need to sort the artifact in a way that the env artifact is always under the artifact he is connected to- this is cause of the way the ngx databale work + private sortArtifacts = ((artifacts) => { + const sortedArtifacts = []; + _.forEach(artifacts, (artifact: ArtifactModel): void => { + const envArtifact = this.getEnvArtifact(artifact, artifacts); + if (!artifact.generatedFromId) { + sortedArtifacts.push(artifact); + } + if (envArtifact) { + sortedArtifacts.push(envArtifact); + } + }); + return sortedArtifacts; + }) + + ngOnInit(): void { + this.componentId = this.workspaceService.metadata.uniqueId; + this.componentType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new GetArtifactsByTypeAction({ + componentType: this.componentType, + componentId: this.componentId, + artifactType: ArtifactGroupType.DEPLOYMENT + })); + this.deploymentArtifacts$ = this.store.select(ArtifactsState.getArtifactsByType).pipe(map((filterFn) => filterFn(ArtifactType.DEPLOYMENT))).pipe(map(artifacts => { + return this.sortArtifacts(artifacts); + })); + } + + onActivate(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + public addOrUpdateArtifact = (artifact: ArtifactModel, isViewOnly: boolean) => { + this.artifactsService.openArtifactModal(this.componentId, this.componentType, artifact, ArtifactGroupType.DEPLOYMENT, isViewOnly); + } + + public deleteArtifact = (artifactToDelete) => { + this.artifactsService.deleteArtifact(this.componentType, this.componentId, artifactToDelete); + } + + private openPopOver = (title: string, content: string, positionOnPage: IPoint, location: string) => { + this.popoverService.createPopOver(title, content, positionOnPage, location); + } + + public updateEnvParams = (artifact: ArtifactModel, isViewOnly: boolean) => { + this.artifactsService.openUpdateEnvParams(this.componentType, this.componentId, artifact ); + } + + private openGenericArtifactBrowserModal = (artifact: ArtifactModel): void => { + const titleStr = 'Generic Artifact Browser'; + const modalConfig = { + size: 'sdc-xl', + title: titleStr, + type: SdcUiCommon.ModalType.custom, + buttons: [{ + id: 'closeGABButton', + text: 'Close', + size: 'sm', + closeModal: true + }] as SdcUiCommon.IModalButtonComponent[] + }; + + const uiConfiguration: any = this.cacheService.get('UIConfiguration'); + let noConfig: boolean = false; + let pathsandnamesArr: PathsAndNamesDefinition[] = []; + + if (typeof uiConfiguration.gab === 'undefined') { + noConfig = true; + } else { + const gabConfig: GabConfig = uiConfiguration.gab + .find((config) => config.artifactType === artifact.artifactType); + if (typeof gabConfig === 'undefined') { + noConfig = true; + } else { + pathsandnamesArr = gabConfig.pathsAndNamesDefinitions; + } + } + + + if (noConfig) { + const msg = this.translateService.translate('DEPLOYMENT_ARTIFACT_GAB_NO_CONFIG'); + this.modalService.openAlertModal(titleStr, msg); + } + + const modalInputs = { + pathsandnames: pathsandnamesArr, + artifactid: artifact.esId, + resourceid: this.componentId + }; + + this.modalService.openCustomModal(modalConfig, GenericArtifactBrowserComponent, modalInputs); + + } + +} @@ -0,0 +1,35 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; +import {ArtifactFormModule} from "../../../components/forms/artifacts-form/artifact-form.module"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; +import {DeploymentArtifactsPageComponent} from "./deployment-artifacts-page.component"; +import {TranslatePipe} from "../../../shared/translator/translate.pipe"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {GenericArtifactBrowserModule} from "../../../components/logic/generic-artifact-browser/generic-artifact-browser.module"; + +@NgModule({ + declarations: [ + DeploymentArtifactsPageComponent + ], + imports: [ + TranslateModule, + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + UiElementsModule, + ArtifactFormModule, + GenericArtifactBrowserModule + ], + exports: [ + DeploymentArtifactsPageComponent + ], + entryComponents: [ + DeploymentArtifactsPageComponent + ], + providers:[ArtifactsService] +}) +export class DeploymentArtifactsPageModule { +} @@ -0,0 +1,86 @@ +// import ' rxjs/add/observable/of'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxsModule, Store } from '@ngxs/store'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Observable'; +import { deploymentArtifactMock } from '../../../../../jest/mocks/artifacts-mock'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ArtifactsService } from '../../../components/forms/artifacts-form/artifacts.service'; +import { CacheService } from '../../../services/cache.service'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { ArtifactsState } from '../../../store/states/artifacts.state'; +import { WorkspaceService } from '../workspace.service'; +import { DeploymentArtifactsPageComponent } from './deployment-artifacts-page.component'; +import {ModalService} from "../../../services/modal.service"; + +describe('deployment artifacts page', () => { + + let fixture: ComponentFixture<DeploymentArtifactsPageComponent>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let store: Store; + + beforeEach( + async(() => { + + topologyTemplateServiceMock = { + getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(deploymentArtifactMock)) + }; + workspaceServiceMock = { + metadata: <ComponentMetadata>{ + uniqueId: 'service_unique_id', + componentType: 'SERVICE' + } + } + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + } + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DeploymentArtifactsPageComponent], + imports: [NgxDatatableModule, TranslateModule, NgxsModule.forRoot([ArtifactsState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}, + {provide: ArtifactsService, useValue: {}}, + {provide: SdcUiServices.PopoverService, useValue: {}}, + {provide: CacheService, useValue: {}}, + {provide: SdcUiServices.ModalService, useValue: {}}, + {provide: ModalService, useValue: {}}, + {provide: TranslateService, useValue: {}} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DeploymentArtifactsPageComponent); + store = testBed.get(Store); + }); + }) + ); + + it('should match current snapshot of informational artifact pages component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should see exactly 2 tosca artifacts', () => { + fixture.componentInstance.ngOnInit(); + fixture.componentInstance.deploymentArtifacts$.subscribe((artifacts) => { + expect(artifacts.length).toEqual(8); + }) + store.selectOnce((state) => state.artifacts.deploymentArtifacts).subscribe((artifacts) => { + expect(artifacts.length).toEqual(8); + }); + }); + +}); @@ -0,0 +1,11 @@ +<div class="deployment-page"> + <deployment-graph></deployment-graph> + <panel-wrapper-component> + <sdc-tabs class="deployment-tabs" [iconsSize]="'large'" [isVertical]="true"> + <sdc-tab *ngFor="let tab of tabs" [titleIcon]="tab.titleIcon" [active]="tab.isActive" + [tooltipText]="tab.tooltipText"> + <hierarchy-tab *ngIf="isDataAvailable" [isViewOnly]="(isViewOnly$ | async)"></hierarchy-tab> + </sdc-tab> + </sdc-tabs> + </panel-wrapper-component> +</div>
\ No newline at end of file @@ -0,0 +1,24 @@ +@import './../../../../../assets/styles/variables.less'; +@import './../../../../../assets/styles/override.less'; +.deployment-page { + width: 100%; + height: 100%; + + /deep/ .sdc-tabs { + height: 100%; + } + /deep/ .sdc-tabs-list { + position: absolute; + top: 22px; + right: 303px; + background-color: @sdcui_color_silver; + + svg-icon-label { + vertical-align: middle; + } + } + /deep/ .sdc-tab-content { + height: 100%; + } +} + @@ -0,0 +1,78 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import {Component} from "@angular/core"; +import {HierarchyTabComponent} from "./panel/panel-tabs/hierarchy-tab/hierarchy-tab.component"; +import {ComponentGenericResponse} from "../../../services/responses/component-generic-response"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../workspace.service"; +import {Module} from "app/models"; +import {SdcUiServices} from "onap-ui-angular"; +import {Select} from "@ngxs/store"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import {DeploymentGraphService} from "../../composition/deployment/deployment-graph.service"; + +const tabs = + { + hierarchyTab: { + titleIcon: 'composition-o', + component: HierarchyTabComponent, + input: {}, + isActive: true, + tooltipText: 'Hierarchy' + } + }; + +@Component({ + selector: 'deployment-page', + templateUrl: './deployment-page.component.html', + styleUrls: ['deployment-page.component.less'] +}) + +export class DeploymentPageComponent { + public tabs: Array<any>; + public resourceType: string; + public modules: Array<Module>; + public isDataAvailable: boolean; + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + + constructor(private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private deploymentService: DeploymentGraphService, + private loaderService: SdcUiServices.LoaderService) { + this.tabs = []; + this.isDataAvailable = false; + } + + ngOnInit(): void { + this.topologyTemplateService.getDeploymentGraphData(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response: ComponentGenericResponse) => { + this.deploymentService.componentInstances = response.componentInstances; + this.deploymentService.componentInstancesRelations = response.componentInstancesRelations; + this.deploymentService.modules = response.modules; + this.isDataAvailable = true; + this.loaderService.deactivate(); + }); + + this.loaderService.activate(); + this.resourceType = this.workspaceService.getMetadataType(); + this.tabs.push(tabs.hierarchyTab); + } +} @@ -0,0 +1,30 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {DeploymentPageComponent} from "./deployment-page.component"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {HierarchyTabModule} from "./panel/panel-tabs/hierarchy-tab/hierarchy-tab.module"; +import {DeploymentGraphService} from "../../composition/deployment/deployment-graph.service"; +import {DeploymentGraphModule} from "../../composition/deployment/deployment-graph.module"; + +@NgModule({ + declarations: [DeploymentPageComponent], + imports: [CommonModule, + DeploymentGraphModule, + SdcUiComponentsModule, + UiElementsModule, + TranslateModule, + GlobalPipesModule, + HierarchyTabModule + ], + exports: [DeploymentPageComponent], + entryComponents: [DeploymentPageComponent], + providers: [DeploymentGraphService] +}) +export class DeploymentPageModule { +}
\ No newline at end of file @@ -0,0 +1,29 @@ +<div class="edit-module-name"> + <div class="edit-module-name-label vfInstance-name" data-tests-id="popover-vfinstance-name" sdc-tooltip [tooltip-text]="selectModule.vfInstanceName">{{selectModule.vfInstanceName}}</div> + <div class="edit-module-name-heatName"> + <sdc-input #heatName [maxLength]="50" + [(value)]="selectModule.heatName" + [testId]="'popover-heat-name'" + [placeHolder]="'Enter Name'"> + </sdc-input> + <sdc-validation [validateElement]="heatName"> + <sdc-regex-validator [message]="'Special characters not allowed.'" [pattern]="pattern"></sdc-regex-validator> + </sdc-validation> + </div> + <div class="edit-module-name-label module-name" data-tests-id="'popover-module-name'" sdc-tooltip [tooltip-text]="selectModule.moduleName">{{selectModule.moduleName}}</div> + <sdc-button class="edit-module-name-btn cancel-button" + [text]="'Cancel'" + [testId]="'popover-close-button'" + [type]="'primary'" + [size] = "'small'" + (click)="clickButton(false)"> + </sdc-button> + <sdc-button class="edit-module-name-btn save-button" + [text]="'Save'" + [testId]="'popover-save-button'" + [type]="'primary'" + [size] = "'small'" + (click)="clickButton(true)" + [disabled]="selectModule.heatName == originalName"> + </sdc-button> +</div>
\ No newline at end of file @@ -0,0 +1,20 @@ +.edit-module-name-btn{ + float:right; + margin-left: 10px; + margin-bottom: 20px; +} +.save-button { + margin-left: 30px; +} +.cancel-button { + margin-left: 20px; +} +.edit-module-name-heatName { + margin-bottom: 15px; +} +.edit-module-name-label { + text-overflow: ellipsis; + display: block; + white-space: nowrap; + margin-bottom: 10px; +} @@ -0,0 +1,24 @@ +import { Component, Input, Output, OnInit } from "@angular/core"; +import { EventEmitter } from "@angular/core"; +import { DisplayModule } from "../../../../../../../models/modules/base-module"; +import { ValidationConfiguration } from "../../../../../../../models/validation-config"; + +@Component({ + selector: 'edit-module-name', + templateUrl: './edit-module-name.component.html', + styleUrls: ['edit-module-name.component.less'] +}) +export class EditModuleName implements OnInit{ + @Input() selectModule:DisplayModule; + @Output() clickButtonEvent: EventEmitter<String> = new EventEmitter(); + private pattern = ValidationConfiguration.validation.validationPatterns.stringOrEmpty; + private originalName: string; + constructor(){} + public ngOnInit(): void { + this.originalName = this.selectModule.heatName; + } + + private clickButton(saveOrCancel: boolean) : void { + this.clickButtonEvent.emit(saveOrCancel ? this.selectModule.heatName : null); + } +}
\ No newline at end of file @@ -0,0 +1,119 @@ +<div class="sdc-hierarchy-tab" ng-class=""> + <sdc-loader [global]="false" [testId]="'hierarchy-tab-loader'" [active]="isLoading" [relative]="true" [size]="'medium'"></sdc-loader> + <div class="sdc-hierarchy-tab-title" + [attr.data-tests-id]="'tab-header'">{{'DEPLOYMENT_TAB_TITLE' | translate }}</div> + <div [ngClass]="{'scroll-module-list': selectedModule}"> + <div *ngIf="topologyTemplateType != 'SERVICE'; else isService" class="modules-list"> + <div> + <div class="sdc-hierarchy-tab-sub-title" data-tests-id="tab-sub-header">{{topologyTemplateName}}</div> + <div *ngFor="let module of modules; index as i"> + <sdc-accordion [title]="module.name" [arrow-direction]="'left'" + [css-class]="'expand-collapse-container'" + [ngClass]="{'selected': selectedModule !== undefined && selectedModule.uniqueId === module.uniqueId}" + [testId]="'hierarchy-module-' + i + '-title'" tooltip="{{module.name}}" + (click)="onModuleSelected(module)"> + <div *ngFor="let memberId of getKeys(module.members)"> + <div class="expand-collapse-sub-title" tooltip="{{memberId}}">{{memberId}}</div> + </div> + </sdc-accordion> + </div> + </div> + </div> + + <ng-template #isService> + <div class="module-list"> + <div *ngFor="let instance of componentInstances; index as instanceIndex"> + <sdc-accordion [title]="instance.name" [arrow-direction]="'left'" + [css-class]="'expand-collapse-container outer-container'" + [testId]="'hierarchy-instance-' + instanceIndex + '-title'" + tooltip="{{instance.name}}"> + <div *ngFor="let module of instance.groupInstances; index as moduleIndex"> + <sdc-accordion [title]="module.name" [arrow-direction]="'left'" + [css-class]="'expand-collapse-container inner-container'" + [ngClass]="{'selected': selectedModule && selectedModule.groupInstanceUniqueId === module.uniqueId}" + [testId]="'hierarchy-module-' + moduleIndex + '-title'" + tooltip="{{module.uniqueId}}" + (click)="onModuleSelected(module, instance.uniqueId)"> + <div *ngFor="let memberId of getKeys(module.members)"> + <div class="expand-collapse-sub-title" tooltip="{{memberId}}">{{memberId}}</div> + </div> + </sdc-accordion> + </div> + </sdc-accordion> + </div> + </div> + </ng-template> + + <!--TODO: Add Resizable--> + <div *ngIf="selectedModule"class="module-data-container" [attr.data-tests-id]="'selected-module-data'"> + <div class="module-data"> + <div class="module-name-container"> + <div class="module-name module-text-overflow" [attr.data-tests-id]="'selected-module-name'" + tooltip="{{selectedModule.name}}">{{selectedModule.name}}</div> + <div class="edit-name-container" *ngIf="topologyTemplateType != 'SERVICE'"> + <svg-icon name="edit-o" [size]="'medium'" [ngClass]="{'hand-pointer': !isViewOnly}" (click)="openEditModuleNamePopup($event)"></svg-icon> + </div> + </div> + <div [attr.data-tests-id]="'selected-module-group-uuid'" tooltip="{{selectedModule.groupUUID}}"> + <div class="selected-module-property-header">Module ID:</div> + <div class="selected-module-property-value small-font">{{selectedModule.groupUUID}}</div> + </div> + <div [attr.data-tests-id]="'selected-module-group-customization-uuid'" + *ngIf="topologyTemplateType == 'SERVICE' && isViewOnly" + tooltip="{{selectedModule.customizationUUID}}"> + <div class="selected-module-property-header">Customization ID:</div> + <div class="selected-module-property-value small-font">{{selectedModule.customizationUUID}}</div> + </div> + <div [attr.data-tests-id]="'selected-module-group-invariant-uuid'" + tooltip="{{selectedModule.invariantUUID}}"> + <div class="selected-module-property-header">Invariant UUID:</div> + <div class="selected-module-property-value small-font">{{selectedModule.invariantUUID}}</div> + </div> + <div [attr.data-tests-id]="'selected-module-version'" class="selected-module-property-container"> + <div class="selected-module-property-header">Version:</div> + <div class="selected-module-property-value same-row">{{selectedModule.version}}</div> + </div> + <div data-tests-id="selected-module-is-base" class="selected-module-property-container"> + <div class="selected-module-property-header">IsBase:</div> + <div class="selected-module-property-value same-row">{{selectedModule.isBase}}</div> + </div> + + </div> + <sdc-accordion [title]="'Properties'" [arrow-direction]="'right'" + [css-class]="'expand-collapse-module-data-container'"> + <div *ngFor="let property of selectedModule.properties | orderBy:['name']:['asc']"> + <div class="module-data-list-item"> + <div class="module-data-list-item-value property-name" + [attr.data-tests-id]="'selected-module-property-name'"> + <span tooltip="{{property.name}}" [ngClass]="{'hand-pointer': !isViewOnly}" + (click)="!isViewOnly && openEditPropertyModal(property)">{{property.name}}</span> + </div> + <div class="module-data-list-item-value property-info" + [attr.data-tests-id]="'selected-module-property-type'"> Type: {{property.type}}</div> + <div class="module-data-list-item-value property-info" + [attr.data-tests-id]="'selected-module-property-schema-type'"> + Value: {{property.value}}</div> + </div> + </div> + </sdc-accordion> + <sdc-accordion [title]="'Artifacts'" [arrow-direction]="'right'" + [css-class]="'expand-collapse-module-data-container'"> + <div *ngFor="let artifact of selectedModule.artifacts | orderBy:['artifactName']:['asc']"> + <div class="module-data-list-item"> + <div class="artifact-list-item"> + <div class="module-data-list-item-value" + [attr.data-tests-id]="'selected-module-artifact-name'" + tooltip="{{artifact.artifactName}}">{{artifact.artifactName}}</div> + <div class="module-data-list-item-value artifact-info" + [attr.data-tests-id]="'selected-module-artifact-uuid'" + tooltip="{{artifact.artifactUUID}}">UUID: {{artifact.artifactUUID}}</div> + <div class="module-data-list-item-value artifact-info" + [attr.data-tests-id]="'selected-module-artifact-version'"> + Version: {{artifact.artifactVersion}}</div> + </div> + </div> + </div> + </sdc-accordion> + </div> + </div> +</div> @@ -0,0 +1,222 @@ +@import './../../../../../../../../assets/styles/variables.less'; +.sdc-hierarchy-tab { + padding: 15px 0 0 0; + background-color: #f8f8f8; + height: 100%; + box-shadow: 0.3px 1px 3px rgba(24, 24, 25, 0.42); + display: flex; + flex-flow: column; + + .sdc-hierarchy-tab-title { + color: @main_color_a; + padding: 0 0 15px 20px; + border-bottom: 1px solid #d2d2d2; + } + + .sdc-hierarchy-tab-sub-title { + color: @main_color_a; + padding: 15px 20px 15px 20px; + } + + .scroll-module-list { + overflow-y: auto; + display: flex; + height: 100%; + flex-direction: column; + } + + /deep/ .expand-collapse-container { + margin-bottom: 0; + + .sdc-accordion-header { + white-space: nowrap; + line-height: 22px; + background-color: @tlv_color_u; + padding: 8px 20px 8px 8px; + box-shadow: inset 0px -1px 0px 0px rgba(255, 255, 255, 0.7); + height: 40px; + + .title { + overflow: hidden; + text-overflow: ellipsis; + max-width: 215px; + } + } + + .sdc-accordion-body.open { + padding: 0 0 5px 0; + } + + .sdc-accordion-header:hover { + background-color: @main_color_o; + } + + &.outer-container { + .sdc-accordion-body { + padding-left: 0; + } + } + + &.inner-container { + margin-bottom: 0; + + .sdc-accordion-header { + padding: 8px 20px 8px 30px; + background-color: @tlv_color_t + } + } + } + + sdc-accordion.selected { + /deep/ .expand-collapse-container { + .sdc-accordion-header { + background-color: @main_color_a; + color: @main_color_p; + + .svg-icon { + fill: @main_color_p; + } + } + } + } + + .expand-collapse-sub-title { + max-width: 225px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 10px 0 0 33px; + } + + .expand-collapse-content { + .expand-collapse-title { + padding: 0 10px 0 30px; + } + } + + .module-data-container { + width: 100%; + overflow-y: overlay; + background-color: @tlv_color_v; + border: 1px solid @main_color_a; + border-top: 4px solid @main_color_a; + box-shadow: 0.3px 1px 2px rgba(24, 24, 25, 0.32); + .module-data { + color: @main_color_a; + padding: 10px 0 10px 0; + margin: 0 20px 0 20px; + + .selected-module-property-header { + font-weight: bold; + } + + .selected-module-property-value { + font-family: @font-opensans-regular; + + &.small-font { + font-size: 12px; + } + } + + .module-name-container { + + display: flex; + flex-direction: row; + + .module-name { + font-size: 14px; + width: 75%; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .edit-name-container { + float: right; + border-left: 1px solid @main_color_a; + height: 20px; + padding-left: 12px; + + svg-icon { + padding-top: 3px; + fill: @main_color_s; + + &.hand-pointer { + cursor: pointer; + } + + } + } + } + } + + .selected-module-property-container { + flex-direction: row; + display: flex; + + .selected-module-property-value { + text-indent: 2px; + } + } + + /deep/ .expand-collapse-module-data-container { + margin-bottom: 0; + + .sdc-accordion-header { + white-space: nowrap; + line-height: 22px; + padding: 8px 20px 8px 16px; + height: 40px; + background-color: @tlv_color_w; + color: @main_color_l; + border-top: 1px solid @main_color_a; + border-bottom: 1px solid @main_color_a; + width: 100%; + } + + } + + .module-data-list-item { + padding-bottom: 10px; + margin: 0 20px 0 20px; + + .artifact-list-item { + color: @main_color_m; + } + + .module-data-list-item-value { + width: 100%; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &.artifact-info { + font-family: @font-opensans-regular; + font-size: 12px; + } + + &.property-name { + font-weight: 400; + color: @main_color_a; + + .hand-pointer { + cursor: pointer; + } + } + + &.property-info { + color: @func_color_s; + font-family: @font-opensans-regular; + } + } + } + } +} + +.modules-list { + overflow-y: overlay; + flex-grow: 1; +}
\ No newline at end of file @@ -0,0 +1,133 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {HierarchyTabComponent} from "./hierarchy-tab.component"; +import {ConfigureFn, configureTests} from "../../../../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {TranslateModule} from "../../../../../../shared/translator/translate.module"; +import {TopologyTemplateService} from "../../../../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../../../../workspace.service"; +import {ModulesService} from "../../../../../../services/modules.service"; +import {GlobalPipesModule} from "../../../../../../pipes/global-pipes.module"; +import {TranslateService} from "../../../../../../shared/translator/translate.service"; +import {ModalsHandler} from "../../../../../../../utils/modals-handler"; +import {ComponentFactory} from "../../../../../../../utils/component-factory"; +import {NgxsModule} from "@ngxs/store"; +import { SdcUiServices } from "onap-ui-angular"; +import {Observable} from "rxjs"; +import {DisplayModule, Module} from "../../../../../../../models/modules/base-module"; +import {DeploymentGraphService} from "../../../../../composition/deployment/deployment-graph.service"; +import {ComponentMetadata} from "../../../../../../../models/component-metadata"; + +describe('HierarchyTabComponent', () => { + + let fixture: ComponentFixture<HierarchyTabComponent>; + let workspaceService: Partial<WorkspaceService>; + let popoverServiceMock: Partial<SdcUiServices.PopoverService>; + let modulesServiceMock: Partial<ModulesService>; + + let editModuleNameInstanceMock = {innerPopoverContent:{instance: { clickButtonEvent: Observable.of("new heat name")}}, + closePopover: jest.fn()}; + let eventMock = {x: 1650, y: 350}; + let moduleMock: Array<Module> = [{name: "NewVf2..base_vepdg..module-0", uniqueId: '1'}]; + let selectedModuleMock: DisplayModule = {name: "NewVf2..base_vepdg..module-0", vfInstanceName: "NewVf2", moduleName:"module-0", + heatName: "base_vepdg", uniqueId: '1', updateName: jest.fn().mockImplementation(() => { + selectedModuleMock.name = selectedModuleMock.vfInstanceName + '..' + selectedModuleMock.heatName + '..' + + selectedModuleMock.moduleName;})} + let updateSelectedModuleMock = () => { + selectedModuleMock.heatName = "base_vepdg"; + selectedModuleMock.name = "NewVf2..base_vepdg..module-0"; + fixture.componentInstance.selectedModule = selectedModuleMock; + fixture.componentInstance.modules = moduleMock; + } + beforeEach( + async(() => { + + workspaceService ={ + metadata: <ComponentMetadata> { + name: '', + componentType: '' + } + } + popoverServiceMock = { + createPopOverWithInnerComponent: jest.fn().mockImplementation(() => {return editModuleNameInstanceMock}) + } + modulesServiceMock = { + updateModuleMetadata: jest.fn().mockReturnValue(Observable.of({})) + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [HierarchyTabComponent], + schemas: [NO_ERRORS_SCHEMA], + imports: [TranslateModule, NgxsModule.forRoot([]), GlobalPipesModule], + providers: [ + {provide: DeploymentGraphService, useValue: {}}, + {provide: ComponentFactory, useValue: {}}, + {provide: TopologyTemplateService, useValue: {}}, + {provide: WorkspaceService, useValue: workspaceService}, + {provide: ModulesService, useValue: modulesServiceMock}, + {provide: TranslateService, useValue: {}}, + {provide: ModalsHandler, useValue: {}}, + {provide: SdcUiServices.PopoverService, useValue: popoverServiceMock} + ] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(HierarchyTabComponent); + }); + }) + ); + + it('expected heirarchy component to be defined', () => { + expect(fixture).toBeDefined(); + }); + + it('Update heat name and name sucessfully', () => { + updateSelectedModuleMock(); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled(); + expect(modulesServiceMock.updateModuleMetadata).toHaveBeenCalled(); + expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..new heat name..module-0'); + expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..new heat name..module-0'); + expect(fixture.componentInstance.selectedModule.heatName).toEqual('new heat name'); + }) + it('Try to update heat name and name and get error from server', () => { + updateSelectedModuleMock(); + modulesServiceMock.updateModuleMetadata.mockImplementation(() => Observable.throwError({})); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled(); + expect(modulesServiceMock.updateModuleMetadata).toHaveBeenCalled(); + expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..base_vepdg..module-0'); + expect(fixture.componentInstance.selectedModule.heatName).toEqual('base_vepdg'); + expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..base_vepdg..module-0'); + }) + it('Try to update heat name and name but not find the module with the same uniqueId', () => { + selectedModuleMock.uniqueId = '2' + updateSelectedModuleMock(); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled(); + expect(modulesServiceMock.updateModuleMetadata).not.toHaveBeenCalled(); + expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..base_vepdg..module-0'); + expect(fixture.componentInstance.selectedModule.heatName).toEqual('base_vepdg'); + expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..base_vepdg..module-0'); + selectedModuleMock.uniqueId = '1' + }) + it('Open edit module name popover and change the heat name', () => { + updateSelectedModuleMock(); + spyOn(fixture.componentInstance, 'updateHeatName'); + spyOn(fixture.componentInstance, 'updateOriginalHeatName'); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(popoverServiceMock.createPopOverWithInnerComponent).toHaveBeenCalled(); + expect(fixture.componentInstance.selectedModule.heatName).toEqual("new heat name"); + expect(fixture.componentInstance.updateHeatName).toHaveBeenCalled(); + }) + + + it('Open edit module name popover and not change the heat name', () => { + updateSelectedModuleMock(); + editModuleNameInstanceMock.innerPopoverContent.instance.clickButtonEvent = Observable.of(null); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(popoverServiceMock.createPopOverWithInnerComponent).toHaveBeenCalled(); + expect(fixture.componentInstance.selectedModule.heatName).toEqual("base_vepdg"); + }) +});
\ No newline at end of file @@ -0,0 +1,139 @@ +import {Component, Input} from "@angular/core"; +import {Component as TopologyTemplate, ComponentInstance, DisplayModule, Module, PropertyModel} from "app/models"; +import {TranslateService} from "app/ng2/shared/translator/translate.service"; +import {ComponentType} from "app/utils/constants"; +import {WorkspaceService} from "../../../../workspace.service"; +import {ModulesService} from "../../../../../../services/modules.service"; +import * as _ from "lodash"; +import {ModalsHandler} from "../../../../../../../utils/modals-handler"; +import {ComponentFactory} from "../../../../../../../utils/component-factory"; +import {Select, Store} from "@ngxs/store"; +import { SdcUiServices } from "onap-ui-angular"; +import { EditModuleName } from "../edit-module-name/edit-module-name.component"; +import {GraphState} from "../../../../../composition/common/store/graph.state"; +import {DeploymentGraphService} from "../../../../../composition/deployment/deployment-graph.service"; +import {OnSidebarOpenOrCloseAction} from "../../../../../composition/common/store/graph.actions"; + +@ Component({ + selector: 'hierarchy-tab', + templateUrl: './hierarchy-tab.component.html', + styleUrls: ['./hierarchy-tab.component.less'], +}) +export class HierarchyTabComponent { + + @Select(GraphState.withSidebar) withSidebar$: boolean; + @Input() isViewOnly: boolean; + public selectedIndex: number; + public selectedModule: DisplayModule; + public isLoading: boolean; + public topologyTemplateName: string; + public topologyTemplateType: string; + public modules: Array<Module> = []; + public componentInstances: Array<ComponentInstance> = []; + private editPropertyModalTopologyTemplate: TopologyTemplate; + + constructor(private translateService: TranslateService, + private workspaceService: WorkspaceService, + private deploymentService: DeploymentGraphService, + private modulesService: ModulesService, + private ModalsHandler: ModalsHandler, + private componentFactory: ComponentFactory, + private store: Store, + private popoverService: SdcUiServices.PopoverService) { + this.isLoading = false; + this.topologyTemplateName = this.workspaceService.metadata.name; + this.topologyTemplateType = this.workspaceService.metadata.componentType; + } + + ngOnInit() { + this.modules = this.deploymentService.modules; + this.componentInstances = this.deploymentService.componentInstances; + this.editPropertyModalTopologyTemplate = this.componentFactory.createEmptyComponent(this.topologyTemplateType); + this.editPropertyModalTopologyTemplate.componentInstances = this.deploymentService.componentInstances; + } + + onModuleSelected(module: Module, componentInstanceId?: string): void { + + let onSuccess = (module: DisplayModule) => { + console.log("Module Loaded: ", module); + this.selectedModule = module; + this.isLoading = false; + }; + + let onFailed = () => { + this.isLoading = false; + }; + + if (!this.selectedModule || (this.selectedModule && this.selectedModule.uniqueId != module.uniqueId)) { + this.isLoading = true; + if (this.topologyTemplateType == ComponentType.SERVICE) { + // this.selectedInstanceId = componentInstanceId; + this.modulesService.getComponentInstanceModule(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, componentInstanceId, module.uniqueId).subscribe((resultModule: DisplayModule) => { + onSuccess(resultModule); + }, () => { + onFailed(); + }); + } else { + this.modulesService.getModuleForDisplay(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, module.uniqueId).subscribe((resultModule: DisplayModule) => { + onSuccess(resultModule); + }, () => { + onFailed(); + }); + } + } + } + + updateHeatName(): void { + this.isLoading = true; + let originalName: string = this.selectedModule.name; + + this.selectedModule.updateName(); + let moduleIndex: number = _.indexOf(this.modules, _.find(this.modules, (module: Module) => { + return module.uniqueId === this.selectedModule.uniqueId; + })); + + if (moduleIndex !== -1) { + this.modules[moduleIndex].name = this.selectedModule.name; + this.modulesService.updateModuleMetadata(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, this.modules[moduleIndex]).subscribe(() => { + this.isLoading = false; + }, () => { + this.updateOriginalHeatName(originalName, moduleIndex); + this.modules[moduleIndex].name = originalName; + }); + } else { + this.updateOriginalHeatName(originalName, moduleIndex); + } + }; + + private updateOriginalHeatName(originalName: string, moduleIndex: number){ + this.isLoading = false; + this.selectedModule.name = originalName; + this.selectedModule.heatName = this.selectedModule.name.split('..')[1]; + } + + openEditPropertyModal(property: PropertyModel): void { + this.editPropertyModalTopologyTemplate.setComponentMetadata(this.workspaceService.metadata); + this.ModalsHandler.openEditModulePropertyModal(property, this.editPropertyModalTopologyTemplate, this.selectedModule, this.selectedModule.properties).then(() => { + }); + } + + private getKeys(map: Map<any, any>) { + return _.keys(map); + } + + private toggleSidebarDisplay = () => { + // this.withSidebar = !this.withSidebar; + this.store.dispatch(new OnSidebarOpenOrCloseAction()); + } + + public openEditModuleNamePopup($event) { + const editModuleNameInstance = this.popoverService.createPopOverWithInnerComponent('Edit Module Name', '', {x:$event.x , y:$event.y }, EditModuleName, {selectModule: _.cloneDeep(this.selectedModule)}, 'top'); + editModuleNameInstance.innerPopoverContent.instance.clickButtonEvent.subscribe((newHeatName) => { + if(newHeatName != null){ + this.selectedModule.heatName = newHeatName; + this.updateHeatName(); + } + editModuleNameInstance.closePopover(); + }) + } +} @@ -0,0 +1,24 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {HierarchyTabComponent} from "./hierarchy-tab.component"; +import {UiElementsModule} from "../../../../../../components/ui/ui-elements.module"; +import {TranslateModule} from "../../../../../../shared/translator/translate.module"; +import {CommonModule} from "@angular/common"; +import {GlobalPipesModule} from "../../../../../../pipes/global-pipes.module"; +import { EditModuleName } from "../edit-module-name/edit-module-name.component"; + +@NgModule({ + declarations: [HierarchyTabComponent, EditModuleName], + imports: [CommonModule, + UiElementsModule, + SdcUiComponentsModule, + TranslateModule, + GlobalPipesModule], + entryComponents: [HierarchyTabComponent, EditModuleName], + exports: [HierarchyTabComponent, EditModuleName], +}) +export class HierarchyTabModule { +}
\ No newline at end of file @@ -0,0 +1,62 @@ +<div class="status-page"> + <ngx-datatable + class="material" + [columnMode]="'standard'" + [rowHeight]="'auto'" + [reorderable]="false" + [swapColumns]="false" + [rows]="artifacts" + [scrollbarH]="true" + #statusTable> + <ngx-datatable-row-detail [rowHeight]="'auto'"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div *ngFor="let status of row.statuses"> + <span class = "status" [attr.data-tests-id]="generateDataTestID('statusTimeStamp_',componentName, row.name, status.status)">{{ status.timeStamp | date:'short':'UTC'}}</span> + <span class = "status" [attr.data-tests-id]="generateDataTestID('statusValue_',componentName, row.name, status.status)">{{ status.status }}</span> + </div> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column name="Component ID" [resizeable]="false" [width]="250"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded" > + <div> + <span class="urlValue"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" (click)="expandRow(row)" [attr.data-tests-id]="generateDataTestID('expandIcon_compID_', componentName, row.name)"></svg-icon> + </span> + <span class="urlValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('compID_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="componentName"> + {{ componentName }} + </span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="280" name="Artifact Name"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('artName_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="row.name">{{ row.name }}</div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="380" name="URL"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div> + <span class="urlValue ellipsisCell" id="urlCell" [attr.data-tests-id]="generateDataTestID('url_',componentName, row.name)">{{ row.url }}</span> + <span class="urlCopyIcon" title="Copy URL"> + <svg-icon-label [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'" + [label]="" [name]="'copy-o'" [testId]="'copyToClipboard'" + (click)="copyToClipboard(row.url)"> + </svg-icon-label> + </span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="180" name="Time(UTC)"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('time_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="getLatestArtifact(row.name).timeStamp | date:'short':'UTC'">{{ getLatestArtifact(row.name).timeStamp | date:'short':'UTC'}}</div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="280" name="Status"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('status_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="getLatestArtifact(row.name).status">{{ getLatestArtifact(row.name).status }}</div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,78 @@ +:host ::ng-deep { + .ngx-datatable { + > div { + min-height: 5px; + } + } +} + +.datatable-header-cell { + text-align: left; + color: red; +} + +.statusHeaderTable { + color: #000000; + font-family: OpenSans-Bold, sans-serif; + font-size: 12px; + font-weight: bold; + float: left; +} + +.status { + padding-right: 30px; + color: #5a5a5a; + font-family: OpenSans-Regular, sans-serif; + font-size: 12px; +} + +.distributionIDBlock { + display: inline-block; +} + +.distributionRowContainer{ + background-color: #eaeaea; + text-align: center; +} + +.distributionRowLabel { + overflow: hidden; + padding-top: 10px; + color: #000000; + font-family: OpenSans-Semibold, sans-serif; + font-size: 12px; + font-weight: bold; +} + +.distributionRowValue { + color: #263d4d; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; +} + +.urlValue { + float: left; + color: #263d4d; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; +} + +.urlCopyIcon { + float: right; + width: 8%; +} + +.ellipsisCell { + width: 92%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + + + + + + + + @@ -0,0 +1,90 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { DistributionService } from '../../distribution.service'; +import { DistributionComponentArtifactTableComponent } from './distribution-component-artifact-table.component'; + +describe('DistributionComponentArtifactTableComponent', () => { + let fixture: ComponentFixture<DistributionComponentArtifactTableComponent>; + let distibutionServiceMock: Partial<DistributionService>; + + const mockArtifactsForDistributionAndComponentName = [ + { + name: 'Artifact1', + statuses: [ + {timeStamp: '7/25/2019 12:48AM', status: 'DEPLOY_OK'}, + {timeStamp: '7/25/2019 12:48AM', status: 'DOWNLOAD_OK'}, + {timeStamp: '7/25/2019 12:48AM', status: 'NOTIFIED'} + ], + url: 'URL1', + }, + { + name: 'Artifact2', + statuses: [ + {timeStamp: '7/26/2019 12:48AM', status: 'STATUS_TO_DISPLAY'}, + {timeStamp: '7/25/2019 12:48AM', status: 'DOWNLOAD_OK'}, + {timeStamp: '7/25/2019 12:48AM', status: 'NOTIFIED'} + ], + url: 'URL2', + }, + { + name: 'ArtifactWithNoStatuses', + url: 'URL2', + } + ]; + + beforeEach(() => { + + distibutionServiceMock = { + getArtifactstByDistributionIDAndComponentsName: jest.fn().mockReturnValue(mockArtifactsForDistributionAndComponentName), + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DistributionComponentArtifactTableComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DistributionService, useValue: distibutionServiceMock} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DistributionComponentArtifactTableComponent); + }); + + }); + + it('Get Latest Artifact (status and timeStamp) - So the Component Table will display the last time stamp of the notification', async () => { + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.getLatestArtifact('Artifact2')).toEqual({status: 'STATUS_TO_DISPLAY', timeStamp: '7/26/2019 12:48AM'}); + expect(fixture.componentInstance.getLatestArtifact('ArtifactWithNoStatuses')).toEqual(null); + }); + + it('Once the Distribution Component Artifact Table Component is created - artifacts will keep the relevant artifacts for a specific distributionID and Component Name', async () => { + await fixture.componentInstance.ngOnInit(); + // tslint:disable:no-string-literal + expect(fixture.componentInstance.artifacts.length).toBe(3); + expect(fixture.componentInstance.artifacts[0].name).toBe('Artifact1'); + expect(fixture.componentInstance.artifacts[0].url).toBe('URL1'); + expect(fixture.componentInstance.artifacts[0].statuses.length).toBe(3); + + expect(fixture.componentInstance.artifacts[1].name).toBe('Artifact2'); + }); + + it('Once the Distribution Component Artifact Table Component is created for Modal- artifacts will keep the relevant artifacts for a ' + + 'specific distributionID and Component Name filtered by Status', async () => { + fixture.componentInstance.statusFilter = 'DOWNLOAD_OK'; + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.artifacts.length).toBe(3); + expect(fixture.componentInstance.artifacts[0].name).toBe('Artifact1'); + expect(fixture.componentInstance.artifacts[0].url).toBe('URL1'); + + expect(fixture.componentInstance.artifacts[0].statuses.length).toBe(1); + expect(fixture.componentInstance.artifacts[0].statuses[0]).toEqual({status: 'DOWNLOAD_OK', timeStamp: '7/25/2019 12:48AM'}); + + }); +}); @@ -0,0 +1,68 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import * as _ from 'lodash'; +import { DistributionService } from '../../distribution.service'; + +// tslint:disable:no-string-literal + +@Component({ + selector: 'app-distribution-component-artifact-table', + templateUrl: './distribution-component-artifact-table.component.html', + styleUrls: ['./distribution-component-artifact-table.component.less'] +}) +export class DistributionComponentArtifactTableComponent implements OnInit { + + @ViewChild('statusTable', {}) table: any; + + @Input() componentName: string; + @Input() rowDistributionID: string; + @Input() statusFilter: string; + + public artifacts = []; + + constructor(private distributionService: DistributionService) { + } + + ngOnInit() { + this.artifacts = this.distributionService.getArtifactstByDistributionIDAndComponentsName(this.rowDistributionID, this.componentName); + if (this.statusFilter) { + this.artifacts.forEach( + (artifact) => { + artifact.statuses = _.filter(artifact.statuses, {status: this.statusFilter}); + }); + } + } + + public getLatestArtifact(artifactName: string) { + const selectedArtifact = this.artifacts.filter((artifact) => artifact.name === artifactName); + if (selectedArtifact && selectedArtifact[0] && selectedArtifact[0]['statuses'] && selectedArtifact[0]['statuses'][0]) { + return selectedArtifact[0]['statuses'][0]; + } else { + return null; + } + } + + private copyToClipboard(urlToCopy: any) { + + const inputForCopyToClipboard = document.getElementById('inputForCopyToClipboard') as HTMLInputElement; + inputForCopyToClipboard.value = urlToCopy; + /* Select the text field */ + inputForCopyToClipboard.select(); + + /* Copy the text inside the text field */ + document.execCommand('copy'); + + } + + private generateDataTestID(preFix: string, componentName: string, artifactName: string, status?: string) { + if (!status) { + return preFix + componentName + '_' + artifactName; + } else { + return preFix + status + '_' + componentName + '_' + artifactName; + } + } + + private expandRow(row: any) { + this.table.rowDetail.toggleExpandRow(row); + } + +} @@ -0,0 +1,47 @@ +<div > + <div class="distributionSummary" *ngIf="!isModal"> + <span class= "rightVerticalSeperator titleSummaryFontSettings" data-tests-id="totalDistributionArtifactsLabel">Total Artifacts <span class="blue" data-tests-id="totalDistributionArtifactsValue">{{ getTotalArtifactsForDistributionID(rowDistributionID) }} </span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'NOTIFIED')" data-tests-id="totalDistributionNotifiedArtifactsLabel">Notified <span data-tests-id="totalDistributionNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOTIFIED') }}</span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'DOWNLOAD_OK')" data-tests-id="totalDistributionDownloadedArtifactsLabel">Downloaded <span data-tests-id="totalDistributionDownloadedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_OK') }}</span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'DEPLOY_OK')" data-tests-id="totalDistributionDeployedArtifactsLabel">Deployed <span data-tests-id="totalDistributionDeployedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_OK') }}</span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'NOT_NOTIFIED')" data-tests-id="totalDistributionNotNotifiedArtifactsLabel">Not Notified <span data-tests-id="totalDistributionNotNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOT_NOTIFIED') }}</span></span> + <span class="blue rightVerticalSeperator floatRight" (click)="openModal(rowDistributionID,'DEPLOY_ERROR')" data-tests-id="totalDistributionDeployErrorArtifactsLabel">Deploy Errors <span class="red" data-tests-id="totalDistributionDeployErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_ERROR') }}</span></span> + <span class="blue rightVerticalSeperator floatRight" (click)="openModal(rowDistributionID,'DOWNLOAD_ERROR ')" data-tests-id="totalDistributionDownloadErrorArtifactsLabel">Download Errors <span class="red" data-tests-id="totalDistributionDownloadErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_ERROR') }}</span></span> + </div> + + <div class="distributionSummary" *ngIf="isModal"> + <span data-tests-id="modalStatusLabel"><a>Status {{ statusFilter }} <span class="blue" data-tests-id="statusValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, statusFilter) }}</span></a></span> + </div> + + + <div class="componentShiftLeft" *ngFor="let component of components"> + <div class="componentSummary" *ngIf="!isModal"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="isExpanded(component) ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" [attr.data-tests-id]="generateExpandDataTestID(component)" (click)="expandRow(component)"></svg-icon> + + + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, '')">{{ component }} <span class="blue" data-tests-id="totalComponentArtifactsValue">{{ getTotalArtifactsForDistributionID(rowDistributionID, component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Notified')">Notified <span class="blue" data-tests-id="totalComponentNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOTIFIED', component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Downloaded')">Downloaded <span class="blue" data-tests-id="totalComponentDownloadedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_OK', component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Deployed')">Deployed <span class="blue" data-tests-id="totalComponentDeployedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_OK', component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'NotNotified')">Not Notified <span class="blue" data-tests-id="totalComponentNotNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOT_NOTIFIED', component) }}</span></span> + <span class="msoStatus" [ngClass]="{'red': getMSOStatus (rowDistributionID, component) === 'COMPONENT_DONE_ERROR', 'green': getMSOStatus (rowDistributionID, component) === 'COMPONENT_DONE_OK'}">{{ getMSOStatus (rowDistributionID, component) }}</span> + <span class="rightVerticalSeperatorComponent floatRight titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'DeployErrors')">Deploy Errors <span class="red" data-tests-id="totalComponentDeployErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_ERROR', component) }}</span></span> + <span class="rightVerticalSeperatorComponent floatRight titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'DownloadErrors')">Download Errors <span class="red" data-tests-id="totalComponentDownloadErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_ERROR', component) }}</span></span> + </div> + + <div class="componentSummary" *ngIf="isModal"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="isExpanded(component) ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" [attr.data-tests-id]="generateExpandDataTestID(component+'_ForModal')" (click)="expandRow(component)"></svg-icon> + <span data-tests-id="modalComponentLabel"><a>{{ component }} <span class="blue" data-tests-id="modalComponentValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, statusFilter, component) }} </span></a></span> + </div> + + <div *ngIf="isExpanded(component)"> + <app-distribution-component-artifact-table [rowDistributionID]= rowDistributionID [componentName]=component + [statusFilter]="statusFilter"></app-distribution-component-artifact-table> + </div> + </div> +</div> + @@ -0,0 +1,66 @@ +.red { + color: red; +} + +.green { + color: green; +} + +.msoStatus { + padding-left: 5px; +} + +.blue { + color: #009fdb; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; +} + +.rightVerticalSeperator { + border-right: 1px solid #d2d2d2; + padding-left: 5px; + padding-right: 5px; + cursor: pointer; +} + +.rightVerticalSeperatorComponent { + border-right: 1px solid #d2d2d2; + padding-left: 5px; + padding-right: 5px; +} + +.floatRight{ + float: right; +} + +.distributionSummary { + padding-top: 5px; + padding-bottom: 5px; + background-color: #eaeaea; + padding-left: 25px; + padding-right: 25px; +} + +.componentSummary { + margin-top: 5px; + margin-bottom: 5px; + padding-top: 5px; + padding-bottom: 5px; + background-color: #eaeaea; + padding-left: 25px; + padding-right: 25px; +} + +.componentShiftLeft { + margin-left: 15px; +} + +.titleSummaryFontSettings { + color: #191919; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; +} + + + + @@ -0,0 +1,47 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ConfigureFn, configureTests } from '../../../../../../jest/test-config.helper'; +import { DistributionService } from '../distribution.service'; +import { DistributionComponentTableComponent } from './distribution-component-table.component'; + +describe('DistributionComponentTableComponent', () => { + let fixture: ComponentFixture<DistributionComponentTableComponent>; + let distibutionServiceMock: Partial<DistributionService>; + + const mockComponentsForDistribution = ['Consumer1', 'Consumer2']; + + beforeEach(() => { + + distibutionServiceMock = { + getComponentsByDistributionID: jest.fn().mockReturnValue(mockComponentsForDistribution), + getArtifactstByDistributionIDAndComponentsName: jest.fn(), + getArtifactsForDistributionIDAndComponentByStatus: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DistributionComponentTableComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DistributionService, useValue: distibutionServiceMock}, + {provide: SdcUiServices.ModalService, useValue: {}} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DistributionComponentTableComponent); + }); + + }); + + it('Once the Distribution Component Table Component is created - components will keep the relevant components for a specific distributionID', async () => { + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.components.length).toBe(2); + expect(fixture.componentInstance.components[0]).toBe('Consumer1'); + expect(fixture.componentInstance.components[1]).toBe('Consumer2'); + }); +}); @@ -0,0 +1,104 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component'; +import { DistributionComponent } from '../distribution.component'; +import { DistributionService } from '../distribution.service'; + +@Component({ + selector: 'app-distribution-component-table', + templateUrl: './distribution-component-table.component.html', + styleUrls: ['./distribution-component-table.component.less'] +}) +export class DistributionComponentTableComponent implements OnInit { + + @Input() rowDistributionID: string; + @Input() isModal: boolean = false; + @Input() statusFilter: string; + public components = []; + private customModalInstance: ModalComponent; + private expanded = []; + constructor(private distributionService: DistributionService, + private modalService: SdcUiServices.ModalService) { + } + + ngOnInit() { + this.initComponents(); + } + + private generateTotalComponentArtifactsLabel(componentName: any, status: string): string { + return 'total' + componentName + status + 'ArtifactsLabel'; + } + + private generateExpandDataTestID(componentName: string) { + return 'expandIcon_' + componentName; + } + + private initComponents() { + this.components = this.distributionService.getComponentsByDistributionID(this.rowDistributionID); + this.components.map((component) => this.expanded.push({componentName: component, expanded: false})); + } + + private getTotalArtifactsForDistributionID(distributionID: string, componentName?: string): number { + return this.distributionService.getArtifactstByDistributionIDAndComponentsName(distributionID, componentName).length; + } + + private getLengthArtifactsForDistributionIDByStatus(distributionID: string, statusToSerach: string, componentName?: string): number { + if (componentName) { + return this.distributionService.getArtifactsForDistributionIDAndComponentByStatus(distributionID, statusToSerach, componentName).length; + } else { + return this.distributionService.getArtifactsForDistributionIDAndComponentByStatus(distributionID, statusToSerach).length; + } + } + + private openModal(rowDistributionID: string, statusFilter: string) { + + const title: string = 'Distribution by Status'; + const attributeModalConfig = { + title, + size: 'sdc-xl', + type: SdcUiCommon.ModalType.custom, + buttons: [ + { + id: 'close', + text: 'Close', + size: 'sm', + closeModal: true, + disabled: false, + } + ] as SdcUiCommon.IModalButtonComponent[] + }; + + this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, DistributionComponent, { + // inputs + rowDistributionID, + statusFilter, + isModal: true, + }); + } + + private expandRow(componentName: string) { + console.log('Should expand componentSummary for componentName = ' + componentName); + const selectedComponent = this.expanded.find((component) => component.componentName === componentName); + // tslint:disable:no-string-literal + const selectedComponentExpandedVal = selectedComponent['expanded']; + // this.expanded = !this.expanded; + for (const i in this.expanded) { + if (this.expanded[i].componentName === componentName) { + this.expanded[i].expanded = !this.expanded[i].expanded; + break; //Stop this loop, we found it! + } + } + const selectedComponentAfter = this.expanded.find((component) => component.componentName === componentName); + const selectedComponentExpandedValAfter = selectedComponentAfter['expanded']; + } + + private isExpanded(componentName: string) { + const selectedComponent = this.expanded.find((component) => component.componentName === componentName); + return selectedComponent['expanded']; + } + + + private getMSOStatus(rowDistributionID: string, componentName: string): string { + return this.distributionService.getMSOStatus(rowDistributionID, componentName); + } +} @@ -0,0 +1,80 @@ +<div *ngIf="!isModal"> + <div *ngIf="serviceHasDistibutions" class="w-sdc-distribution-view-header"> + <div class="w-sdc-distribution-view-title" data-tests-id="DistributionsLabel">DISTRIBUTION <span class="blue-font" data-tests-id="totalArtifacts">[{{distributions.length}}]</span></div> + <div class="header-spacer"></div> + <input type="text" value="GeeksForGeeks" id="inputForCopyToClipboard" [ngStyle]="{'z-index': '-2', 'width': '25px'}"> + <div class="top-search"> + <input type="text" + style="width: auto;" + class="search-text" + data-tests-id="searchTextbox" + placeholder="Search" + data-ng-model="searchBind" + ng-model-options="{ debounce: 500 }" + (keyup)="updateFilter($event)"/> + </div> + <div class="sprite-new refresh-btn" data-tests-id="refreshButton" (click)="refreshDistributions()" title="Refresh"></div> + </div> + <div class="w-sdc-distribution-view-header w-sdc-distribution-view-title" data-tests-id="noDistributionsLabel" *ngIf="!serviceHasDistibutions">No Distributions To Present</div> +</div> + +<div *ngIf="serviceHasDistibutions"> + <ngx-datatable + [columnMode]="'flex'" + [rowHeight]="'auto'" + [reorderable]="false" + [swapColumns]="false" + [scrollbarV]="false" + [rows]="distributions" + [sorts]="[{prop: 'timestamp', dir: 'desc'}]" + + #distributionTable> + <ngx-datatable-row-detail [rowHeight]="'auto'"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <app-distribution-component-table [rowDistributionID]=row.distributionID [isModal]="isModal" + [statusFilter]="statusFilter"></app-distribution-component-table> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column [resizeable]="false" [flexGrow]="2" name="Distribution ID"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded" > + <div class="expand-collapse-cell"> + <a><svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" (click)="expandRow(row, expanded)" [attr.data-tests-id]="generateDataTestID('expandIcon_', row.distributionID, isModal)"></svg-icon></a> + + </div> + <div class="distributionIDBlock"> + <div class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('distID_', row.distributionID, isModal)">{{ row.distributionID }}</div> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="1" name="User id"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('userID_', row.distributionID)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="row.userId">{{ row.userId }}</div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="1" name="Time[UTC]"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('timeStamp_', row.distributionID)">{{ row.timestamp }} </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false"[flexGrow]="1" name="Status" > + <ng-template ngx-datatable-cell-template let-row="row"> + <div> + <span class="statusIcon"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]= "getIconName(row.deployementStatus)" [mode]="'primary'" + [size]="'medium'"></svg-icon> + </span> + <span class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('status_', row.distributionID)"> + {{ row.deployementStatus }} + </span> + <span class="btnMarkAsDistributed" (click)="markDeploy(row.distributionID, row.deployementStatus)"> + <svg-icon [clickable]="true" [name]= "'success'" [mode]="getIconMode(row.deployementStatus)" + [size]="'medium'"></svg-icon> + </span> + </div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div> @@ -0,0 +1,92 @@ +:host ::ng-deep { + .ngx-datatable { + > div { + min-height: 500px; + datatable-body { + max-height: max-content; + } + } + } +} + +.w-sdc-distribution-view-header { + display: flex; + -webkit-justify-content: space-between; + margin: 0 25px 5px 40px; + + .header-spacer { + flex-grow: 5; + } + + .w-sdc-distribution-view-title{ + color: #191919; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; + line-height: 30px; + + + .blue-font { + color: #009fdb; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + } + } + +} + +.distribution-page { + max-height: 150px; +} + + .distributionIDBlock { + display: inline-block; + } + + .expand-collapse-cell { + display: inline-block; + } + + .statusIcon { + display: inline-block; + margin-right: 10px; + } + + .btnMarkAsDistributed { + float: right; + background-color: #E5F3FF; + border: 1px solid #8DCCD5; + width: 55px; + height: 21px; + text-align: center; + } + + .distributionRowContainer{ + background-color: #eaeaea; + text-align: center; + } + + .distributionRowLabel { + overflow: hidden; + padding-top: 10px; + color: #000000; + font-family: OpenSans-Semibold, sans-serif; + font-size: 12px; + font-weight: bold; + } + + .distributionRowValue { + color: #263d4d; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; + } + +.ellipsisCell { + width: 92%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + + + + @@ -0,0 +1,92 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { AuthenticationService } from '../../../services/authentication.service'; +import { WorkspaceService } from '../workspace.service'; +import { DistributionComponent } from './distribution.component'; +import { DistributionService } from './distribution.service'; +import {EventListenerService} from "../../../../services/event-listener-service"; + +describe('DistributionComponent', () => { + let fixture: ComponentFixture<DistributionComponent>; + let distibutionServiceMock: Partial<DistributionService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let authenticationServiceMock: Partial <AuthenticationService>; + let eventListenerService: Partial <EventListenerService>; + + const mockDistributionListFromService = [ + { + deployementStatus: 'Distributed', + distributionID: '1', + timestamp: '2019-07-21 08:37:02.834 UTC', + userId: 'Aretha Franklin(op0001)' + }, { + deployementStatus: 'Distributed', + distributionID: '2', + timestamp: '2019-07-21 09:37:02.834 UTC', + userId: 'Aretha Franklin(op0001)' + }]; + + beforeEach(() => { + + distibutionServiceMock = { + initDistributionsList: jest.fn(), + getDistributionList: jest.fn().mockReturnValue(mockDistributionListFromService), + initDistributionsStatusForDistributionID: jest.fn() + }; + + const componentMetadata = new ComponentMetadata(); + componentMetadata.uuid = '111'; + + workspaceServiceMock = { + metadata : componentMetadata + }; + + authenticationServiceMock = { + getLoggedinUser: jest.fn().mockReturnValue({role: 'designer'}) + }; + + eventListenerService = { + registerObserverCallback: jest.fn(), + unRegisterObserver: jest.fn() + } + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DistributionComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DistributionService, useValue: distibutionServiceMock}, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}, + {provide: AuthenticationService, useValue: authenticationServiceMock}, + {provide: EventListenerService, useValue: eventListenerService} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DistributionComponent); + }); + + }); + + it('Once the Distribution Component is created - distributionsResponseFromServer save all the distributions from the Service', async () => { + fixture.componentInstance.componentUuid = 'componentUid'; + fixture.componentInstance.rowDistributionID = null; + fixture.componentInstance.isModal = false; + + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.distributions.length).toBe(2); + }); +}); @@ -0,0 +1,117 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { SdcUiCommon, SdcUiServices } from 'onap-ui-angular'; +import { EventListenerService } from '../../../../services/event-listener-service'; +import { AuthenticationService } from '../../../services/authentication.service'; +import { WorkspaceService } from '../workspace.service'; +import { DistributionService } from './distribution.service'; +import { EVENTS } from '../../../../utils/constants'; + +@Component({ + selector: 'distribution', + templateUrl: './distribution.component.html', + styleUrls: ['../../../../../assets/styles/table-style.less', './distribution.component.less'] +}) +export class DistributionComponent implements OnInit { + + @ViewChild('distributionTable', { }) table: any; + + @Input() isModal: boolean = false; + @Input() statusFilter: string; + @Input() rowDistributionID: string; + public componentUuid: string; + public distributions = []; + private expanded: any = {}; + private serviceHasDistibutions: boolean = false; + private readonly uniqueId: string; + private userRole: string; + + constructor(private workspaceService: WorkspaceService, + private distributionService: DistributionService, + private loaderService: SdcUiServices.LoaderService, + private authService: AuthenticationService, + private eventListenerService: EventListenerService) { + this.componentUuid = this.workspaceService.metadata.uuid; + this.uniqueId = this.workspaceService.metadata.uniqueId; + } + + + + async ngOnInit() { + this.userRole = this.authService.getLoggedinUser().role; + this.eventListenerService.registerObserverCallback(EVENTS.ON_DISTRIBUTION_SUCCESS, async () => { + await this.refreshDistributions(); + }); + await this.initDistributions(this.componentUuid, this.rowDistributionID); + } + + ngOnDestroy(): void { + this.eventListenerService.unRegisterObserver(EVENTS.ON_DISTRIBUTION_SUCCESS); + } + + async initDistributions(componentUuid: string, specificDistributionID?: string) { + this.loaderService.activate(); + await this.distributionService.initDistributionsList(componentUuid); + this.distributions = this.distributionService.getDistributionList(); + this.distributions.length > 0 ? this.serviceHasDistibutions = true : this.serviceHasDistibutions = false; + if (specificDistributionID) { + this.distributions = this.distributionService.getDistributionList(specificDistributionID); + } + this.loaderService.deactivate(); + } + + getIconName(rowStatus: string ) { + if (rowStatus === 'Distributed') { + return 'distributed'; + } + if (rowStatus === 'Deployed') { + return 'v-circle'; + } + } + + getIconMode(rowStatus: string) { + if (rowStatus === 'Distributed') { + return 'primary'; + } + if (rowStatus === 'Deployed') { + return 'secondary'; + } + } + + private async markDeploy(distributionID: string, status: string) { + if (status === 'Distributed') { + console.log('Should send MarkDeploy POST Request ServiceID:' + this.uniqueId + ' DISTID:' + distributionID); + await this.distributionService.markDeploy(this.uniqueId, distributionID); + this.refreshDistributions(); + } + } + + private async refreshDistributions() { + await this.initDistributions(this.componentUuid); + } + + private updateFilter(event) { + const val = event.target.value.toLowerCase(); + + // filter our data + this.distributions = this.distributionService.getDistributionList().filter((distribution: any[]) => { + return !val || + // tslint:disable:no-string-literal + distribution['distributionID'].toLowerCase().indexOf(val) !== -1; + }); + } + + private generateDataTestID(preFix: string, distributionID: string, isModal?: boolean ): string { + if (isModal) { + return preFix + distributionID.substring(0, 5) + '_Modal'; + } else { + return preFix + distributionID.substring(0, 5); + } + } + + private async expandRow(row: any, expanded: boolean) { + if (!expanded) { + await this.distributionService.initDistributionsStatusForDistributionID(row.distributionID); + } + this.table.rowDetail.toggleExpandRow(row); + } +} @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { DistributionComponentArtifactTableComponent } from './distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component'; +import { DistributionComponentTableComponent } from './distribution-component-table/distribution-component-table.component'; +import { DistributionComponent } from './distribution.component'; +import { DistributionService } from './distribution.service'; + +@NgModule({ + declarations: [ + DistributionComponent, + DistributionComponentTableComponent, + DistributionComponentArtifactTableComponent, + ], + imports: [ + // TranslateModule, + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + ], + exports: [ + DistributionComponent, + DistributionComponentTableComponent + ], + entryComponents: [ + DistributionComponent, + DistributionComponentTableComponent, + DistributionComponentArtifactTableComponent + ], + providers: [DistributionService] +}) +export class DistributionModule { +} @@ -0,0 +1,233 @@ +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { tap } from 'rxjs/operators'; +import { Distribution } from '../../../../models/distribution'; +import { ISdcConfig, SdcConfigToken } from '../../../config/sdc-config.config'; + +@Injectable() +export class DistributionService { + protected baseUrl; + private distributionList = []; + private distributionStatusesMap = {}; + + // tslint:disable:no-string-literal + + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { + this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; + } + + // Once the distribution page is loaded or when the user wants to refresh the list + async initDistributionsList(componentUuid: string): Promise<object> { + const distributionsListURL = this.baseUrl + 'services/' + componentUuid + '/distribution'; + const res = this.http.get<Distribution[]>(distributionsListURL).pipe(tap( (result) => { + this.distributionList = result['distributionStatusOfServiceList']; + this.insertDistrbutionsToMap(); + } )); + return res.toPromise(); + } + + // Once the user click on the relevant distribution ID in the distribution table (open and close) + async initDistributionsStatusForDistributionID(distributionID: string): Promise<object> { + const distributionStatus = this.baseUrl + 'services/distribution/' + distributionID; + const res = this.http.get<object>(distributionStatus).pipe(tap( (result) => { + this.insertDistributionStatusToDistributionsMap(distributionID, result['distributionStatusList']); + } )); + return res.toPromise(); + } + + public getDistributionList(specificDistributionID?: string) { + if (specificDistributionID) { + return this.distributionList.filter((distribution) => { + return distribution['distributionID'] === specificDistributionID; + }); + } else { + return this.distributionList; + } + } + + public getComponentsByDistributionID(distributionID: string) { + const components = []; + const distributionStatusMap = this.getStatusMapForDistributionID(distributionID); + if (distributionStatusMap) { + distributionStatusMap.forEach((component) => components.push(component.componentID)); + } + return components; + } + + // get array of artifacts per distributionID w/o componentName, sliced by artifact status + public getArtifactsForDistributionIDAndComponentByStatus(distributionID: string, statusToSearch: string, componentName?: string) { + const filteredArtifactsByStatus = []; + + if (componentName) { + this.getArtifactstByDistributionIDAndComponentsName(distributionID, componentName).forEach ( (artifact) => { + if (this.artifactStatusHasMatch(artifact, statusToSearch)) { + filteredArtifactsByStatus.push(artifact); + } + } ); + } else { + this.getArtifactstByDistributionIDAndComponentsName(distributionID).forEach ( (artifact) => { + if (this.artifactStatusHasMatch(artifact, statusToSearch)) { + filteredArtifactsByStatus.push(artifact); + } + } ); + } + return filteredArtifactsByStatus; + } + + public getArtifactstByDistributionIDAndComponentsName(distributionID: string, componentName?: string): any[] { + const artifacts = []; + if (this.getStatusMapForDistributionID(distributionID)) { + if (componentName) { + if (this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName).length > 0) { + const artifactsArr = this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName)[0]['artifacts'] + if (artifactsArr.length > 0) { + artifactsArr.forEach((artifact) => { + const artifactObj = { + url: artifact.artifactUrl, + name: artifact.artifactName, + statuses: artifact.statuses + }; + artifacts.push(artifactObj); + }); + } + } + } else { + const components = this.getComponentsByDistributionID(distributionID); + components.forEach((componentName) => { + if (this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName).length > 0) { + const artifactsArr = this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName)[0]['artifacts'] + if (artifactsArr.length > 0) { + artifactsArr.forEach((artifact) => { + const artifactObj = { + url: artifact.artifactUrl, + name: artifact.artifactName, + statuses: artifact.statuses + }; + artifacts.push(artifactObj); + }); + } + } + }); + } + } + return artifacts; + } + + public getStatusMapForDistributionID(distributionID: string) { + return this.distributionStatusesMap[distributionID]; + } + + public markDeploy(uniqueId: string, distributionID: string): Promise<object> { + const distributionStatus = this.baseUrl + 'services/' + uniqueId + '/distribution/' + distributionID + '/markDeployed'; + const res = this.http.post<object>(distributionStatus, {}).pipe(tap( (result) => { + console.log(result); + } )); + return res.toPromise(); + } + + public getMSOStatus(distributionID: string, componentName: string): string { + const msoStatus = this.distributionStatusesMap[distributionID].filter((component) => component.componentID === componentName)[0].msoStatus; + return msoStatus ? msoStatus : ''; + } + + private artifactStatusHasMatch(artifact: any, statusToSerach: string) { + for (let i = 0; i < artifact.statuses.length; i++) { + if (artifact.statuses[i].status === statusToSerach) { + return true; + } + } + return false; + } + + private insertDistributionStatusToDistributionsMap(distributionID: string, distributionStatusMapResponseFromServer: object[]) { + + // // Clear the Distribution ID array - to avoid statuses duplications + const distribution = this.distributionStatusesMap[distributionID]; + distribution.length = 0; + + // Sort the response of statuses from Server, so it will be easy to pop the latest status when it will be required + const sortedResponseByTimeStamp = distributionStatusMapResponseFromServer.sort((a, b) => b['timestamp'] - a['timestamp']) + + sortedResponseByTimeStamp.map((distributionStatus) => { + const formattedDate = this.formatDate(distributionStatus['timestamp']); + + // if (distributionStatus['url'] === null) { + // distributionStatus['url'] = ""; + // } + + const detailedArtifactStatus = { + componentID: distributionStatus['omfComponentID'], + artifactName: distributionStatus['url']? distributionStatus['url'].split('/').pop() : '', + url: distributionStatus['url'], + time: distributionStatus['timestamp'], + status: distributionStatus['status'], + }; + + + + // Add Component to this.distributionStatusesMap in case not exist. + let componentPosition = _.findIndex(distribution, {componentID: detailedArtifactStatus.componentID}) + + if (componentPosition === -1) { + this.addComponentIdToDistributionStatusMap(distributionID, detailedArtifactStatus.componentID); + componentPosition = distribution.length - 1; + } + + const component = distribution[componentPosition]; + + + // Add Artifact to this.distributionStatusesMap[componentID] in case not exist. + let artifactPosition = _.findIndex(component.artifacts, {artifactUrl: detailedArtifactStatus.url}) + + if (artifactPosition === -1) { + this.addArtifactToComponentId(distributionID, componentPosition, detailedArtifactStatus.artifactName, detailedArtifactStatus.url); + artifactPosition = component.artifacts.length - 1; + } + + + // Add status to relevat artifact in relevent componentID. + if (detailedArtifactStatus.url) { + // Case where there is a url -> should add its status + component.artifacts[artifactPosition].statuses.push({ + timeStamp: detailedArtifactStatus.time, + status: detailedArtifactStatus.status + }); + } else { + // Should update the Component -> status from MSO + this.distributionStatusesMap[distributionID][componentPosition].msoStatus = detailedArtifactStatus.status; + } + + + }); + } + + private addComponentIdToDistributionStatusMap(distributionID: string, componentIDValue: string) { + this.distributionStatusesMap[distributionID].push({ + componentID: componentIDValue, + msoStatus: null, + artifacts: [] + }); + } + + private addArtifactToComponentId(distributionID: string, componentPosition: number, artifactNameValue: string, artifactURLValue: any) { + if (artifactNameValue) { + this.distributionStatusesMap[distributionID][componentPosition].artifacts.push({ + artifactName: artifactNameValue, + artifactUrl: artifactURLValue, + statuses: [] + }); + } + } + + private insertDistrbutionsToMap() { + this.distributionList.map((distribution) => this.distributionStatusesMap[distribution.distributionID] = []); + } + + private formatDate(epochTime: string) { + const intEpochTime = new Date(parseInt(epochTime, 10)); + const amOrPm = (intEpochTime.getHours() + 24) % 24 > 12 ? 'PM' : 'AM'; + const formattedDate = (intEpochTime.getMonth() + 1) + '/' + intEpochTime.getDate() + '/' + intEpochTime.getFullYear() + ' ' + intEpochTime.getHours() + ':' + + intEpochTime.getMinutes() + amOrPm; + return formattedDate; + } +} @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`informational artifacts page should match current snapshot of informational artifact pages component 1`] = ` +<information-artifact-page + addOrUpdateArtifact={[Function Function]} + artifactsService={[Function Object]} + deleteArtifact={[Function Function]} + store={[Function Store]} + table={[Function DatatableComponent]} + workspaceService={[Function Object]} +> + <div + class="information-artifact-page" + > + <svg-icon-label + class="add-artifact-btn" + /> + <ngx-datatable + class="ngx-datatable" + columnmode="flex" + > + <div + visibilityobserver="" + > + + <datatable-body + class="datatable-body" + > + <datatable-selection> + + + + </datatable-selection> + </datatable-body> + + </div> + </ngx-datatable> + </div> +</information-artifact-page> +`; @@ -0,0 +1,82 @@ +<div class="information-artifact-page"> + <svg-icon-label class="add-artifact-btn" [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'" + [label]="'Add'" [name]="'plus'" [testId]="'add-information-artifact-button'" + (click)="addOrUpdateArtifact()"></svg-icon-label> + <ngx-datatable + columnMode="flex" + [headerHeight]="40" + [reorderable]="false" + [swapColumns]="false" + [rows]="informationArtifacts$ | async" + [footerHeight]="'undefined'" + [sorts]="[{prop: 'artifactDisplayName', dir: 'desc'}]" + #informationArtifactsTable + (activate)="onActivate($event)"> + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div [attr.data-tests-id]="row.artifactDisplayName+'Description'">{{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="3" + [prop]="'artifactDisplayName'"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span [attr.data-tests-id]="'artifactDisplayName_' + row.artifactDisplayName">{{row.artifactDisplayName }}</span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.artifactType}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.artifactVersion }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="UUID" [flexGrow]="2"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.artifactUUID }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class="download-artifact-button"> + <svg-icon class="action-icon" *ngIf="!row.isThirdParty()" [mode]="'primary2'" + [disabled]="isViewOnly$ | async" [name]="'edit-o'" + testId="edit_{{row.artifactDisplayName}}" clickable="true" size="medium" + (click)="addOrUpdateArtifact(row)"></svg-icon> + <svg-icon class="action-icon" *ngIf="!row.isThirdParty()" [mode]="'primary2'" + [disabled]="isViewOnly$ | async" [name]="'trash-o'" + testId="delete_{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="deleteArtifact(row)"></svg-icon> + <download-artifact class="action-icon" [disabled]="isViewOnly$ | async" [artifact]="row" + [componentId]="componentId" + [componentType]="componentType" + testId="download_{{row.artifactDisplayName}}"></download-artifact> + </div> + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-footer> + <ng-template ngx-datatable-footer-template> + <div class="add-artifacts-dynamic-btn-list"> + <sdc-button *ngFor="let artifact of informationArtifactsAsButtons$ | async" + class="add-artifacts-dynamic-btn" + testId="add_artifact_{{artifact.artifactDisplayName}}" + [type]="'secondary'" + [size]="'xx-large'" + [text]="'ADD ' + artifact.artifactDisplayName" + [icon_name]="'plus-circle-o'" + [icon_mode] = "'secondary'" + [icon_position]="'left'" + (click)="addOrUpdateArtifact(artifact)"> + </sdc-button> + </div> + </ng-template> + </ngx-datatable-footer> + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,29 @@ +.information-artifact-page { + + .add-artifact-btn { + display: flex; + cursor: pointer; + justify-content: flex-end; + margin-bottom: 10px; + } + .download-artifact-button { + display: flex; + justify-content: center; + + .action-icon{ + margin-right: 10px; + } + } + + .add-artifacts-dynamic-btn-list { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin: 20px 0px; + .add-artifacts-dynamic-btn{ + width: 350px; + margin-top: 15px; + } + } +}
\ No newline at end of file @@ -0,0 +1,69 @@ +import {Component, OnInit, ViewChild} from "@angular/core"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiCommon, SdcUiComponents, SdcUiServices} from "onap-ui-angular"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import * as _ from "lodash"; +import {ArtifactGroupType, ArtifactType} from "../../../../utils/constants"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; +import {DeleteArtifactAction, GetArtifactsByTypeAction} from "../../../store/actions/artifacts.action"; +import {Select, Store} from "@ngxs/store"; +import {Observable} from "rxjs/index"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {map} from "rxjs/operators"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import {ArtifactModel} from "../../../../models/artifacts"; + +@Component({ + selector: 'information-artifact-page', + templateUrl: './information-artifact-page.component.html', + styleUrls: ['./information-artifact-page.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class InformationArtifactPageComponent implements OnInit { + + public componentId: string; + public componentType: string; + public informationArtifacts$: Observable<ArtifactModel[]>; + public informationArtifactsAsButtons$: Observable<ArtifactModel[]>; + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + @ViewChild('informationArtifactsTable') table: any; + + constructor(private workspaceService: WorkspaceService, + private artifactsService: ArtifactsService, + private store: Store) { + } + + ngOnInit(): void { + this.componentId = this.workspaceService.metadata.uniqueId; + this.componentType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new GetArtifactsByTypeAction({ + componentType: this.componentType, + componentId: this.componentId, + artifactType: ArtifactGroupType.INFORMATION + })); + + let artifacts = this.store.select(ArtifactsState.getArtifactsByType).pipe(map(filterFn => filterFn(ArtifactType.INFORMATION))); + this.informationArtifacts$ = artifacts.pipe(map(artifacts => _.filter(artifacts, (artifact) => { + return artifact.esId; + }))); + + this.informationArtifactsAsButtons$ = artifacts.pipe(map(artifacts => _.filter(artifacts, (artifact) => { + return !artifact.esId; + }))); + } + + onActivate(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + public addOrUpdateArtifact = (artifact: ArtifactModel, isViewOnly?: boolean) => { + this.artifactsService.openArtifactModal(this.componentId, this.componentType, artifact, ArtifactGroupType.INFORMATION, isViewOnly); + } + + public deleteArtifact = (artifactToDelete) => { + this.artifactsService.deleteArtifact(this.componentType, this.componentId, artifactToDelete) + } + +}
\ No newline at end of file @@ -0,0 +1,30 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; +import {InformationArtifactPageComponent} from "./information-artifact-page.component"; +import {ArtifactFormModule} from "../../../components/forms/artifacts-form/artifact-form.module"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; + +@NgModule({ + declarations: [ + InformationArtifactPageComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + UiElementsModule, + ArtifactFormModule + ], + exports: [ + InformationArtifactPageComponent + ], + entryComponents: [ + InformationArtifactPageComponent + ], + providers:[ArtifactsService] +}) +export class InformationArtifactPageModule { +} @@ -0,0 +1,77 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {Observable} from "rxjs/Observable"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import 'rxjs/add/observable/of'; +import {NgxsModule, Store} from "@ngxs/store"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {InformationArtifactPageComponent} from "./information-artifact-page.component"; +import { informationalArtifactsMock} from "../../../../../jest/mocks/artifacts-mock"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; + +describe('informational artifacts page', () => { + + let fixture: ComponentFixture<InformationArtifactPageComponent>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let store: Store; + + beforeEach( + async(() => { + + topologyTemplateServiceMock = { + getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(informationalArtifactsMock)) + }; + workspaceServiceMock = {metadata: <ComponentMetadata>{uniqueId: 'service_unique_id', componentType: 'SERVICE'}} + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [InformationArtifactPageComponent], + imports: [NgxDatatableModule, NgxsModule.forRoot([ArtifactsState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }, + {provide: ArtifactsService, useValue: {}}, + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(InformationArtifactPageComponent); + store = testBed.get(Store); + }); + }) + ); + + it('should match current snapshot of informational artifact pages component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should see exactly 3 informational artifacts and six buttons to add artifact by template', () => { + fixture.componentInstance.ngOnInit(); + fixture.componentInstance.informationArtifacts$.subscribe((artifacts)=> { + expect(artifacts.length).toEqual(3); + }) + fixture.componentInstance.informationArtifactsAsButtons$.subscribe((artifacts)=> { + expect(artifacts.length).toEqual(6); + }) + + store.selectOnce(state => state.artifacts.artifacts).subscribe(artifacts => { + expect(artifacts.length).toEqual(9); + }); + }) + + +});
\ No newline at end of file @@ -0,0 +1,22 @@ +<div class="capabilities-properties-table"> + <ngx-datatable #componentsMetadataTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rows]="capabilitiesProperties" + [sorts]="[{prop: 'name', dir: 'desc'}]"> + <ngx-datatable-column *ngFor="let column of capabilityPropertiesColumns" [ngSwitch]="column.prop" [resizeable]="false" + [draggable]="false" name={{column.name}} [flexGrow]="column.flexGrow"> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'name'"> + <a data-tests-id="row[column.prop]" sdc-tooltip [tooltip-text]="row[column.prop]" (click)="updateProperty(row)">{{row[column.prop]}}</a> + </ng-template> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'schema'"> + <span *ngIf="row[column.prop] && row[column.prop].property" data-tests-id="row[column.prop].property.type" + sdc-tooltip [tooltip-text]="row[column.prop].property.type">{{row[column.prop].property.type}}</span> + </ng-template> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchDefault> + <span data-tests-id="row[column.prop]" sdc-tooltip [tooltip-text]="row[column.prop]" [tooltip-placement]="3">{{row[column.prop]}}</span> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,9 @@ + +:host ::ng-deep { + .ngx-datatable { + > div { + min-height: auto !important; + } + + } +}
\ No newline at end of file @@ -0,0 +1,33 @@ + +import { ViewChild, Input, OnInit, Component } from "@angular/core"; +import {SdcUiServices} from "onap-ui-angular"; +import { ModalsHandler } from "../../../../../../utils/modals-handler"; +import { WorkspaceService } from "../../../workspace.service"; +import { PropertyModel } from "../../../../../../models/properties"; + + +@Component({ + selector: 'capabilities-properties', + templateUrl: './capabilities-properties.html', + styleUrls: ['./capabilities-properties.less', '../../../../../../../assets/styles/table-style.less'] +}) +export class CapabilitiesPropertiesComponent { + @Input() public capabilitiesProperties: Array<PropertyModel> = []; + + private capabilityPropertiesColumns = [ + {name: 'Name', prop: 'name', flexGrow: 1}, + {name: 'Type', prop: 'type', flexGrow: 1}, + {name: 'Schema', prop: 'schema', flexGrow: 1}, + {name: 'Description', prop: 'description', flexGrow: 1}, + ]; + constructor(private modalsHandler: ModalsHandler, + private workspaceService: WorkspaceService) {} + + private updateProperty(property: PropertyModel): void { + _.forEach(this.capabilitiesProperties, (prop: PropertyModel) => { + prop.readonly = true; + }); + this.modalsHandler.openEditPropertyModal(property, this.workspaceService.metadata, this.capabilitiesProperties, false, 'component', + this.workspaceService.metadata.uniqueId); + } +}
\ No newline at end of file @@ -0,0 +1,59 @@ +<div class="capabilities-table"> + <div class="expand-collapse-all-rows"> + <svg-icon class="selected-all-capabilities" + [mode]="'primary'" [clickable]="true" [name]="'expand-o'" + [size]="'medium'" (click)="capabilitiesTable.rowDetail.expandAllRows()"> + </svg-icon> + <svg-icon class="unselected-all-capabilities" + [mode]="'primary'" [clickable]="true" [name]="'minimize-o'" + [size]="'medium'" (click)="capabilitiesTable.rowDetail.collapseAllRows()"> + </svg-icon> + </div> + <ngx-datatable #capabilitiesTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rows]="capabilities" + (select)="onSelectCapabilities($event)" + [selectionType]="'single'"> + <ngx-datatable-row-detail [rowHeight]="undefiend"> + <ng-template let-row="row" ngx-datatable-row-detail-template> + <div class="properties-title">Properties</div> + <capabilities-properties [capabilitiesProperties]="row.properties"></capabilities-properties> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column name="Name" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" (click)="expendRow(row)"></svg-icon> + <span data-tests-id="row.name" sdc-tooltip [tooltip-text]="row.name" [tooltip-placement]="3" (click)="editCapability(row)">{{row.name}}</span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Type" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.type" sdc-tooltip [tooltip-text]="row.type" [tooltip-placement]="3">{{row.type ? row.type.replace("tosca.capabilities.",""): ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Description" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.description" sdc-tooltip [tooltip-text]="row.description" [tooltip-placement]="3">{{row.description}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Valid Source" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.validSourceTypes.join(',')" sdc-tooltip [tooltip-text]="row.validSourceTypes ? row.validSourceTypes.join(',') : null" [tooltip-placement]="3"> + {{row.validSourceTypes ? row.validSourceTypes.join(','): ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Occurrences" [flexGrow]="1" [prop]="'minOccurrences'" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.minOccurrences+','+row.maxOccurrences" sdc-tooltip + [tooltip-text]="row.minOccurrences+','+row.maxOccurrences" [tooltip-placement]="3"> + {{row.minOccurrences}},{{row.maxOccurrences}}</span> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,16 @@ +:host ::ng-deep { + .datatable-row-detail { + width: 1260px; + } + .datatable-body-row { + cursor: pointer; + } +} +.expand-collapse-all-rows { + position: absolute; + top: 172px; + left: 890px; +} +.properties-title { + padding-bottom: 10px; +}
\ No newline at end of file @@ -0,0 +1,79 @@ +import {Capability, CapabilityUI} from "../../../../../models/capability"; +import { ViewChild, Input, OnInit, Component } from "@angular/core"; +import {SdcUiServices} from "onap-ui-angular"; +import {CapabilitiesEditorComponent} from "./capabilityEditor/capabilities-editor.component"; +import {WorkspaceService} from "../../workspace.service"; +import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service"; +import {ReqAndCapabilitiesService} from "../req-and-capabilities.service"; +import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component"; +import {EventListenerService} from "../../../../../services/event-listener-service"; + + +@Component({ + selector: 'capabilities', + templateUrl: './capabilities.component.html', + styleUrls: ['./capabilities.component.less','../../../../../../assets/styles/table-style.less'] +}) +export class CapabilitiesComponent { + @Input() public capabilities: Array<Capability>; + @ViewChild('capabilitiesTable') capabilitiesTable: any; + private customModalInstance: ModalComponent; + + constructor( + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private reqAndCapabilitiesService : ReqAndCapabilitiesService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService) { + } + + private onSelectCapabilities({ selected }) { + } + + editCapability(cap: CapabilityUI) { + let modalConfig = { + size: 'md', + title: 'Update Capability', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Update'), + size: "'x-small'", + callback: () => this.updateCapability(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + capability: cap, + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, CapabilitiesEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + } + + expendRow(row) { + this.capabilitiesTable.rowDetail.toggleExpandRow(row); + } + + private updateCapability() { + const capability = this.customModalInstance.innerModalContent.instance.capabilityData; + this.loaderService.activate(); + if (capability.uniqueId) { + this.topologyTemplateService.updateCapability(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, capability).subscribe((result) => { + let index = this.capabilities.findIndex((cap) => result[0].uniqueId === cap.uniqueId); + this.capabilities[index] = new CapabilityUI(result[0], this.workspaceService.metadata.uniqueId); + this.loaderService.deactivate(); + this.eventListenerService.notifyObservers('CAPABILITIES_UPDATED'); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + +}
\ No newline at end of file @@ -0,0 +1,93 @@ +<div class="capability-editor"> + <form class="w-sdc-form"> + <div class="i-sdc-form-content-capability-content"> + <div class="content-row"> + <div class="i-sdc-form-item"> + <sdc-input + label="{{ 'CAP_NAME' | translate }}" + required="true" + class="i-sdc-form-input" + testId="capName" + [disabled]="isReadonly" + [(value)]="capabilityData.name" + (valueChange)="validityChanged()"> + </sdc-input> + </div> + </div> + + <div class="group-with-border"> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'CAP_TYPE' | translate }}" + required="true" + class="i-sdc-form-select" + testId="capType" + [disabled]="isReadonly" + [options]="capabilityTypesMappedList" + selectedOption="{{ capabilityData.type }}" + [placeHolder] = "capabilityData.type" + (changed)="onSelectCapabilityType($event)"> + </sdc-dropdown> + </div> + <div class="content-row i-sdc-form-item"> + <label class="i-sdc-form-label"> {{ 'CAP_DESCRIPTION' | translate }} </label> + <textarea + rows="3" + class="i-sdc-form-input description" + data-tests-id="capDesc" + disabled + value="{{capabilityData.description}}"> + </textarea> + </div> + <div class="content-row i-sdc-form-item"> + <label class="i-sdc-form-label valid-source-label"> {{ 'CAP_VALID_SOURCE' | translate }} </label> + <textarea + rows="2" + class="i-sdc-form-input" + data-tests-id="capValidSrc" + disabled + value="{{capabilityData.validSourceTypes}}"> + </textarea> + </div> + </div> + + <label class="i-sdc-form-label occurrences-label"> {{ 'REQ_CAP_OCCURRENCES' | translate }} </label> + <div class="content-row occurrences-section"> + <div class="min-occurrences-value"> + <sdc-input + label="{{ 'REQ_CAP_OCCURRENCES_MIN' | translate }}" + class="i-sdc-form-input" + testId="capOccurrencesMin" + [disabled]="isReadonly" + [(value)]="capabilityData.minOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + <div class="sdc-input"> + <label class="sdc-input__label"> {{ 'REQ_CAP_OCCURRENCES_MAX' | translate }} </label> + <div class="max-occurrences-value"> + <sdc-checkbox + class="checkbox-label unbounded-value" + testId="capOccurrencesMaxUnbounded" + label="{{translatedUnboundTxt.toLowerCase()}}" + (checkedChange)="onUnboundedChanged()" + [checked]="isUnboundedChecked" + [disabled]="isReadonly"> + </sdc-checkbox> + + <sdc-input + *ngIf="!isUnboundedChecked" + class="i-sdc-form-input" + testId="capOccurrencesMax" + [disabled]="isReadonly" + [(value)]="capabilityData.maxOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + </div> + </div> + </div> + </form> +</div>
\ No newline at end of file @@ -0,0 +1,38 @@ +@import '../../../../../../../assets/styles/variables.less'; + +.capability-editor { + .i-sdc-form-content-capability-content { + padding: 10px 25px; + .group-with-border { + margin: 25px 0; + padding: 15px 0; + border-top: 1px solid @tlv_color_u; + border-bottom: 1px solid @tlv_color_u; + .content-row:not(:last-of-type) { + padding-bottom: 13px; + } + } + + .occurrences-label { + font-family: @font-opensans-bold; + margin-bottom: 19px; + } + .occurrences-section, /deep/ .max-occurrences-value { + display: flex; + .min-occurrences-value { + padding-right: 30px; + } + .unbounded-value { + padding-top: 7px; + padding-right: 20px; + .sdc-checkbox__label { + text-transform: capitalize; + } + } + } + textarea { + min-height: unset; + height: unset; + } + } +}
\ No newline at end of file @@ -0,0 +1,81 @@ +import {Component} from '@angular/core'; +import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; +import {Capability, CapabilityTypeModel} from 'app/models'; +import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {TranslateService} from 'app/ng2/shared/translator/translate.service'; +import {Subject} from "rxjs"; + +@Component({ + selector: 'capabilities-editor', + templateUrl: './capabilities-editor.component.html', + styleUrls: ['./capabilities-editor.component.less'], + providers: [ServiceServiceNg2] +}) + +export class CapabilitiesEditorComponent { + input: { + test: string, + capability: Capability, + capabilityTypesList: Array<CapabilityTypeModel>, + isReadonly: boolean; + }; + capabilityData: Capability; + capabilityTypesMappedList: Array<DropdownValue>; + isUnboundedChecked: boolean; + isReadonly: boolean; + translatedUnboundTxt: string; + + public onValidationChange: Subject<boolean> = new Subject(); + + constructor(private translateService: TranslateService) { + } + + ngOnInit() { + this.capabilityData = new Capability(this.input.capability); + this.translatedUnboundTxt = ''; + this.capabilityData.minOccurrences = this.capabilityData.minOccurrences || 0; + this.translateService.languageChangedObservable.subscribe(lang => { + this.translatedUnboundTxt = this.translateService.translate('REQ_CAP_OCCURRENCES_UNBOUNDED'); + this.capabilityData.maxOccurrences = this.capabilityData.maxOccurrences || this.translatedUnboundTxt; + this.isUnboundedChecked = this.capabilityData.maxOccurrences === this.translatedUnboundTxt; + }); + this.capabilityTypesMappedList = _.map(this.input.capabilityTypesList, capType => new DropdownValue(capType.toscaPresentation.type, capType.toscaPresentation.type)); + this.isReadonly = this.input.isReadonly; + this.validityChanged(); + } + + onUnboundedChanged() { + this.isUnboundedChecked = !this.isUnboundedChecked; + this.capabilityData.maxOccurrences = this.isUnboundedChecked ? this.translatedUnboundTxt : null; + this.validityChanged(); + } + + checkFormValidForSubmit() { + return this.capabilityData.name && this.capabilityData.name.length && + this.capabilityData.type && this.capabilityData.type.length && !_.isEqual(this.capabilityData.minOccurrences, "") && this.capabilityData.minOccurrences >= 0 && + ( + this.isUnboundedChecked || + (this.capabilityData.maxOccurrences && (this.capabilityData.minOccurrences <= parseInt(this.capabilityData.maxOccurrences))) + ); + } + + onSelectCapabilityType(selectedCapType: DropdownValue) { + this.capabilityData.type = selectedCapType && selectedCapType.value; + if (selectedCapType && selectedCapType.value) { + let selectedCapabilityTypeObj: CapabilityTypeModel = this.input.capabilityTypesList.find(capType => capType.toscaPresentation.type === selectedCapType.value); + this.capabilityData.description = selectedCapabilityTypeObj.toscaPresentation.description; + this.capabilityData.validSourceTypes = selectedCapabilityTypeObj.toscaPresentation.validTargetTypes; + this.capabilityData.properties = _.forEach( + _.toArray(selectedCapabilityTypeObj.properties), + prop => prop.uniqueId = null //a requirement for the BE + ); + } + this.validityChanged(); + } + + validityChanged = () => { + let validState = this.checkFormValidForSubmit(); + this.onValidationChange.next(validState); + } + +}
\ No newline at end of file @@ -0,0 +1,29 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CapabilitiesEditorComponent} from './capabilities-editor.component'; +import {FormsModule} from '@angular/forms'; +import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module'; +import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module'; +import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import {SdcUiComponentsModule} from 'onap-ui-angular'; +// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; + +@NgModule({ + declarations: [ + CapabilitiesEditorComponent + ], + imports: [CommonModule, + FormsModule, + FormElementsModule, + UiElementsModule, + TranslateModule, + SdcUiComponentsModule + ], + exports: [], + entryComponents: [ + CapabilitiesEditorComponent + ], + providers: [] +}) +export class CapabilitiesEditorModule { +}
\ No newline at end of file @@ -0,0 +1,21 @@ +<div class="workspace-req-and-cap"> + <div> + <span class="addTitle" *ngIf="selectTabName === 'REQUIREMENTS'" (click)="addRequiremnet()">Add Requirement</span> + <span class="addTitle" *ngIf="selectTabName !== 'REQUIREMENTS'" (click)="addCapability()">Add Capability</span> + <span class="req-and-cap-filter" *ngIf="notEmptyTable"> + <sdc-filter-bar + [placeHolder]="'Search'" + (keyup)="updateFilter($event)" + [testId]="'search-box'"> + </sdc-filter-bar> + </span> + </div> + <sdc-tabs (selectedTab)="selectTab($event)" [tabStyle]="'sdc-table-tab'"> + <sdc-tab [title]="'Requirements('+(requirements.length||'0')+')'" [active]="true" [testId]="'req-tab'"> + <div #requirmentsContainer></div> + </sdc-tab> + <sdc-tab [title]="'Capabilities('+(capabilities.length||'0')+')'" [active]="false" [testId]="'cap-tab'"> + <div #capabilitiesContainer></div> + </sdc-tab> + </sdc-tabs> +</div>
\ No newline at end of file @@ -0,0 +1,19 @@ +.req-and-cap-filter { + width: 336px; + float: right; + margin-right: 10px; +} + +.addTitle { + float: right; + text-transform: uppercase; + font-family: OpenSans-Semibold, sans-serif; + color: #009fdb; + cursor: pointer; +} + +:host ::ng-deep .sdc-tabs { + .sdc-tab-content { + margin-top: 0; + } +} @@ -0,0 +1,127 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA} from "@angular/core"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; + +import {Observable} from "rxjs/Observable"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {SdcUiServices, SdcUiCommon} from "onap-ui-angular"; +import 'rxjs/add/observable/of'; +import {ReqAndCapabilitiesComponent} from "./req-and-capabilities.component"; +import {ReqAndCapabilitiesService} from "./req-and-capabilities.service"; +import {WorkspaceService} from "../workspace.service"; +import { + capabilitiesMock, + filterRequirmentsMock, + requirementMock +} from "../../../../../jest/mocks/req-and-capabilities.mock"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import { TopologyTemplateService } from "../../../services/component-services/topology-template.service"; +import {EventListenerService} from "../../../../services/event-listener-service"; + +describe('req and capabilities component', () => { + + let fixture: ComponentFixture<ReqAndCapabilitiesComponent>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let createDynamicComponentServiceMock: Partial<SdcUiServices.CreateDynamicComponentService> + let reqAndCapabilitiesService: Partial<ReqAndCapabilitiesService>; + let modalService: Partial<SdcUiServices.ModalService>; + let eventListenerService: Partial<EventListenerService>; + + + + beforeEach( + async(() => { + + workspaceServiceMock = { + metadata: new ComponentMetadata() + }; + + topologyTemplateServiceMock = { + getRequirementsAndCapabilitiesWithProperties: jest.fn().mockImplementation(() => + Observable.of({requirements: {'tosca.requirements.Node': requirementMock}, + capabilities: {'tosca.capabilities.Node': capabilitiesMock}})) + }; + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + } + createDynamicComponentServiceMock = { + insertComponentDynamically: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ReqAndCapabilitiesComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: WorkspaceService, useValue: workspaceServiceMock }, + { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }, + { provide: TopologyTemplateService, useValue: topologyTemplateServiceMock }, + { provide: SdcUiServices.CreateDynamicComponentService, useValue: createDynamicComponentServiceMock }, + { provide: ReqAndCapabilitiesService, useValue: reqAndCapabilitiesService }, + { provide: SdcUiServices.ModalService, useValue: modalService }, + { provide: EventListenerService, useValue: eventListenerService } + ], + }); + }; + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ReqAndCapabilitiesComponent); + }); + }) + ); + + it('should see exactly 2 requirement in requirements table when call initCapabilitiesAndRequirements and meta data requirements null', () => { + workspaceServiceMock.metadata.requirements = null; + fixture.componentInstance.initCapabilitiesAndRequirements(); + expect(workspaceServiceMock.metadata.requirements["tosca.requirements.Node"].length).toBe(3); + }); + it('should see exactly 2 capabilities in capabilities table when call initCapabilitiesAndRequirements and meta data capabilities null', () => { + workspaceServiceMock.metadata.capabilities = null; + fixture.componentInstance.initCapabilitiesAndRequirements(); + expect(workspaceServiceMock.metadata.capabilities["tosca.capabilities.Node"].length).toBe(2); + }); + + it('capabilities array papulated when call populateReqOrCap with capabilities', () => { + workspaceServiceMock.metadata.capabilities = {"tosca.capabilities.Node": capabilitiesMock, "tosca.capabilities.Scalable": capabilitiesMock}; + fixture.componentInstance.populateReqOrCap("capabilities"); + expect(fixture.componentInstance.capabilities.length).toBe(4); + }); + + it('create requirements component when call loadReqOrCap with true', () => { + createDynamicComponentServiceMock.insertComponentDynamically.mockImplementation(() => { return {instance: {requirements: requirementMock}}}); + fixture.componentInstance.requirements = requirementMock; + fixture.componentInstance.loadReqOrCap(true); + expect(fixture.componentInstance.instanceRef.instance.requirements.length).toEqual(3); + }); + + it('create capabilities component when call loadReqOrCap with false', () => { + fixture.componentInstance.instanceRef = {instance: {requirements: null}}; + createDynamicComponentServiceMock.insertComponentDynamically.mockImplementation(() => { return {instance: {capabilities: capabilitiesMock}}}); + fixture.componentInstance.capabilities = capabilitiesMock; + fixture.componentInstance.requirementsUI = filterRequirmentsMock; + let event = { + target : { + value : 'root' + } + } + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.instanceRef.instance.requirements.length).toBe(1); + }); + + it('should filter 1 capabilities when searching and call updateFilter function and instanceRef is capabilities component', () => { + fixture.componentInstance.instanceRef = {instance: {capabilities: null}}; + fixture.componentInstance.capabilities = capabilitiesMock; + fixture.componentInstance.selectTabName = 'CAPABILITIES'; + let event = { + target : { + value : '1source' + } + } + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.instanceRef.instance.capabilities[0].type).toBe("tosca.capabilities.Node"); + }); +}); @@ -0,0 +1,229 @@ +import { Component, ComponentRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Capability, CapabilityUI } from '../../../../models/capability'; +import { Requirement, RequirementUI } from '../../../../models/requirement'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../services/responses/component-generic-response'; +import { WorkspaceService } from '../workspace.service'; +import { CapabilitiesComponent } from './capabilities/capabilities.component'; +import { RequirmentsComponent } from './requirements/requirments.components'; +import {ReqAndCapabilitiesService} from "./req-and-capabilities.service"; +import {CapabilitiesEditorComponent} from "./capabilities/capabilityEditor/capabilities-editor.component"; +import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component"; +import {EventListenerService} from "../../../../services/event-listener-service"; +import {RequirementsEditorComponent} from "./requirements/requirementEditor/requirements-editor.component"; + +@Component({ + selector: 'req-and-capabilities', + templateUrl: './req-and-capabilities.component.html', + styleUrls: ['./req-and-capabilities.component.less'] +}) +export class ReqAndCapabilitiesComponent implements OnInit { + + @ViewChild('requirmentsContainer', { read: ViewContainerRef }) requirmentsContainer: ViewContainerRef; + @ViewChild('capabilitiesContainer', { read: ViewContainerRef }) capabilitiesContainer: ViewContainerRef; + private requirements: Requirement[] = []; + private requirementsUI: RequirementUI[] = []; + private capabilities: Capability[] = []; + private selectTabName: string = 'REQUIREMENTS'; + private notEmptyTable: boolean = true; + private instanceRef: ComponentRef<any>; + private customModalInstance: ModalComponent; + readonly INPUTS_FOR_CAPABILITIES: string = 'INPUTS_FOR_CAPABILITIES'; + readonly INPUTS_FOR_REQUIREMENTS: string = 'INPUTS_FOR_REQUIREMENTS'; + + constructor(private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private createDynamicComponentService: SdcUiServices.CreateDynamicComponentService, + private reqAndCapabilitiesService : ReqAndCapabilitiesService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService) { + } + + ngOnInit(): void { + this.initCapabilitiesAndRequirements(); + + this.eventListenerService.registerObserverCallback('CAPABILITIES_UPDATED', () => { + this.loadReqOrCap(); + }); + + this.eventListenerService.registerObserverCallback('REQUIREMENTS_UPDATED', () => { + this.loadReqOrCap(); + }); + } + + + + private initCapabilitiesAndRequirements(): void { + if (!this.workspaceService.metadata.capabilities || !this.workspaceService.metadata.requirements) { + this.loaderService.activate(); + this.topologyTemplateService.getRequirementsAndCapabilitiesWithProperties + (this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId) + .subscribe((response: ComponentGenericResponse) => { + this.workspaceService.metadata.capabilities = response.capabilities; + this.workspaceService.metadata.requirements = response.requirements; + this.initReqOrCap(); + this.loaderService.deactivate(); + }, (error) => { + this.loaderService.deactivate(); + }); + } else { + this.initReqOrCap(); + } + } + + private initReqOrCap() { + this.populateReqOrCap('requirements'); + this.extendRequirementsToRequiremnetsUI(this.requirements); + this.populateReqOrCap('capabilities'); + this.loadReqOrCap(); + } + + private populateReqOrCap(instanceName: string) { + _.forEach(this.workspaceService.metadata[instanceName], (concatArray: any[], name) => { + this[instanceName] = this[instanceName].concat(concatArray); + }); + } + + private updateFilter(event) { + const val = event.target.value.toLowerCase(); + if (this.selectTabName === 'REQUIREMENTS') { + this.instanceRef.instance.requirements = this.requirementsUI.filter((req: Requirement) => { + return !val || this.filterRequirments(req, val); + }); + } else { + this.instanceRef.instance.capabilities = this.capabilities.filter((cap: Capability) => { + return !val || this.filterCapabilities(cap, val); + }); + } + + } + + private selectTab($event) { + this.selectTabName = $event.title.contains('Requirement') ? 'REQUIREMENTS' : 'CATPABILITIES'; + this.loadReqOrCap(); + } + + private async loadReqOrCap() { + if (this.instanceRef) { + this.instanceRef.destroy(); + } + + if (this.selectTabName === 'REQUIREMENTS') { + this.notEmptyTable = this.requirementsUI.length !== 0; + this.instanceRef = this.createDynamicComponentService. + insertComponentDynamically(RequirmentsComponent, {requirements: this.requirementsUI}, this.requirmentsContainer); + // TODO - Keep the initInputs, so it will be called only for the first time - no need to wait to thse API's every time that a user switches tab + await this.reqAndCapabilitiesService.initInputs(this.INPUTS_FOR_REQUIREMENTS); + } else { + this.notEmptyTable = this.capabilities.length !== 0; + this.instanceRef = this.createDynamicComponentService. + insertComponentDynamically(CapabilitiesComponent, {capabilities: this.capabilities}, this.capabilitiesContainer); + // TODO - Keep the initInputs, so it will be called only for the first time - no need to wait to thse API's every time that a user switches tab + await this.reqAndCapabilitiesService.initInputs(this.INPUTS_FOR_CAPABILITIES); + } + } + + private filterCapabilities(capability: Capability, val: string): boolean { + return _.includes([capability.name, capability.description, capability.validSourceTypes.join(), + capability.minOccurrences, capability.maxOccurrences].join('').toLowerCase(), val) || + (capability.type && capability.type.replace('tosca.capabilities.', '').toLowerCase().indexOf(val) !== -1); + } + + private filterRequirments(requirement: Requirement, val: string): boolean { + return _.includes([requirement.name, requirement.minOccurrences, requirement.maxOccurrences].join('').toLowerCase(), val) || + (requirement.capability && requirement.capability.substring('tosca.capabilities.'.length).toLowerCase().indexOf(val) !== -1) || + (requirement.node && requirement.node.substring('tosca.node.'.length).toLowerCase().indexOf(val) !== -1) || + (requirement.relationship && requirement.relationship.substring('tosca.relationship.'.length) + .toLowerCase().indexOf(val) !== -1); + } + + private addCapability() { + let modalConfig = { + size: 'md', + title: 'Add Capability', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Create'), + size: "'x-small'", + callback: () => this.createCapability(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, CapabilitiesEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + } + + private createCapability() { + const capability = this.customModalInstance.innerModalContent.instance.capabilityData; + this.loaderService.activate(); + if (!capability.uniqueId) { + this.topologyTemplateService.createCapability(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, capability).subscribe((result) => { + this.capabilities.unshift(new CapabilityUI(result[0], this.workspaceService.metadata.uniqueId)); + this.loadReqOrCap(); + this.loaderService.deactivate(); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + private addRequiremnet () { + let modalConfig = { + size: 'md', + title: 'Add Requirement', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Create'), + size: "'x-small'", + callback: () => this.createRequirement(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + // requirement: req, + relationshipTypesList: this.reqAndCapabilitiesService.getRelationsShipeTypeList(), + nodeTypesList: this.reqAndCapabilitiesService.getNodeTypesList(), + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + // isReadonly: this.$scope.isViewMode() || !this.$scope.isDesigner(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, RequirementsEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + } + + + private createRequirement() { + const requirement = this.customModalInstance.innerModalContent.instance.requirementData; + this.loaderService.activate(); + if (!requirement.uniqueId) { + this.topologyTemplateService.createRequirement(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, requirement).subscribe(result => { + this.requirementsUI.unshift(new RequirementUI(result[0], this.workspaceService.metadata.uniqueId)); + this.loadReqOrCap(); + this.loaderService.deactivate(); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + private extendRequirementsToRequiremnetsUI(requirements: Requirement[]) { + this.requirements.map((requirement) => { + this.requirementsUI.push(new RequirementUI(requirement, this.workspaceService.metadata.uniqueId)); + }); + } +} @@ -0,0 +1,49 @@ +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; + +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import { ReqAndCapabilitiesComponent } from "./req-and-capabilities.component"; +import { CommonModule } from "@angular/common"; + +import {RequirmentsComponent } from "./requirements/requirments.components"; +import { CapabilitiesComponent } from "./capabilities/capabilities.component"; +import { CapabilitiesPropertiesComponent } from "./capabilities/capabilities-properties/capabilities-properties"; +import {ReqAndCapabilitiesService} from "./req-and-capabilities.service"; +import {RequirementsEditorComponent} from "./requirements/requirementEditor/requirements-editor.component"; +import {CapabilitiesEditorComponent} from "./capabilities/capabilityEditor/capabilities-editor.component"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {ToscaTypesServiceNg2} from "../../../services/tosca-types.service"; + +@NgModule({ + declarations: [ + ReqAndCapabilitiesComponent, + CapabilitiesComponent, + RequirmentsComponent, + CapabilitiesPropertiesComponent, + RequirementsEditorComponent, + CapabilitiesEditorComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + TranslateModule + ], + exports: [ + ReqAndCapabilitiesComponent, + CapabilitiesComponent, + RequirmentsComponent, + CapabilitiesPropertiesComponent + ], + entryComponents: [ + ReqAndCapabilitiesComponent, + CapabilitiesComponent, + RequirmentsComponent, + CapabilitiesPropertiesComponent, + RequirementsEditorComponent, + CapabilitiesEditorComponent + ], + providers: [ ReqAndCapabilitiesService] +}) +export class reqAndCapabilitiesModule { +}
\ No newline at end of file @@ -0,0 +1,80 @@ +import { Injectable } from "@angular/core"; +import { TopologyTemplateService } from "../../../services/component-services/topology-template.service"; +import { Store } from "@ngxs/store"; +import { SdcUiServices } from "onap-ui-angular"; +import { CapabilityTypeModel } from "../../../../models/capability-types"; +import { RelationshipTypeModel } from "../../../../models/relationship-types"; +import { NodeTypeModel } from "../../../../models/node-types"; +import { WorkspaceService } from "../workspace.service"; +import { ToscaTypesServiceNg2 } from "../../../services/tosca-types.service"; + + + +@Injectable() +export class ReqAndCapabilitiesService { + + private capabilityTypesList: CapabilityTypeModel[]; + private relationshipTypesList: RelationshipTypeModel[]; + private nodeTypesList: NodeTypeModel[]; + private capabilitiesListUpdated: boolean = false; + private requirementsListUpdated: boolean = false; + private nodeTypeListUpdated: boolean = false; + + readonly INPUTS_FOR_REQUIREMENTS: string = 'INPUTS_FOR_REQUIREMENTS'; + readonly INPUTS_FOR_CAPABILITIES: string = 'INPUTS_FOR_CAPABILITIES'; + + constructor( + private workspaceService: WorkspaceService, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private store: Store, + private toscaTypesServiceNg2: ToscaTypesServiceNg2){} + + public isViewOnly = (): boolean => { + return this.store.selectSnapshot((state) => state.workspace.isViewOnly); + } + + public isDesigner = (): boolean => { + return this.store.selectSnapshot((state) => state.workspace.isDesigner); + } + + public async initInputs(initInputsFor: string) { + + if (!this.capabilitiesListUpdated){ + // -- COMMON for both -- + this.capabilityTypesList = []; + let capabilityTypesResult = await this.toscaTypesServiceNg2.fetchCapabilityTypes(); + Object.keys(capabilityTypesResult).forEach(key => {this.capabilityTypesList.push(capabilityTypesResult[key])}) + this.capabilitiesListUpdated = true; + } + + if (initInputsFor === 'INPUTS_FOR_REQUIREMENTS') { + if (!this.requirementsListUpdated){ + this.relationshipTypesList = []; + let relationshipTypesResult = await this.toscaTypesServiceNg2.fetchRelationshipTypes(); + Object.keys(relationshipTypesResult).forEach(key => {this.relationshipTypesList.push(relationshipTypesResult[key])}); + this.requirementsListUpdated = true; + } + + if (!this.nodeTypeListUpdated){ + this.nodeTypesList = []; + let nodeTypesResult = await this.toscaTypesServiceNg2.fetchNodeTypes(); + Object.keys(nodeTypesResult).forEach(key => {this.nodeTypesList.push(nodeTypesResult[key])}) + this.nodeTypeListUpdated = true; + } + } + } + + getCapabilityTypesList() { + return this.capabilityTypesList; + } + + getRelationsShipeTypeList() { + return this.relationshipTypesList; + } + + getNodeTypesList() { + return this.nodeTypesList; + } +}
\ No newline at end of file @@ -0,0 +1,91 @@ +<div class="requirement-editor"> + <form class="w-sdc-form"> + <div class="i-sdc-form-content-requirement-content"> + <div class="content-row"> + <div class="i-sdc-form-item"> + <sdc-input + label="{{ 'REQ_NAME' | translate}}" + required="true" + testId="reqName" + [disabled]="isReadonly" + [(value)]="requirementData.name" + (valueChange)="validityChanged()"> + </sdc-input> + </div> + </div> + + <div class="group-with-border"> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'REQ_RELATED_CAPABILITY' | translate }}" + testId="reqRelatedCapability" + required="true" + [disabled]="isReadonly" + [options]="capabilityTypesMappedList" + selectedOption="{{requirementData.capability}}" + [placeHolder] = "requirementData.capability" + (changed)="onCapabilityChanged($event)"> + </sdc-dropdown> + </div> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'REQ_NODE' | translate }}" + testId="reqNode" + [disabled]="isReadonly" + [options]="nodeTypesMappedList" + selectedOption="{{requirementData.node}}" + [placeHolder] = "requirementData.node" + (changed)="onNodeChanged($event)"> + </sdc-dropdown> + </div> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'REQ_RELATIONSHIP' | translate }}" + testId="reqRelationship" + [disabled]="isReadonly" + [options]="relationshipTypesMappedList" + selectedOption="{{requirementData.relationship}}" + [placeHolder] = "requirementData.relationship" + (changed)="onRelationshipChanged($event)"> + </sdc-dropdown> + </div> + </div> + + <label class="i-sdc-form-label occurrences-label"> {{ 'REQ_CAP_OCCURRENCES' | translate}} </label> + <div class="content-row occurrences-section"> + <div class="min-occurrences-value"> + <sdc-input + label="{{ 'REQ_CAP_OCCURRENCES_MIN' | translate}}" + testId="reqOccurrencesMin" + [disabled]="isReadonly" + [(value)]="requirementData.minOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + <div class="sdc-input"> + <label class="sdc-input__label"> {{ 'REQ_CAP_OCCURRENCES_MAX' | translate}} </label> + <div class="max-occurrences-value"> + <sdc-checkbox + class="checkbox-label unbounded-value" + testId="reqOccurrencesMaxUnbounded" + label="{{translatedUnboundTxt.toLowerCase()}}" + (checkedChange)="onUnboundedChanged()" + [checked]="isUnboundedChecked" + [disabled]="isReadonly"> + </sdc-checkbox> + <sdc-input + *ngIf="!isUnboundedChecked" + testId="reqOccurrencesMax" + [disabled]="isReadonly" + [(value)]="requirementData.maxOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + + </div> + </div> + </div> + </form> +</div>
\ No newline at end of file @@ -0,0 +1,35 @@ +@import '../../../../../../../assets/styles/variables.less'; + +.requirement-editor { + .i-sdc-form-content-requirement-content { + padding: 10px 25px; + + .group-with-border { + margin: 25px 0; + padding: 15px 0; + border-top: 1px solid @tlv_color_u; + border-bottom: 1px solid @tlv_color_u; + .content-row:not(:last-of-type) { + padding-bottom: 13px; + } + } + + .occurrences-label { + font-family: @font-opensans-bold; + margin-bottom: 19px; + } + .occurrences-section, /deep/ .max-occurrences-value { + display: flex; + .min-occurrences-value { + padding-right: 30px; + } + .unbounded-value { + padding-top: 7px; + padding-right: 20px; + .sdc-checkbox__label { + text-transform: capitalize; + } + } + } + } +}
\ No newline at end of file @@ -0,0 +1,90 @@ +import {Component} from '@angular/core'; +import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; +import {Requirement, RelationshipTypeModel, NodeTypeModel, CapabilityTypeModel} from 'app/models'; +import {TranslateService} from 'app/ng2/shared/translator/translate.service'; +import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {Subject} from "rxjs"; + +@Component({ + selector: 'requirements-editor', + templateUrl: 'requirements-editor.component.html', + styleUrls: ['requirements-editor.component.less'], + providers: [ServiceServiceNg2, TranslateService] +}) + +export class RequirementsEditorComponent { + + input: { + requirement: Requirement, + relationshipTypesList: Array<RelationshipTypeModel>; + nodeTypesList: Array<NodeTypeModel>; + capabilityTypesList: Array<CapabilityTypeModel>; + isReadonly: boolean; + }; + requirementData: Requirement; + capabilityTypesMappedList: Array<DropdownValue>; + relationshipTypesMappedList: Array<DropdownValue>; + nodeTypesMappedList: Array<DropdownValue>; + isUnboundedChecked: boolean; + isReadonly: boolean; + translatedUnboundTxt: string; + + public onValidationChange: Subject<boolean> = new Subject(); + + constructor(private translateService: TranslateService) { + } + + ngOnInit() { + this.requirementData = new Requirement(this.input.requirement); + this.requirementData.minOccurrences = this.requirementData.minOccurrences || 0; + this.translatedUnboundTxt = ''; + this.capabilityTypesMappedList = _.map(this.input.capabilityTypesList, capType => new DropdownValue(capType.toscaPresentation.type, capType.toscaPresentation.type)); + this.relationshipTypesMappedList = _.map(this.input.relationshipTypesList, rType => new DropdownValue(rType.toscaPresentation.type, rType.toscaPresentation.type)); + this.nodeTypesMappedList = _.map(this.input.nodeTypesList, nodeType => { + return new DropdownValue( + nodeType.componentMetadataDefinition.componentMetadataDataDefinition.toscaResourceName, + nodeType.componentMetadataDefinition.componentMetadataDataDefinition.toscaResourceName) + }); + this.translateService.languageChangedObservable.subscribe(lang => { + this.translatedUnboundTxt = this.translateService.translate('REQ_CAP_OCCURRENCES_UNBOUNDED'); + this.requirementData.maxOccurrences = this.requirementData.maxOccurrences || this.translatedUnboundTxt; + this.isUnboundedChecked = this.requirementData.maxOccurrences === this.translatedUnboundTxt; + }); + this.isReadonly = this.input.isReadonly; + this.validityChanged(); + } + + onUnboundedChanged() { + this.isUnboundedChecked = !this.isUnboundedChecked; + this.requirementData.maxOccurrences = this.isUnboundedChecked ? this.translatedUnboundTxt : null; + this.validityChanged(); + } + + onCapabilityChanged(selectedCapability: DropdownValue) { + this.requirementData.capability = selectedCapability && selectedCapability.value; + this.validityChanged(); + } + + onNodeChanged(selectedNode: DropdownValue) { + this.requirementData.node = selectedNode && selectedNode.value; + } + + onRelationshipChanged(selectedRelationship: DropdownValue) { + this.requirementData.relationship = selectedRelationship && selectedRelationship.value; + } + + checkFormValidForSubmit() { + return this.requirementData.name && this.requirementData.name.length && + this.requirementData.capability && this.requirementData.capability.length && !_.isEqual(this.requirementData.minOccurrences, "") && this.requirementData.minOccurrences >= 0 && + ( + this.isUnboundedChecked || + (this.requirementData.maxOccurrences && (this.requirementData.minOccurrences <= parseInt(this.requirementData.maxOccurrences))) + ); + } + + validityChanged = () => { + let validState = this.checkFormValidForSubmit(); + this.onValidationChange.next(validState); + } + +}
\ No newline at end of file @@ -0,0 +1,28 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {RequirementsEditorComponent} from "./requirements-editor.component"; +import {FormsModule} from "@angular/forms"; +// import {FormElementsModule} from "../../../components/ui/form-components/form-elements.module"; +import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import {SdcUiComponentsModule} from "onap-ui-angular/"; +import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module'; +// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; + +@NgModule({ + declarations: [ + RequirementsEditorComponent + ], + imports: [CommonModule, + FormsModule, + FormElementsModule, + TranslateModule, + SdcUiComponentsModule + ], + exports: [], + entryComponents: [ + RequirementsEditorComponent + ], + providers: [] +}) +export class RequirementsEditorModule { +}
\ No newline at end of file @@ -0,0 +1,4 @@ +/deep/ .importedFromFile { + background-color: #f8f8f8; + color: #959595; + }
\ No newline at end of file @@ -0,0 +1,38 @@ +<div class="requirements-table"> + <ngx-datatable #capabilitiesTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rowClass]="getRowClass" + [rows]="requirements"> + <ngx-datatable-column name="Name" [flexGrow]="1" [resizeable]="false" > + <ng-template ngx-datatable-cell-template let-row="row"> + <span [ngStyle]="{'cursor':row.isCreatedManually ? 'pointer' : 'null' }" data-tests-id="row.name" sdc-tooltip [tooltip-text]="row.name" [tooltip-placement]="3" (click)="editRequirement(row)">{{row.name}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Capability" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.capability" sdc-tooltip [tooltip-text]="row.capability" [tooltip-placement]="3">{{row.capability ? row.capability.substring("tosca.capabilities.".length) : ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Node" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.node" sdc-tooltip [tooltip-text]="row.node" [tooltip-placement]="3">{{row.node ? row.node.substring("tosca.nodes.".length) : ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Relationship" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.relationship" sdc-tooltip [tooltip-text]="row.relationship" [tooltip-placement]="3">{{row.relationship ? row.relationship.substring("tosca.relationships.".length): ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Connected To" [flexGrow]="1" [resizeable]="false"> + </ngx-datatable-column> + <ngx-datatable-column name="Occurrences" [flexGrow]="1" [prop]="'minOccurrences'" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.minOccurrences+','+row.maxOccurrences" sdc-tooltip + [tooltip-text]="row.minOccurrences+','+row.maxOccurrences" [tooltip-placement]="3"> + {{row.minOccurrences}},{{row.maxOccurrences}}</span> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,103 @@ +import {Input, Component, OnInit} from "@angular/core"; +import {Requirement, RequirementUI} from "../../../../../models/requirement"; +import {RequirementsEditorComponent} from "./requirementEditor/requirements-editor.component"; +import {WorkspaceService} from "../../workspace.service"; +import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service"; +import {ReqAndCapabilitiesService} from "../req-and-capabilities.service"; +import {EventListenerService} from "../../../../../services/event-listener-service"; +import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component"; +import {SdcUiServices} from "onap-ui-angular"; +import sortedIndexBy = require("lodash/sortedIndexBy"); + +@Component({ + selector: 'requirments', + templateUrl: './requirments.components.html', + styleUrls: ['../../../../../../assets/styles/table-style.less', './requirements.component.less'] +}) + + + +export class RequirmentsComponent implements OnInit { + @Input() public requirements: Array<RequirementUI>; + private customModalInstance: ModalComponent; + + constructor( + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private reqAndCapabilitiesService : ReqAndCapabilitiesService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService) { + } + + + ngOnInit(): void { + let isCreatedManually: RequirementUI[] = []; + let isImportedFromFile: RequirementUI[] = []; + + isCreatedManually = this.requirements.filter((requirement) => requirement.isCreatedManually); + isImportedFromFile = this.requirements.filter((requirement) => !requirement.isCreatedManually); + + this.requirements = []; + + isCreatedManually.map((requirement) => this.requirements.push(requirement)); + isImportedFromFile.map((requirement) => this.requirements.push(requirement)); + + } + + + + editRequirement(req) { + + let modalConfig = { + size: 'md', + title: 'Update Requirement', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Update'), + size: "'x-small'", + callback: () => this.updateRequirement(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + requirement: req, + relationshipTypesList: this.reqAndCapabilitiesService.getRelationsShipeTypeList(), + nodeTypesList: this.reqAndCapabilitiesService.getNodeTypesList(), + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + // isReadonly: this.$scope.isViewMode() || !this.$scope.isDesigner(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, RequirementsEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + + } + + private updateRequirement() { + const requirement = this.customModalInstance.innerModalContent.instance.requirementData; + this.loaderService.activate(); + if (requirement.uniqueId) { + this.topologyTemplateService.updateRequirement(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, requirement).subscribe(result => { + let index = this.requirements.findIndex(req => result[0].uniqueId === req.uniqueId); + this.requirements[index] = new RequirementUI(result[0], this.workspaceService.metadata.uniqueId); + this.eventListenerService.notifyObservers('REQUIREMENTS_UPDATED'); + this.loaderService.deactivate(); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + getRowClass(row) { + if (!row.isCreatedManually) { + return { + 'importedFromFile': true + }; + } + } + +}
\ No newline at end of file @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tosca artifacts page should match current snapshot of tosca artifact pages component 1`] = ` +<tosca-artifact-page + serviceLoader={[Function Object]} + store={[Function Store]} + table={[Function DatatableComponent]} + workspaceService={[Function Object]} +> + <div + class="tosca-artifact-page" + > + <ngx-datatable + class="ngx-datatable" + columnmode="flex" + > + <div + visibilityobserver="" + > + + <datatable-body + class="datatable-body" + > + <datatable-selection> + + + + </datatable-selection> + </datatable-body> + + </div> + </ngx-datatable> + </div> +</tosca-artifact-page> +`; @@ -0,0 +1,50 @@ +<div class="tosca-artifact-page"> + <ngx-datatable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [reorderable]="false" + [swapColumns]="false" + [rows]="toscaArtifacts$ | async" + [sorts]="[{prop: 'artifactDisplayName', dir: 'desc'}]" + #toscaArtifactsTable + (activate)="onActivate($event)"> + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div>Label: {{row.artifactLabel}}</div> + <div>UUID: {{row.artifactUUID}}</div> + <div>Description: {{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="3" + [prop]="'artifactDisplayName'"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span>{{row.artifactDisplayName }}</span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false"name="Type" [flexGrow]="3"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.artifactType}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.artifactVersion }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false"[flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class="download-artifact-button"> + <download-artifact [artifact]="row" [componentId]="componentId" + [componentType]="componentType" + testId="download_{{row.artifactDisplayName}}"></download-artifact> + </div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file @@ -0,0 +1,7 @@ +.tosca-artifact-page { + .download-artifact-button { + text-align: center; + padding-top: 4px; + + } +}
\ No newline at end of file @@ -0,0 +1,46 @@ +import {Component, OnInit, ViewChild} from "@angular/core"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {ArtifactModel} from "../../../../models"; +import {Select, Store} from "@ngxs/store"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import * as _ from "lodash"; +import {ArtifactGroupType, COMPONENT_FIELDS} from "../../../../utils"; +import {GetArtifactsByTypeAction} from "../../../store/actions/artifacts.action"; +import {Observable} from "rxjs/index"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {ArtifactType} from "../../../../utils/constants"; +import {map} from "rxjs/operators"; + +@Component({ + selector: 'tosca-artifact-page', + + templateUrl: './tosca-artifact-page.component.html', + styleUrls: ['./tosca-artifact-page.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class ToscaArtifactPageComponent implements OnInit { + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + @ViewChild('toscaArtifactsTable') table: any; + public toscaArtifacts$: Observable<ArtifactModel[]>; + public componentId: string; + public componentType:string; + + constructor(private serviceLoader: SdcUiServices.LoaderService, private workspaceService: WorkspaceService, private store: Store) { + } + + + ngOnInit(): void { + this.componentId = this.workspaceService.metadata.uniqueId; + this.componentType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new GetArtifactsByTypeAction({componentType:this.componentType, componentId:this.componentId, artifactType:ArtifactGroupType.TOSCA})); + this.toscaArtifacts$ = this.store.select(ArtifactsState.getArtifactsByType).pipe(map(filterFn => filterFn(ArtifactGroupType.TOSCA))); + } + + onActivate(event) { + if(event.type === 'click'){ + this.table.rowDetail.toggleExpandRow(event.row); + } + } +}
\ No newline at end of file @@ -0,0 +1,28 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {ToscaArtifactPageComponent} from "./tosca-artifact-page.component"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; + +@NgModule({ + declarations: [ + ToscaArtifactPageComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + UiElementsModule + ], + exports: [ + ToscaArtifactPageComponent + ], + entryComponents: [ + ToscaArtifactPageComponent + ], + +}) +export class ToscaArtifactPageModule { +} @@ -0,0 +1,71 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ToscaArtifactPageComponent} from "./tosca-artifact-page.component"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {Observable} from "rxjs/Observable"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import 'rxjs/add/observable/of'; +import {NgxsModule, Store} from "@ngxs/store"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {toscaArtifactMock} from "../../../../../jest/mocks/artifacts-mock"; + +describe('tosca artifacts page', () => { + + let fixture: ComponentFixture<ToscaArtifactPageComponent>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let store: Store; + + + beforeEach( + async(() => { + + topologyTemplateServiceMock = { + getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(toscaArtifactMock)) + }; + workspaceServiceMock = {metadata: <ComponentMetadata>{uniqueId: 'service_unique_id', componentType: 'SERVICE'}} + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ToscaArtifactPageComponent], + imports: [NgxDatatableModule, NgxsModule.forRoot([ArtifactsState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ToscaArtifactPageComponent); + store = testBed.get(Store); + }); + }) + ); + + it('should match current snapshot of tosca artifact pages component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should see exactly 2 tosca artifacts', () => { + fixture.componentInstance.ngOnInit(); + fixture.componentInstance.toscaArtifacts$.subscribe((artifacts)=> { + expect(artifacts.length).toEqual(2); + }) + store.selectOnce(state => state.artifacts.toscaArtifacts).subscribe(artifacts => { + expect(artifacts.length).toEqual(9); + }); + }) + +});
\ No newline at end of file @@ -1,3 +1,6 @@ +/** + * Created by ob0695 on 6/24/2018. + */ /*- * ============LICENSE_START======================================================= * SDC @@ -17,23 +20,18 @@ * limitations under the License. * ============LICENSE_END========================================================= */ +import {Store} from "@ngxs/store"; +import {Injectable} from "@angular/core"; +import {UpdateIsViewOnly} from "../../store/actions/workspace.action"; + +@Injectable() +export class WorkspaceNg1BridgeService { + + constructor(private store: Store) { + }; -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServicePathComponent} from "./service-path.component"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; + public updateIsViewOnly = (isViewOnly: boolean):void => { + this.store.dispatch(new UpdateIsViewOnly(isViewOnly)); + } -@NgModule({ - declarations: [ - ServicePathComponent - ], - imports: [CommonModule, - UiElementsModule], - exports: [], - entryComponents: [ - ServicePathComponent - ], - providers: [] -}) -export class ServicePathModule { -}
\ No newline at end of file +} @@ -0,0 +1,3 @@ +/** + * Created by ob0695 on 6/11/2018. + */ @@ -0,0 +1,50 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {CompositionPageModule} from "../composition/composition-page.module"; + +import {NgxsModule} from "@ngxs/store"; +import {TopologyTemplateService} from "../../services/component-services/topology-template.service"; +import {WorkspaceState} from "../../store/states/workspace.state"; +import {WorkspaceService} from "./workspace.service"; +import {DeploymentPageModule} from "./deployment/deployment-page.module"; +import {ToscaArtifactPageModule} from "./tosca-artifacts/tosca-artifact-page.module"; +import {InformationArtifactPageModule} from "./information-artifact/information-artifact-page.module"; +import { reqAndCapabilitiesModule } from "./req-and-capabilities/req-and-capabilities.module"; +import {AttributesModule} from "./attributes/attributes.module"; +import {ArtifactsState} from "../../store/states/artifacts.state"; +import {InstanceArtifactsState} from "../../store/states/instance-artifacts.state"; +import {DeploymentArtifactsPageModule} from "./deployment-artifacts/deployment-artifacts-page.module"; +import { DistributionModule } from './disribution/distribution.module'; +import { ActivityLogModule } from './activity-log/activity-log.module'; + +@NgModule({ + declarations: [], + imports: [ + DeploymentPageModule, + CompositionPageModule, + AttributesModule, + reqAndCapabilitiesModule, + ToscaArtifactPageModule, + DeploymentArtifactsPageModule, + InformationArtifactPageModule, + DistributionModule, + ActivityLogModule, + NgxsModule.forFeature([WorkspaceState, ArtifactsState, InstanceArtifactsState]) + ], + + exports: [], + entryComponents: [], + providers: [TopologyTemplateService, WorkspaceService] +}) + +export class WorkspaceModule { + + constructor() { + + } +} @@ -0,0 +1,70 @@ +/** + * Created by ob0695 on 6/5/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +/** + * Created by rc2122 on 5/23/2017. + */ +import { Injectable } from '@angular/core'; +import {WorkspaceMode, ComponentState, Role} from "../../../utils/constants"; +import {Component as TopologyTemplate, ComponentMetadata} from "app/models"; +import {CacheService} from "../../services/cache.service"; +import {IComponentMetadata} from "../../../models/component-metadata"; +import {ComponentType} from "../../../utils"; + +@Injectable() +export class WorkspaceService { + + public metadata:ComponentMetadata; + + constructor(private cacheService:CacheService) { + + } + + public setComponentMetadata = (metadata: ComponentMetadata) => { + this.metadata = metadata; + } + + public getMetadataType(): string { + switch (this.metadata.componentType) { + case ComponentType.SERVICE: + return ComponentType.SERVICE; + default: + return this.metadata.resourceType; + } + } + + public getComponentMode = (component:TopologyTemplate):WorkspaceMode => {//return if is edit or view for resource or service + let mode = WorkspaceMode.VIEW; + + let user = this.cacheService.get("user"); + if (component.lifecycleState === ComponentState.NOT_CERTIFIED_CHECKOUT && + component.lastUpdaterUserId === user.userId) { + if ((component.isService() || component.isResource()) && user.role == Role.DESIGNER) { + mode = WorkspaceMode.EDIT; + } + } + return mode; + } +} + +
\ No newline at end of file @@ -0,0 +1,151 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import {Pipe, PipeTransform} from "@angular/core"; +import {Component, Resource} from "app/models"; +import {ComponentType} from "app/utils/constants"; + +export interface ISearchFilter { + [key:string]: string; +} + +export interface IEntityFilterObject { + // Types + selectedComponentTypes?:Array<string>; + selectedResourceSubTypes?:Array<string>; + // Categories + selectedCategoriesModel?:Array<string>; + // Statuses + selectedStatuses?:Array<string>; + // distributed + distributed?:Array<string>; + // search + search?:ISearchFilter; + +} + +@Pipe({name: 'entity-filter'}) +export class EntityFilterPipe implements PipeTransform{ + constructor() { + } + + public static transform(components:Array<Component>, filter:IEntityFilterObject) { + let filteredComponents:Array<Component> = components; + + // filter by type + // -------------------------------------------------------------------------- + if ((filter.selectedComponentTypes && filter.selectedComponentTypes.length > 0) || (filter.selectedResourceSubTypes && filter.selectedResourceSubTypes.length > 0)) { + let filteredTypes = []; + angular.forEach(components, (component:Component):void => { + // Filter by component type + let typeLower:string = component.componentType.toLowerCase(); + let typeFirstCapital:string = typeLower.charAt(0).toUpperCase() + typeLower.slice(1); + if (filter.selectedComponentTypes.indexOf(typeFirstCapital) !== -1) { + filteredTypes.push(component); + } + + // Filter by resource sub type, only in case the resource checkbox was not selected (because in this case we already added all the components in above section). + if (component.isResource() && filter.selectedComponentTypes.indexOf("Resource") === -1 && filter.selectedResourceSubTypes.length > 0) { + //filteredComponents.pop(); // Remove the last inserted component. + let resource:Resource = <Resource>component; + if (filter.selectedResourceSubTypes.indexOf(resource.getComponentSubType()) !== -1) { + filteredTypes.push(component); + } + } + }); + filteredComponents = filteredTypes; + } + + // filter by categories & subcategories & groupings + // -------------------------------------------------------------------------- + if (filter.selectedCategoriesModel && filter.selectedCategoriesModel.length > 0) { + let filteredCategories = []; + angular.forEach(filteredComponents, (component:Component):void => { + let componentCategory = component.categoryNormalizedName + + ((component.subCategoryNormalizedName) ? '.' + component.subCategoryNormalizedName : ''); + if (component.componentType === ComponentType.RESOURCE) { + componentCategory = 'resourceNewCategory.' + componentCategory; + } else if (component.componentType === ComponentType.SERVICE) { + componentCategory = 'serviceNewCategory.' + componentCategory; + } + if (filter.selectedCategoriesModel.indexOf(componentCategory) !== -1) { + filteredCategories.push(component); + } + }); + filteredComponents = filteredCategories; + } + + // filter by statuses + // -------------------------------------------------------------------------- + if (filter.selectedStatuses && filter.selectedStatuses.length > 0) { + + let filteredStatuses = []; + angular.forEach(filteredComponents, (component:Component):void => { + if (filter.selectedStatuses.indexOf(component.lifecycleState) > -1) { + filteredStatuses.push(component); + } + //if status DISTRIBUTED && CERTIFIED are selected the component will added in CERTIFIED status , not need to add twice + if (filter.selectedStatuses.indexOf('DISTRIBUTED') > -1 && !(filter.selectedStatuses.indexOf('CERTIFIED') > -1)) { + if (component.distributionStatus && component.distributionStatus.indexOf('DISTRIBUTED') > -1 && component.lifecycleState.indexOf('CERTIFIED') > -1) { + filteredStatuses.push(component); + } + } + }); + filteredComponents = filteredStatuses; + } + + // filter by statuses and distributed + // -------------------------------------------------------------------------- + if (filter.distributed != undefined && filter.distributed.length > 0) { + let filterDistributed:Array<any> = filter.distributed; + let filteredDistributed = []; + angular.forEach(filteredComponents, (entity) => { + filterDistributed.forEach((distribute) => { + let distributeItem = distribute.split(','); + distributeItem.forEach((item) => { + if (item !== undefined && entity.distributionStatus === item) { + filteredDistributed.push(entity); + } + }) + }); + }); + filteredComponents = filteredDistributed; + } + + // filter by search + // -------------------------------------------------------------------------- + if (filter.search != undefined) { + Object.keys(filter.search).forEach((searchKey) => { + let searchVal = filter.search[searchKey]; + if (searchVal) { + searchVal = searchVal.toLowerCase(); + filteredComponents = filteredComponents.filter((component:Component) => + component[searchKey].toLowerCase().indexOf(searchVal) !== -1); + } + }); + } + + return filteredComponents; + } + + public transform(components:Array<Component>, filter:IEntityFilterObject) { + return EntityFilterPipe.transform(components, filter); + } +} @@ -26,7 +26,10 @@ import {GroupByPipe} from "./groupBy.pipe"; import {ResourceNamePipe} from "./resource-name.pipe"; import {NgModule} from "@angular/core"; import {SafeUrlSanitizerPipe} from "./safeUrlSanitizer.pipe"; -import {OrderByPipe} from "./orderBy.pipe"; +import {EntityFilterPipe} from "./entity-filter.pipe"; +import {KeyValuePipe} from "./key-value.pipe"; +import {PropertiesOrderByPipe} from "./properties-order-by.pipe"; +import {OrderByPipe} from "./order-by.pipe"; @NgModule({ declarations: [ @@ -36,6 +39,9 @@ import {OrderByPipe} from "./orderBy.pipe"; SafeUrlSanitizerPipe, SearchFilterPipe, ResourceNamePipe, + EntityFilterPipe, + KeyValuePipe, + PropertiesOrderByPipe, OrderByPipe ], exports: [ @@ -45,7 +51,20 @@ import {OrderByPipe} from "./orderBy.pipe"; SafeUrlSanitizerPipe, SearchFilterPipe, ResourceNamePipe, - OrderByPipe + EntityFilterPipe, + PropertiesOrderByPipe, + OrderByPipe, + KeyValuePipe + ], + providers: [ + ContentAfterLastDotPipe, + GroupByPipe, + KeysPipe, + SafeUrlSanitizerPipe, + SearchFilterPipe, + ResourceNamePipe, + EntityFilterPipe, + KeyValuePipe ] }) @@ -26,6 +26,9 @@ import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'groupBy'}) export class GroupByPipe implements PipeTransform { transform(value: Array<any>, field: string): Array<any> { + if(!value) { + return null; + } const groupedObj = value.reduce((prev, cur)=> { if(!prev[cur[field]]) { prev[cur[field]] = [cur]; @@ -0,0 +1,41 @@ +/** + * Created by ob0695 on 7/3/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +/** + * Created by rc2122 on 5/17/2017. + */ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({name: 'keyValue'}) +export class KeyValuePipe implements PipeTransform { + transform(value, field: string): Array<any> { + if(!value) { + return null; + } + let keyValueObject = []; + for (let key in value) { + keyValueObject.push({key:key, value: value[key]}); + } + return keyValueObject; + } +} @@ -23,6 +23,9 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'keys'}) export class KeysPipe implements PipeTransform { transform(value, args:string[]) : any { + if(!value) { + return null; + } let keys = []; for (let key in value) { keys.push(key); @@ -20,10 +20,10 @@ import { Pipe, PipeTransform } from '@angular/core'; -@Pipe({ name: 'orderBy' }) +@Pipe({name: 'orderBy'}) export class OrderByPipe implements PipeTransform { transform(records: Array<any>, args?: any): any { - if (!records || !args.path) return records; + if (!records || !args.path) { return records; } let len = args.path.length; return records.sort((itemIdx1, itemIdx2) => { var i = 0; @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 Huawei Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'propertiesOrderBy' }) +export class PropertiesOrderByPipe implements PipeTransform { + transform(records: Array<any>, args?: any): any { + if (!records || !args.path) return records; + let len = args.path.length; + return records.sort((itemIdx1, itemIdx2) => { + var i = 0; + while (i < len) { itemIdx1 = itemIdx1[args.path[i]]; itemIdx2 = itemIdx2[args.path[i]]; i++; } + // Order * (-1): We change our order + return itemIdx1 + "" > itemIdx2 + "" ? args.direction : args.direction * (- 1); + }) + }; +} @@ -20,15 +20,20 @@ import { Pipe, PipeTransform } from '@angular/core'; +import * as _ from 'lodash'; @Pipe({name: 'resourceName'}) export class ResourceNamePipe implements PipeTransform { + + public static getDisplayName (value:string): string { + const newName:string = + _.last(value.split(/tosca\.nodes\..*network\..*relationships\..*org\.openecomp\..*resource\.nfv\..*nodes\.module\..*cp\..*vl\./)); + return (newName) ? newName : value; + } + transform(value) : any { if (value) { - //newName = _.last(newName.split('.')); - const newName:string = - _.last(value.split(/tosca\.nodes\..*network\..*relationships\..*org\.openecomp\..*resource\.nfv\..*nodes\.module\..*cp\..*vl\./)); - return (newName) ? newName : value; + return ResourceNamePipe.getDisplayName(value); } } } @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable} from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Activity } from '../../models/activity'; +import { ServerTypeUrl } from '../../utils/constants'; +import { ISdcConfig, SdcConfigToken } from '../config/sdc-config.config'; +import { HttpHelperService } from './http-hepler.service'; + +@Injectable() +export class ActivityLogService { + url: string; + + constructor(private httpClient: HttpClient, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { + this.url = this.sdcConfig.api.root + this.sdcConfig.api.GET_activity_log; + } + + public getActivityLog(componentType: string, uid: string): Observable<Activity[]> { + + // Compose URL: audit-records/services_or_resources/uid + const url = HttpHelperService.replaceUrlParams(this.url, { + type: ServerTypeUrl.toServerTypeUrl(componentType), + id: uid + }); + + return this.httpClient.get<Activity[]>(url); + } + +} @@ -1,67 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import { Injectable, Inject } from "@angular/core"; -import { Observable } from "rxjs/Observable"; -import { HttpService } from "./http.service"; -import { SdcConfigToken, ISdcConfig } from "../config/sdc-config.config"; -import { Component } from "../../models"; -import { ComponentFactory } from 'app/utils/component-factory'; - - -@Injectable() -export class ArchiveService { - protected baseUrl; - - constructor(private http: HttpService, @Inject(SdcConfigToken) sdcConfig:ISdcConfig, private componentFactory:ComponentFactory/*, @Inject(ComponentFactory) componentFactory */) { - this.baseUrl = sdcConfig.api.root ; - } - - public getArchiveCatalog() { - let archiveCatalogItems:Component[] = []; - let archiveCatalogResourceItems:Component[] = []; - let archiveCatalogServiceItems:Component[] = []; - - return this.http.get(this.baseUrl + '/v1/catalog/archive/', {}).map(res => { - let archiveCatalogObject = res.json(); - if (archiveCatalogObject.resources) archiveCatalogResourceItems = this.getResourceItems(archiveCatalogObject.resources); - if (archiveCatalogObject.services) archiveCatalogServiceItems = this.getServiceItems(archiveCatalogObject.services); - archiveCatalogItems = [].concat(archiveCatalogResourceItems, archiveCatalogServiceItems); - - return archiveCatalogItems; - }); - } - - - private getResourceItems(resources){ - let resourceItems = resources.map((resource)=>{ - return this.componentFactory.createResource(resource) - }) - return resourceItems; - } - - private getServiceItems(services){ - let serviceItems = services.map((service)=>{ - return this.componentFactory.createService(service) - }) - return serviceItems; - } - -}
\ No newline at end of file @@ -20,38 +20,44 @@ import {Injectable, Inject} from '@angular/core'; import {IAppConfigurtaion, ICookie} from "../../models/app-config"; -import {Response, Headers, RequestOptions, Http} from '@angular/http'; import {Cookie2Service} from "./cookie.service"; import { Observable } from 'rxjs/Observable'; import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; +import { IUserProperties } from "app/models"; +import { CacheService } from "app/ng2/services/cache.service"; +import { HttpClient, HttpHeaders } from '@angular/common/http'; @Injectable() export class AuthenticationService { - constructor(private cookieService:Cookie2Service, private http: Http, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { + private _loggedinUser:IUserProperties; + + constructor(private cookieService:Cookie2Service, private http: HttpClient, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig, private cacheService: CacheService) { this.cookieService = cookieService; - this.http = http; } - private getAuthHeaders():any { + private getAuthHeaders():HttpHeaders { let cookie:ICookie = this.sdcConfig.cookie; - let authHeaders:any = {}; - authHeaders[cookie.userFirstName] = this.cookieService.getFirstName(); - authHeaders[cookie.userLastName] = this.cookieService.getLastName(); - authHeaders[cookie.userEmail] = this.cookieService.getEmail(); - authHeaders[cookie.userIdSuffix] = this.cookieService.getUserId(); + let authHeaders: HttpHeaders = new HttpHeaders(); + authHeaders = authHeaders.set(cookie.userFirstName, this.cookieService.getFirstName()) + .set(cookie.userLastName, this.cookieService.getLastName()) + .set(cookie.userEmail, this.cookieService.getEmail()) + .set(cookie.userIdSuffix, this.cookieService.getUserId()) return authHeaders; } - public authenticate(): Observable<JSON> { - let options = new RequestOptions({ - headers: new Headers(this.getAuthHeaders()) - }); - + public authenticate(): Observable<IUserProperties> { let authUrl = this.sdcConfig.api.root + this.sdcConfig.api.GET_user_authorize; - return this.http - .get(authUrl, options) - .map((res: Response) => res.json()); + return this.http.get<IUserProperties>(authUrl, {headers: this.getAuthHeaders()}); + } + + public getLoggedinUser():IUserProperties { + return this._loggedinUser; } + public setLoggedinUser(loggedinUser:IUserProperties) { + this._loggedinUser = loggedinUser; + this.cacheService.set('user', loggedinUser); + }; + } @@ -0,0 +1,49 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import {Injectable} from "@angular/core"; +import {Dictionary} from "app/utils"; + +@Injectable() +export class CacheService { + private storage:Dictionary<string, any>; + + constructor() { + this.storage = new Dictionary<string, any>(); + }; + + public get(key:string): any { + return this.storage.getValue(key); + } + + public set(key:string, value:any): void { + this.storage.setValue(key, value); + } + + public remove(key:string): void { + if (this.storage.containsKey(key)) { + this.storage.remove(key); + } + } + + public contains(key:string): boolean { + return this.storage.containsKey(key); + } +} @@ -0,0 +1,85 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import { Injectable, Inject } from "@angular/core"; +import { Observable } from "rxjs/Observable"; +import { SdcConfigToken, ISdcConfig } from "../config/sdc-config.config"; +import { Component, IApi, IComponentsArray } from "app/models"; +import { ComponentFactory } from 'app/utils/component-factory'; +import {ResourceType} from "../../utils/constants"; +import {SharingService} from "./sharing.service"; +import { HttpClient, HttpParams } from "@angular/common/http"; + +@Injectable() +export class CatalogService { + protected api:IApi; + protected baseUrl:string; + protected baseMicroServiceUrl:string; + + constructor(private http: HttpClient, + @Inject(SdcConfigToken) sdcConfig:ISdcConfig, + private componentFactory:ComponentFactory, + private sharingService:SharingService) { + this.api = sdcConfig.api; + this.baseUrl = sdcConfig.api.root ; + this.baseMicroServiceUrl = sdcConfig.api.uicache_root; + } + + public getCatalog(): Observable<Array<Component>> { + let searchParams = new HttpParams(); + searchParams = searchParams.append('excludeTypes', ResourceType.VFCMT).append('excludeTypes', ResourceType.CONFIGURATION); + return this.http.get<IComponentsArray>(this.baseMicroServiceUrl + this.api.GET_uicache_catalog, {params: searchParams}) + .map(res => this.processComponentsResponse(res, true)); + } + + public getArchiveCatalog() { + return this.http.get<IComponentsArray>(this.baseUrl + '/v1/catalog/archive/', {}) + .map(res => this.processComponentsResponse(res)); + } + + private processComponentsResponse(componentsArr: IComponentsArray, addSharing:boolean = false) { + const componentsList: Component[] = []; + if (componentsArr.resources) { + componentsList.push(...this.getResourceItems(componentsArr.resources)); + } + if (componentsArr.services) { + componentsList.push(...this.getServiceItems(componentsArr.services)); + } + if (addSharing) { + componentsList.forEach((item) => this.sharingService.addUuidValue(item.uniqueId, item.uuid)); + } + return componentsList; + } + + private getResourceItems(resources){ + let resourceItems = resources.map((resource)=>{ + return this.componentFactory.createResource(resource) + }) + return resourceItems; + } + + private getServiceItems(services){ + let serviceItems = services.map((service)=>{ + return this.componentFactory.createService(service) + }) + return serviceItems; + } + +}
\ No newline at end of file @@ -19,62 +19,102 @@ */ import {Injectable, Inject} from '@angular/core'; -import {Response, RequestOptions, Headers} from '@angular/http'; import { Observable } from 'rxjs/Observable'; import {PropertyFEModel, PropertyBEModel} from "app/models"; -import {CommonUtils} from "app/utils"; -import {Component, ComponentInstance, Capability, PropertyModel} from "app/models"; -import { HttpService } from '../http.service'; +import {CommonUtils, ComponentType, ServerTypeUrl, ComponentInstanceFactory} from "app/utils"; +import {Component, ComponentInstance, Capability, PropertyModel, ArtifactGroupModel, ArtifactModel, AttributeModel, IFileDownload} from "app/models"; import {SdcConfigToken, ISdcConfig} from "../../config/sdc-config.config"; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { InputBEModel } from '../../../models/properties-inputs/input-be-model'; +import { HttpHelperService } from '../http-hepler.service'; @Injectable() export class ComponentInstanceServiceNg2 { protected baseUrl; - constructor(private http: HttpService, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { + constructor(private http: HttpClient, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; } + private getServerTypeUrl = (componentType:string):string => { + switch (componentType) { + case ComponentType.SERVICE: + return ServerTypeUrl.SERVICES; + default: + return ServerTypeUrl.RESOURCES; + } + } getComponentInstanceProperties(component: Component, componentInstanceId: string): Observable<Array<PropertyBEModel>> { - - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstanceId + '/properties') - .map((res: Response) => { - return CommonUtils.initBeProperties(res.json()); + return this.http.get<Array<PropertyBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstanceId + '/properties') + .map(res => { + return CommonUtils.initBeProperties(res); }) } getComponentInstanceInputs(component: Component, componentInstance: ComponentInstance): Observable<Array<PropertyBEModel>> { - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstance.uniqueId + '/' + componentInstance.componentUid + '/inputs') - .map((res: Response) => { - return CommonUtils.initInputs(res.json()); + return this.http.get<Array<InputBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstance.uniqueId + '/' + componentInstance.componentUid + '/inputs') + .map(res => { + return CommonUtils.initInputs(res); }) } - updateInstanceProperties(component: Component, componentInstanceId: string, properties: PropertyBEModel[]) { + getComponentInstanceArtifactsByGroupType = (componentType:string, componentId:string, componentInstanceId:string, artifactGroupType:string):Observable<ArtifactGroupModel> => { + + return this.http.get<ArtifactGroupModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + "/resourceInstances/" + componentInstanceId + "/artifactsByType/" + artifactGroupType) + .map(res => { + return new ArtifactGroupModel(res); + }); + }; + + getArtifactByGroupType = (componentType:string, componentId:string, artifactGroupType:string):Observable<ArtifactGroupModel> => { + + return this.http.get<ArtifactGroupModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + "/artifactsByType/" + artifactGroupType) + .map(response => new ArtifactGroupModel(response)); + }; + + changeResourceInstanceVersion = (componentType:string, componentId:string, componentInstanceId:string, componentUid:string):Observable<ComponentInstance> => { + return this.http.post<ComponentInstance>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + componentInstanceId + '/changeVersion', {'componentUid': componentUid}) + .map((res) => { + return ComponentInstanceFactory.createComponentInstance(res); + }) + }; - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstance/' + componentInstanceId + '/properties', properties) - .map((res: Response) => { - return res.json().map((resProperty) => new PropertyBEModel(resProperty)); + updateComponentInstance = (componentType:string, componentId:string, componentInstance:ComponentInstance):Observable<ComponentInstance> => { + return this.http.post<ComponentInstance>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + componentInstance.uniqueId, componentInstance.toJSON()) + .map((response) => { + return ComponentInstanceFactory.createComponentInstance(response); + }); + }; + + updateInstanceProperties(componentType:string, componentId:string, componentInstanceId: string, properties: PropertyBEModel[]) { + + return this.http.post<Array<PropertyModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + componentInstanceId + '/properties', properties) + .map((res) => { + return res.map((resProperty) => { + let newProp = new PropertyModel(resProperty); + newProp.resourceInstanceUniqueId = componentInstanceId + return newProp; + }); }); } - getInstanceCapabilityProperties(component: Component, componentInstanceId: string, capability: Capability): Observable<Array<PropertyModel>> { + getInstanceCapabilityProperties(componentType: string, componentId: string, componentInstanceId: string, capability: Capability): Observable<Array<PropertyModel>> { - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstanceId + '/capability/' + capability.type + + return this.http.get<Array<PropertyModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/componentInstances/' + componentInstanceId + '/capability/' + capability.type + '/capabilityName/' + capability.name + '/ownerId/' + capability.ownerId + '/properties') - .map((res: Response) => { - capability.properties = res.json().map((capProp) => new PropertyModel(capProp)); // update capability properties + .map((res) => { + capability.properties = res.map((capProp) => new PropertyModel(capProp)); // update capability properties return capability.properties; }) } updateInstanceCapabilityProperties(component: Component, componentInstanceId: string, capability: Capability, properties: PropertyBEModel[]): Observable<Array<PropertyModel>> { - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstanceId + '/capability/' + capability.type + + return this.http.put<Array<PropertyModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstanceId + '/capability/' + capability.type + '/capabilityName/' + capability.name + '/ownerId/' + capability.ownerId + '/properties', properties) - .map((res: Response) => { - const savedProperties: PropertyModel[] = res.json().map((resProperty) => new PropertyModel(resProperty)); + .map((res) => { + const savedProperties: PropertyModel[] = res.map((resProperty) => new PropertyModel(resProperty)); savedProperties.forEach((savedProperty) => { const propIdx = capability.properties.findIndex((p) => p.uniqueId === savedProperty.uniqueId); if (propIdx !== -1) { @@ -87,37 +127,87 @@ export class ComponentInstanceServiceNg2 { updateInstanceInputs(component: Component, componentInstanceId: string, inputs: PropertyBEModel[]): Observable<PropertyBEModel[]> { - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstance/' + componentInstanceId + '/inputs', inputs) - .map((res: Response) => { - return res.json().map((resInput) => new PropertyBEModel(resInput)); + return this.http.post<Array<PropertyModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstance/' + componentInstanceId + '/inputs', inputs) + .map((res) => { + return res.map((resInput) => new PropertyBEModel(resInput)); }); } getComponentGroupInstanceProperties(component: Component, groupInstanceId: string): Observable<Array<PropertyBEModel>> { - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/groups/' + groupInstanceId + '/properties') - .map((res: Response) => { - return CommonUtils.initBeProperties(res.json()); + return this.http.get<Array<PropertyBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/groups/' + groupInstanceId + '/properties') + .map((res) => { + return CommonUtils.initBeProperties(res); + }); + } + + updateComponentGroupInstanceProperties(componentType:string, componentId:string, groupInstanceId: string, properties: PropertyBEModel[]): Observable<Array<PropertyBEModel>> { + return this.http.put<Array<PropertyBEModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/groups/' + groupInstanceId + '/properties', properties) + .map((res) => { + return res.map((resProperty) => new PropertyBEModel(resProperty)); }); } - updateComponentGroupInstanceProperties(component: Component, groupInstanceId: string, properties: PropertyBEModel[]): Observable<Array<PropertyBEModel>> { - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/groups/' + groupInstanceId + '/properties', properties) - .map((res: Response) => { - return res.json().map((resProperty) => new PropertyBEModel(resProperty)); + getComponentPolicyInstanceProperties(componentType:string, componentId:string, policyInstanceId: string): Observable<Array<PropertyBEModel>> { + return this.http.get<Array<PropertyBEModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/policies/' + policyInstanceId + '/properties') + .map((res) => { + return CommonUtils.initBeProperties(res); }); } - getComponentPolicyInstanceProperties(component: Component, policyInstanceId: string): Observable<Array<PropertyBEModel>> { - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/policies/' + policyInstanceId + '/properties') - .map((res: Response) => { - return CommonUtils.initBeProperties(res.json()); + updateComponentPolicyInstanceProperties(componentType:string, componentId:string, policyInstanceId: string, properties: PropertyBEModel[]): Observable<Array<PropertyBEModel>> { + return this.http.put<Array<PropertyBEModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/policies/' + policyInstanceId + '/properties', properties) + .map((res) => { + return res.map((resProperty) => new PropertyBEModel(resProperty)); }); } - updateComponentPolicyInstanceProperties(component: Component, policyInstanceId: string, properties: PropertyBEModel[]): Observable<Array<PropertyBEModel>> { - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/policies/' + policyInstanceId + '/properties', properties) - .map((res: Response) => { - return res.json().map((resProperty) => new PropertyBEModel(resProperty)); + addInstanceArtifact = (componentType:string, componentId:string, instanceId:string, artifact:ArtifactModel):Observable<ArtifactModel> => { + // let deferred = this.$q.defer<ArtifactModel>(); + let headerObj = new HttpHeaders(); + if (artifact.payloadData) { + headerObj = headerObj.set('Content-MD5', HttpHelperService.getHeaderMd5(artifact)); + } + return this.http.post<ArtifactModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + instanceId + '/artifacts', JSON.stringify(artifact), {headers: headerObj}) + .map((res) => { + return new ArtifactModel(res); + }); + }; + + updateInstanceArtifact = (componentType:string, componentId:string, instanceId:string, artifact:ArtifactModel):Observable<ArtifactModel> => { + return this.http.post<ArtifactModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + instanceId + '/artifacts/' + artifact.uniqueId, artifact).map((res) => { + return new ArtifactModel(res); + });; + }; + + deleteInstanceArtifact = (componentId:string, componentType:string, instanceId:string, artifactId:string, artifactLabel:string):Observable<ArtifactModel> => { + return this.http.delete<ArtifactModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + "/resourceInstance/" + instanceId + '/artifacts/' + artifactId + '?operation=' + artifactLabel) + .map((res) => { + return new ArtifactModel(res); }); } + + downloadInstanceArtifact = (componentType:string, componentId:string, instanceId:string, artifactId:string):Observable<IFileDownload> => { + return this.http.get<IFileDownload>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + "/resourceInstances/" + instanceId + "/artifacts/" + artifactId); + }; + + uploadInstanceEnvFile = (componentType:string, componentId:string, instanceId:string, artifact:ArtifactModel):Observable<ArtifactModel> => { + let headerObj = new HttpHeaders(); + if (artifact.payloadData) { + headerObj = headerObj.set('Content-MD5', HttpHelperService.getHeaderMd5(artifact)); + } + return this.http.post<ArtifactModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + instanceId + '/artifacts/' + artifact.uniqueId, JSON.stringify(artifact), {headers: headerObj}); + }; + + + updateInstanceAttribute = (componentType:string, componentId:string, attribute:AttributeModel):Observable<AttributeModel> => { + let instanceId = attribute.resourceInstanceUniqueId; + return this.http.post<AttributeModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + "/resourceInstance/" + instanceId + "/attribute", attribute) + .map((response) => { + let newAttribute = new AttributeModel(response); + newAttribute.readonly = true; + newAttribute.resourceInstanceUniqueId = instanceId; + return newAttribute; + }); + }; + } @@ -24,7 +24,7 @@ import { Injectable } from '@angular/core'; import {WorkspaceMode, ComponentState, Role} from "../../../utils/constants"; import { Component as ComponentData } from "app/models"; -import { CacheService } from "app/services/cache-service" +import { CacheService } from "app/services-ng2"; @Injectable() @@ -20,15 +20,18 @@ import {Injectable} from "@angular/core"; -import {Component} from "../../../models/components/component"; +import {Component} from "app/models"; import {ComponentServiceNg2} from "./component.service"; import {ServiceServiceNg2} from "./service.service"; +import {CacheService} from "app/services-ng2"; @Injectable() export class ComponentServiceFactoryNg2 { + componentService: ComponentServiceNg2; serviceService: ServiceServiceNg2; - constructor(componentService: ComponentServiceNg2, serviceService: ServiceServiceNg2) { + + constructor(componentService: ComponentServiceNg2, serviceService: ServiceServiceNg2, private cacheService:CacheService) { this.serviceService = serviceService; this.componentService = componentService; } @@ -23,51 +23,52 @@ import {Injectable, Inject} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/toPromise'; -import {Response, URLSearchParams, Headers} from '@angular/http'; -import { Component, ComponentInstance, InputBEModel, InstancePropertiesAPIMap, FilterPropertiesAssignmentData, - PropertyBEModel, InterfaceModel, OperationModel, BEOperationModel, Capability, Requirement, PolicyInstance} from "app/models"; -import {COMPONENT_FIELDS, CommonUtils, SERVICE_FIELDS} from "app/utils"; -import {downgradeInjectable} from '@angular/upgrade/static'; +import { Component, InputBEModel, InstancePropertiesAPIMap, FilterPropertiesAssignmentData, OperationModel, CreateOperationResponse, ArtifactModel} from "app/models"; +import {COMPONENT_FIELDS} from "app/utils"; import {ComponentGenericResponse} from "../responses/component-generic-response"; import {InstanceBePropertiesMap} from "../../../models/properties-inputs/property-fe-map"; import {API_QUERY_PARAMS} from "app/utils"; -import { ComponentType, ServerTypeUrl } from "../../../utils/constants"; -import { HttpService } from '../http.service'; +import {ComponentType, ServerTypeUrl, SERVICE_FIELDS} from "../../../utils/constants"; import {SdcConfigToken, ISdcConfig} from "../../config/sdc-config.config"; -import {ConstraintObject} from 'app/ng2/components/logic/service-dependencies/service-dependencies.component'; import {IDependenciesServerResponse} from "../responses/dependencies-server-response"; import {AutomatedUpgradeGenericResponse} from "../responses/automated-upgrade-response"; import {IAutomatedUpgradeRequestObj} from "../../pages/automated-upgrade/automated-upgrade.service"; - -declare var angular:angular.IAngularStatic; - +import {ComponentInstance} from "../../../models/componentsInstances/componentInstance"; +import {CommonUtils} from "../../../utils/common-utils"; +import {RelationshipModel} from "../../../models/graph/relationship"; +import { HttpClient, HttpParams, HttpHeaders } from "@angular/common/http"; +import { BEOperationModel, InterfaceModel } from "../../../models/operation"; +import { PropertyBEModel } from "../../../models/properties-inputs/property-be-model"; +import { PolicyInstance } from "../../../models/graph/zones/policy-instance"; +import { ConstraintObject } from "../../components/logic/service-dependencies/service-dependencies.component"; +import { Requirement } from "../../../models/requirement"; +import { Capability } from "../../../models/capability"; + +/* +PLEASE DO NOT USE THIS SERVICE IN ANGULAR2! Use the topology-template.service instead + */ @Injectable() export class ComponentServiceNg2 { protected baseUrl; - constructor(protected http:HttpService, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; } - protected getComponentDataByFieldsName(componentType:string, componentId: string, fields:Array<string>):Observable<ComponentGenericResponse> { + protected getComponentDataByFieldsName(componentType:string, componentId:string, fields:Array<string>):Observable<ComponentGenericResponse> { - let params:URLSearchParams = new URLSearchParams(); + let params: HttpParams = new HttpParams(); _.forEach(fields, (field:string):void => { - params.append(API_QUERY_PARAMS.INCLUDE, field); + params = params.append(API_QUERY_PARAMS.INCLUDE, field); }); - return this.http.get(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/filteredDataByParams', {search: params}) - .map((res:Response) => { - return this.analyzeComponentDataResponse(res); + return this.http.get<ComponentGenericResponse>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/filteredDataByParams', {params: params}) + .map((res) => { + return new ComponentGenericResponse().deserialize(res); }); } - - protected analyzeComponentDataResponse(res: Response):ComponentGenericResponse { - return new ComponentGenericResponse().deserialize(res.json()); - } - - private getServerTypeUrl = (componentType:string):string => { + protected getServerTypeUrl = (componentType:string):string => { switch (componentType) { case ComponentType.SERVICE: return ServerTypeUrl.SERVICES; @@ -76,15 +77,23 @@ export class ComponentServiceNg2 { } } - getComponentMetadata(component:Component):Observable<ComponentGenericResponse> { - return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_METADATA]); + getFullComponent(uniqueId:string):Observable<ComponentGenericResponse> { + return this.http.get<ComponentGenericResponse>(this.baseUrl + uniqueId) + .map((res) => { + return new ComponentGenericResponse().deserialize(res); + }); } + getComponentMetadata(uniqueId:string, type:string):Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(type, uniqueId, [COMPONENT_FIELDS.COMPONENT_METADATA]); + } + + getComponentInstanceAttributesAndProperties(component:Component):Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES, COMPONENT_FIELDS.COMPONENT_INSTANCES_ATTRIBUTES]); } - getComponentInstanceProperties(component:Component):Observable<ComponentGenericResponse> { + getComponentInstanceProperties(component:Component): Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES]); } @@ -132,17 +141,15 @@ export class ComponentServiceNg2 { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_PROPERTIES]); } - getInterfaces(component:Component):Observable<ComponentGenericResponse> { + getInterfaceOperations(component:Component):Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INTERFACE_OPERATIONS]); } getInterfaceOperation(component:Component, operation:OperationModel):Observable<OperationModel> { - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId) - .map((res:Response) => { - return res.json(); - }); + return this.http.get<OperationModel>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations/' + operation.uniqueId); } + // tslint:disable-next-line:member-ordering createInterfaceOperation(component:Component, operation:OperationModel):Observable<OperationModel> { const operationList = { 'interfaces': { @@ -154,9 +161,9 @@ export class ComponentServiceNg2 { } } }; - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations', operationList) - .map((res:Response) => { - const interf:InterfaceModel = _.find(res.json().interfaces, interf => interf.type === operation.interfaceType); + return this.http.post<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations', operationList) + .map((res:any) => { + const interf:InterfaceModel = _.find(res.interfaces, interf => interf.type === operation.interfaceType); const newOperation:OperationModel = _.find(interf.operations, op => op.name === operation.name); return new OperationModel({ ...newOperation, @@ -167,21 +174,22 @@ export class ComponentServiceNg2 { }); } + // tslint:disable-next-line:member-ordering updateInterfaceOperation(component:Component, operation:OperationModel):Observable<OperationModel> { const operationList = { - 'interfaces': { + interfaces: { [operation.interfaceType]: { - 'type': operation.interfaceType, - 'operations': { + type: operation.interfaceType, + operations: { [operation.name]: new BEOperationModel(operation) } } } }; - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations', operationList) - .map((res:Response) => { - const interf:InterfaceModel = _.find(res.json().interfaces, interf => interf.type === operation.interfaceType); - const newOperation:OperationModel = _.find(interf.operations, op => op.name === operation.name); + return this.http.put<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations', operationList) + .map((res: any) => { + const interf: InterfaceModel = _.find(res.interfaces, interf => interf.type === operation.interfaceType); + const newOperation: OperationModel = _.find(interf.operations, op => op.name === operation.name); return new OperationModel({ ...newOperation, interfaceType: interf.type, @@ -191,18 +199,16 @@ export class ComponentServiceNg2 { }); } - deleteInterfaceOperation(component:Component, operation:OperationModel):Observable<OperationModel> { - return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId) - .map((res:Response) => { - return res.json(); - }); + + deleteInterfaceOperation(component: Component, operation:OperationModel):Observable<OperationModel> { + return this.http.delete<OperationModel>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId); } getInterfaceTypes(component:Component):Observable<{[id:string]: Array<string>}> { - return this.http.get(this.baseUrl + 'interfaceLifecycleTypes') - .map((res:Response) => { + return this.http.get<any>(this.baseUrl + 'interfaceLifecycleTypes') + .map((res: any) => { const interfaceMap = {}; - _.forEach(res.json(), (interf:any) => { + _.forEach(res, (interf: any) => { interfaceMap[interf.toscaPresentation.type] = _.keys(interf.toscaPresentation.operations); }); return interfaceMap; @@ -217,19 +223,18 @@ export class ComponentServiceNg2 { payloadData: oldOperation.artifactData }; - const headers = new Headers(); + const headers = new HttpHeaders(); JSON.stringify(payload); const payloadString = JSON.stringify(payload, null, ' '); const md5Result = md5(payloadString).toLowerCase(); headers.append('Content-MD5', btoa(md5Result)); return this.http.post(this.baseUrl + component.getTypeUrl() + component.uuid + '/interfaces/' + newOperation.interfaceId + '/operations/' + newOperation.uniqueId + '/artifacts/' + newOperation.implementation.artifactUUID, - payload, - {headers} - ).map((res: Response) => { - const fileName = res.json().artifactDisplayName || res.json().artifactName; + payload, {headers} + ).map((res: any) => { + const fileName = res.artifactDisplayName || res.artifactName; newOperation.artifactFileName = fileName; - return res.json(); + return res; }); } @@ -237,78 +242,17 @@ export class ComponentServiceNg2 { return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_REQUIREMENTS, COMPONENT_FIELDS.COMPONENT_CAPABILITIES]); } - createCapability(component: Component, capabilityData: Capability): Observable<Array<Capability>> { - let capBEObj = { - 'capabilities': { - [capabilityData.type]: [capabilityData] - } - }; - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/capabilities', capBEObj) - .map((res: Response) => { - return res.json(); - }); - } - updateCapability(component: Component, capabilityData: Capability): Observable<Array<Capability>> { - let capBEObj = { - 'capabilities': { - [capabilityData.type]: [capabilityData] - } - }; - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/capabilities', capBEObj) - .map((res: Response) => { - return res.json(); - }); - } - deleteCapability(component: Component, capId: string): Observable<Capability> { - return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/capabilities/' + capId) - .map((res: Response) => { - return res.json(); - }); - } - createRequirement(component: Component, requirementData: Requirement): Observable<any> { - let reqBEObj = { - 'requirements': { - [requirementData.capability]: [requirementData] - } - }; - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/requirements', reqBEObj) - .map((res: Response) => { - return res.json(); - }); - } - - updateRequirement(component: Component, requirementData: Requirement): Observable<any> { - let reqBEObj = { - 'requirements': { - [requirementData.capability]: [requirementData] - } - }; - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/requirements', reqBEObj) - .map((res: Response) => { - return res.json(); - }); - } - - deleteRequirement(component: Component, reqId: string): Observable<Requirement> { - return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/requirements/' + reqId) - .map((res: Response) => { - return res.json(); - }); - } getDeploymentGraphData(component:Component):Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_RELATION, COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_GROUPS]); } createInput(component:Component, inputsToCreate:InstancePropertiesAPIMap, isSelf:boolean):Observable<any> { - let inputs = isSelf ? { serviceProperties: inputsToCreate.componentInstanceProperties } : inputsToCreate; - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/inputs', inputs) - .map(res => { - return res.json(); - }) + const inputs = isSelf ? { serviceProperties: inputsToCreate.componentInstanceProperties } : inputsToCreate; + return this.http.post<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/inputs', inputs); } createListInput(component:Component, input:any, isSelf:boolean):Observable<any> { @@ -324,10 +268,7 @@ export class ComponentServiceNg2 { } else { inputs = input; } - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/listInput', inputs) - .map(res => { - return res.json(); - }) + return this.http.post<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/listInput', inputs); } createPolicy(component:Component, policiesToCreate:InstancePropertiesAPIMap, isSelf:boolean):Observable<any> { @@ -341,78 +282,71 @@ export class ComponentServiceNg2 { ...policiesToCreate.componentInstanceProperties } }; - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/policies', policiesList) - .map(res => { - return res.json(); - }); + return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/policies', policiesList); } deletePolicy(component:Component, policy: PolicyInstance) { - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/policies/' + policy.uniqueId + '/undeclare', policy) - .map(res => { - return res.json(); - }); + return this.http.put<PolicyInstance>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/policies/' + policy.uniqueId + '/undeclare', policy); } - restoreComponent(componentType:string, componentId:string){ + restoreComponent(componentType:string, componentId:string) { return this.http.post(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/restore', {}) } - archiveComponent(componentType:string, componentId:string){ + archiveComponent(componentType:string, componentId:string) { return this.http.post(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/archive', {}) } deleteInput(component:Component, input:InputBEModel):Observable<InputBEModel> { - return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/delete/' + input.uniqueId + '/input') - .map((res:Response) => { - return new InputBEModel(res.json()); - }); + + return this.http.delete<InputBEModel>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/delete/' + input.uniqueId + '/input') + .map((res) => { + return new InputBEModel(res); + }) } updateComponentInputs(component:Component, inputs:InputBEModel[]):Observable<InputBEModel[]> { - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/update/inputs', inputs) - .map((res:Response) => { - return res.json().map((input) => new InputBEModel(input)); + + return this.http.post<InputBEModel[]>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/update/inputs', inputs) + .map((res) => { + return res.map((input) => new InputBEModel(input)); }) } - filterComponentInstanceProperties(component: Component, filterData:FilterPropertiesAssignmentData): Observable<InstanceBePropertiesMap> {//instance-property-be-map - let params: URLSearchParams = new URLSearchParams(); + filterComponentInstanceProperties(component:Component, filterData:FilterPropertiesAssignmentData):Observable<InstanceBePropertiesMap> {//instance-property-be-map + let params: HttpParams = new HttpParams(); _.forEach(filterData.selectedTypes, (type:string) => { - params.append('resourceType', type); + params = params.append('resourceType', type); }); - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/filteredproperties/' + filterData.propertyName, {search: params}) - .map((res: Response) => { - return res.json(); - }); + return this.http.get<InstanceBePropertiesMap>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/filteredproperties/' + filterData.propertyName, {params: params}); } createServiceProperty(component: Component, propertyModel:PropertyBEModel): Observable<PropertyBEModel> { let serverObject = {}; serverObject[propertyModel.name] = propertyModel; return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/properties', serverObject) - .map(res => { - let property:PropertyBEModel = new PropertyBEModel(res.json()); + .map((res: PropertyBEModel) => { + const property: PropertyBEModel = new PropertyBEModel(res); return property; }) } - getServiceProperties(component: Component): Observable<Array<PropertyBEModel>> { + getServiceProperties(component: Component): Observable<PropertyBEModel[]> { return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/properties') - .map((res: Response) => { - if (!res.text()){ + .map((res: PropertyBEModel[]) => { + if (!res) { return new Array<PropertyBEModel>(); } - return CommonUtils.initBeProperties(res.json()); + return CommonUtils.initBeProperties(res); }); } updateServiceProperties(component: Component, properties: PropertyBEModel[]) { - return this.http.put( this.baseUrl + component.getTypeUrl() + component.uniqueId + '/properties', properties) - .map((res: Response) => { - const resJson = res.json(); + return this.http.put<PropertyBEModel[]>( this.baseUrl + component.getTypeUrl() + component.uniqueId + '/properties', properties) + .map((res) => { + const resJson = res; return _.map(resJson, (resValue:PropertyBEModel) => new PropertyBEModel(resValue)); }); @@ -425,25 +359,16 @@ export class ComponentServiceNg2 { }) } - getDependencies(componentType:string, componentId: string):Observable<Array<IDependenciesServerResponse>> { - return this.http.get(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/dependencies') - .map((res:Response) => { - return res.json(); - }); + getDependencies(componentType:string, componentId: string):Observable<IDependenciesServerResponse[]> { + return this.http.get<IDependenciesServerResponse[]>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/dependencies'); } - automatedUpgrade(componentType:string, componentId: string, componentsIdsToUpgrade:Array<IAutomatedUpgradeRequestObj>):Observable<AutomatedUpgradeGenericResponse> { - return this.http.post(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/automatedupgrade', componentsIdsToUpgrade) - .map((res:Response) => { - return res.json(); - }); + automatedUpgrade(componentType:string, componentId:string, componentsIdsToUpgrade:IAutomatedUpgradeRequestObj[]):Observable<AutomatedUpgradeGenericResponse> { + return this.http.post<AutomatedUpgradeGenericResponse>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/automatedupgrade', componentsIdsToUpgrade); } - updateComponentInstance(component:Component, componentInstance:ComponentInstance):Observable<ComponentInstance> { - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstance/' + componentInstance.uniqueId, componentInstance) - .map((res:Response) => { - return res.json(); - }); + updateMultipleComponentInstances(componentId:string, instances:ComponentInstance[]):Observable<ComponentInstance[]> { + return this.http.post<ComponentInstance[]>(this.baseUrl + componentId + '/resourceInstance/multipleComponentInstance', instances); } getServiceFilterConstraints(component:Component):Observable<ComponentGenericResponse> { @@ -451,24 +376,18 @@ export class ComponentServiceNg2 { } createServiceFilterConstraints(component:Component, componentInstance:ComponentInstance, constraint:ConstraintObject):Observable<any> { - return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstances/' + componentInstance.uniqueId + '/nodeFilter', constraint) - .map((res:Response) => { - return res.json(); - }); + return this.http.post<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstances/' + componentInstance.uniqueId + '/nodeFilter', constraint); } - updateServiceFilterConstraints(component:Component, componentInstance:ComponentInstance, constraints:Array<ConstraintObject>):Observable<any> { - return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstances/' + componentInstance.uniqueId + '/nodeFilter/', constraints) - .map((res:Response) => { - return res.json(); - }); + updateServiceFilterConstraints(component:Component, componentInstance:ComponentInstance, constraints:ConstraintObject[]):Observable<any> { + return this.http.put<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstances/' + componentInstance.uniqueId + '/nodeFilter/', constraints); } deleteServiceFilterConstraints(component:Component, componentInstance:ComponentInstance, constraintIndex:number) { - return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstances/' + componentInstance.uniqueId + '/nodeFilter/' + constraintIndex) - .map((res:Response) => { - return res.json(); - }); + return this.http.delete<any>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstances/' + componentInstance.uniqueId + '/nodeFilter/' + constraintIndex); } -} + protected analyzeComponentDataResponse(res: Response):ComponentGenericResponse { + return new ComponentGenericResponse().deserialize(res); + } +} @@ -21,14 +21,14 @@ import { Injectable } from '@angular/core'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/toPromise'; -import { Http, Response, Headers, RequestOptions } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class ResourceServiceNg2 { protected baseUrl = ""; - constructor(private http: Http) { + constructor(private http: HttpClient) { } @@ -22,9 +22,7 @@ import { Injectable, Inject } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/toPromise'; -import { Response, URLSearchParams } from '@angular/http'; -import {Service, OperationModel} from "app/models"; -import { HttpService } from '../http.service'; +import {Service} from "app/models"; import {SdcConfigToken, ISdcConfig} from "../../config/sdc-config.config"; import {ForwardingPath} from "app/models/forwarding-path"; @@ -34,77 +32,53 @@ import {Component} from "app/models/components/component"; import {ComponentGenericResponse} from "app/ng2/services/responses/component-generic-response"; import {COMPONENT_FIELDS, SERVICE_FIELDS} from "app/utils/constants"; import {ComponentServiceNg2} from "./component.service"; -import {ServiceGenericResponse} from "app/ng2/services/responses/service-generic-response"; import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map"; -import {ConsumptionInput} from 'app/ng2/components/logic/service-consumption/service-consumption.component'; - +import { HttpClient, HttpParams } from '@angular/common/http'; +import { OperationModel } from '../../../models/operation'; +import { ConsumptionInput } from '../../components/logic/service-consumption/service-consumption.component'; @Injectable() export class ServiceServiceNg2 extends ComponentServiceNg2 { protected baseUrl = ""; - constructor(protected http: HttpService, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { super(http, sdcConfig); this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; } validateConformanceLevel(service: Service): Observable<boolean> { - return this.http.get(this.baseUrl + service.getTypeUrl() + service.uuid + '/conformanceLevelValidation') - .map((res: Response) => { - return res.json(); - }); - } - - getNodesAndLinksMap(service: Service):Observable<Array<ServicePathMapItem>> { - return this.http.get(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/linksMap').map(res => { - return <Array<ServicePathMapItem>>res.json(); - }); - } - - getServicePath(service: Service, id: string):Observable<any> { - return this.http.get(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/paths/' + id) - .map(res => { - return res.json(); - }) + return this.http.get<boolean>(this.baseUrl + service.getTypeUrl() + service.uuid + '/conformanceLevelValidation'); } - getServicePaths(service: Service):Observable<any> { - return this.http.get(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/paths') - .map(res => { - return res.json(); - }) + getNodesAndLinksMap(serviceId: string):Observable<Array<ServicePathMapItem>> { + return this.http.get<Array<ServicePathMapItem>>(this.baseUrl + 'services/' + serviceId + '/linksMap'); } - createOrUpdateServicePath(service: Service, inputsToCreate: ForwardingPath):Observable<ForwardingPath> { + createOrUpdateServicePath(serviceId: string, inputsToCreate: ForwardingPath):Observable<ForwardingPath> { if (inputsToCreate.uniqueId) { - return this.updateServicePath(service, inputsToCreate); + return this.updateServicePath(serviceId, inputsToCreate); } else { - return this.createServicePath(service, inputsToCreate); + return this.createServicePath(serviceId, inputsToCreate); } } - createServicePath(service: Service, inputsToCreate: ForwardingPath):Observable<ForwardingPath> { + createServicePath(serviceId: string, inputsToCreate: ForwardingPath):Observable<ForwardingPath> { let input = new ServicePathRequestData(inputsToCreate); - - return this.http.post(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/paths', input) - .map(res => { - return this.parseServicePathResponse(res); - }); + return this.http.post<ForwardingPath>(this.baseUrl + 'services/' + serviceId + '/paths', input).map((res:any) => { + return this.parseServicePathResponse(res); + }); } - deleteServicePath(service: Service, id: string):Observable<any> { - return this.http.delete(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/paths/' + id ) - .map((res) => { - return res.json(); - }); + deleteServicePath(serviceId: string, id: string):Observable<any> { + return this.http.delete<any>(this.baseUrl + 'services/' + serviceId + '/paths/' + id); } - updateServicePath(service: Service, inputsToUpdate:ForwardingPath):Observable<ForwardingPath> { + updateServicePath(serviceId: string, inputsToUpdate:ForwardingPath):Observable<ForwardingPath> { let input = new ServicePathRequestData(inputsToUpdate); - return this.http.put(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/paths', input) + return this.http.put<{[key:string]:ForwardingPath}>(this.baseUrl + 'services/' + serviceId + '/paths', input) .map((res) => { return this.parseServicePathResponse(res); }); @@ -122,28 +96,25 @@ export class ServiceServiceNg2 extends ComponentServiceNg2 { } getServiceConsumptionInputs(service: Service, serviceInstanceId: String, interfaceId: string, operation: OperationModel): Observable<any> { - return this.http.get(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/consumption/' + serviceInstanceId + '/interfaces/' + interfaceId + '/operations/' + operation.uniqueId + '/inputs') - .map(res => { - return res.json(); - }); + return this.http.get<any>(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/consumption/' + serviceInstanceId + '/interfaces/' + interfaceId + + '/operations/' + operation.uniqueId + '/inputs'); } createOrUpdateServiceConsumptionInputs(service: Service, serviceInstanceId: String, consumptionInputsList: Array<{[id: string]: Array<ConsumptionInput>}>): Observable<any> { return this.http.post(this.baseUrl + service.getTypeUrl() + service.uniqueId + '/consumption/' + serviceInstanceId, consumptionInputsList); } - checkComponentInstanceVersionChange(service: Service, newVersionId: string):Observable<Array<string>> { - let instanceId = service.selectedInstance.uniqueId; - let queries = {componentInstanceId: instanceId, newComponentInstanceId: newVersionId}; + checkComponentInstanceVersionChange(componentType:string, componentId:string, instanceId:string, newInstanceId:string):Observable<Array<string>> { + let queries = {componentInstanceId: instanceId, newComponentInstanceId: newInstanceId}; - let params:URLSearchParams = new URLSearchParams(); + let params:HttpParams = new HttpParams(); _.map(_.keys(queries), (key:string):void => { - params.append(key, queries[key]); + params = params.append(key, queries[key]); }); - let url = this.baseUrl + service.getTypeUrl() + service.uniqueId + '/paths-to-delete'; - return this.http.get(url, {search: params}).map((res: Response) => { - return res.json().forwardingPathToDelete; + let url = this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/paths-to-delete'; + return this.http.get<any>(url, {params: params}).map((res) => { + return res.forwardingPathToDelete; }); } @@ -151,12 +122,8 @@ export class ServiceServiceNg2 extends ComponentServiceNg2 { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_RELATION, COMPONENT_FIELDS.COMPONENT_INSTANCES, SERVICE_FIELDS.FORWARDING_PATHS, COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_POLICIES, COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_GROUPS]); } - protected analyzeComponentDataResponse(res: Response):ComponentGenericResponse { - return new ServiceGenericResponse().deserialize(res.json()); - } - - private parseServicePathResponse(res: Response):ForwardingPath { - let resJSON = res.json(); + private parseServicePathResponse(res: { [key:string]:ForwardingPath }):ForwardingPath { + let resJSON = res; let pathId = Object.keys(resJSON.forwardingPaths)[0]; let forwardingPath = resJSON.forwardingPaths[pathId]; let path:ForwardingPath = new ForwardingPath(); @@ -0,0 +1,515 @@ +/** + * Created by ob0695 on 6/26/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import {Injectable, Inject} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/toPromise'; +import { + Component, + InputBEModel, + InstancePropertiesAPIMap, + FilterPropertiesAssignmentData, + ArtifactModel, + PropertyModel, + IFileDownload, + AttributeModel, + IAttributeModel, Capability, Requirement +} from "app/models"; +import {ArtifactGroupType, COMPONENT_FIELDS} from "app/utils"; +import {ComponentGenericResponse} from "../responses/component-generic-response"; +import {InstanceBePropertiesMap} from "../../../models/properties-inputs/property-fe-map"; +import {API_QUERY_PARAMS} from "app/utils"; +import {ComponentType, ServerTypeUrl, SERVICE_FIELDS} from "../../../utils/constants"; +import {SdcConfigToken, ISdcConfig} from "../../config/sdc-config.config"; +import {IDependenciesServerResponse} from "../responses/dependencies-server-response"; +import {AutomatedUpgradeGenericResponse} from "../responses/automated-upgrade-response"; +import {IAutomatedUpgradeRequestObj} from "../../pages/automated-upgrade/automated-upgrade.service"; +import {ComponentInstance} from "../../../models/componentsInstances/componentInstance"; +import {CommonUtils} from "../../../utils/common-utils"; +import {RelationshipModel} from "../../../models/graph/relationship"; +import {ServiceGenericResponse} from "../responses/service-generic-response"; +import { HttpClient, HttpParams, HttpHeaders } from "@angular/common/http"; +import { HttpHelperService } from "../http-hepler.service"; +import { + Component as TopologyTemplate, + FullComponentInstance, + Service, + OperationModel, +} from 'app/models'; +import { ConsumptionInput } from "../../components/logic/service-consumption/service-consumption.component"; +import { ConstraintObject } from "../../components/logic/service-dependencies/service-dependencies.component"; +import { ComponentMetadata } from "../../../models/component-metadata"; +import { PolicyInstance } from "../../../models/graph/zones/policy-instance"; +import { PropertyBEModel } from "../../../models/properties-inputs/property-be-model"; + +/* we need to use this service from now, we will remove component.service when we finish remove the angular1. + The service is duplicated since we can not use downgrades service with NGXS*/ + +@Injectable() +export class TopologyTemplateService { + + protected baseUrl; + + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { + this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; + } + + getFullComponent(componentType: string, uniqueId: string): Observable<Component> { + return this.http.get<Component>(this.baseUrl + this.getServerTypeUrl(componentType) + uniqueId); + } + + getComponentMetadata(uniqueId: string, type: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(type, uniqueId, [COMPONENT_FIELDS.COMPONENT_METADATA]); + } + + getComponentInstanceAttributesAndProperties(uniqueId: string, type: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(type, uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES, COMPONENT_FIELDS.COMPONENT_INSTANCES_ATTRIBUTES]); + } + + async getComponentAttributes(componentType: string, componentId: string): Promise<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_ATTRIBUTES]).toPromise(); + } + + getComponentCompositionData(componentUniqueId: string, componentType: string): Observable<ComponentGenericResponse> { + const params: string[] = [COMPONENT_FIELDS.COMPONENT_INSTANCES_RELATION, COMPONENT_FIELDS.COMPONENT_INSTANCES, + COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_POLICIES, COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_GROUPS]; + if (componentType === ComponentType.SERVICE) { + params.push(COMPONENT_FIELDS.FORWARDING_PATHS); + } + return this.getComponentDataByFieldsName(componentType, componentUniqueId, params); + } + + getComponentResourcePropertiesData(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, + [COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_POLICIES, COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_GROUPS]); + } + + getComponentResourceInstances(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES]); + } + + getComponentInputs(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INPUTS]); + } + + getComponentInputsWithProperties(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, + [COMPONENT_FIELDS.COMPONENT_INPUTS, COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES, COMPONENT_FIELDS.COMPONENT_PROPERTIES]); + } + + getComponentDeploymentArtifacts(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_DEPLOYMENT_ARTIFACTS]); + } + + getComponentInformationalArtifacts(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INFORMATIONAL_ARTIFACTS]); + } + + getComponentInformationalArtifactsAndInstances(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INFORMATIONAL_ARTIFACTS, COMPONENT_FIELDS.COMPONENT_INSTANCES]); + } + + getComponentToscaArtifacts(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_TOSCA_ARTIFACTS]); + } + + getComponentProperties(component: Component): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_PROPERTIES]); + } + + getCapabilitiesAndRequirements(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_REQUIREMENTS, COMPONENT_FIELDS.COMPONENT_CAPABILITIES]); + } + + getRequirementsAndCapabilitiesWithProperties(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, + [COMPONENT_FIELDS.COMPONENT_REQUIREMENTS, COMPONENT_FIELDS.COMPONENT_CAPABILITIES, COMPONENT_FIELDS.COMPONENT_CAPABILITIES_PROPERTIES]); + } + + getDeploymentGraphData(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_RELATION, COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_GROUPS]); + } + + createInput(component: Component, inputsToCreate: InstancePropertiesAPIMap, isSelf: boolean): Observable<any> { + const inputs = isSelf ? { serviceProperties: inputsToCreate.componentInstanceProperties } : inputsToCreate; + return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/inputs', inputs); + } + + restoreComponent(componentType: string, componentId: string) { + return this.http.post(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/restore', {}); + } + + archiveComponent(componentType: string, componentId: string) { + return this.http.post(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/archive', {}); + } + + deleteInput(component: Component, input: InputBEModel): Observable<InputBEModel> { + return this.http.delete<InputBEModel>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/delete/' + input.uniqueId + '/input') + .map((res) => { + return new InputBEModel(res); + }); + } + + updateComponentInputs(component: Component, inputs: InputBEModel[]): Observable<InputBEModel[]> { + return this.http.post<InputBEModel[]>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/update/inputs', inputs) + .map((res) => { + return res.map((input) => new InputBEModel(input)); + }); + } + + filterComponentInstanceProperties(component: Component, filterData: FilterPropertiesAssignmentData): Observable<InstanceBePropertiesMap> {// instance-property-be-map + let params: HttpParams = new HttpParams(); + _.forEach(filterData.selectedTypes, (type: string) => { + params = params.append('resourceType', type); + }); + + // tslint:disable-next-line:object-literal-shorthand + return this.http.get<InstanceBePropertiesMap>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/filteredproperties/' + filterData.propertyName, {params: params}); + } + + createServiceProperty(componentId: string, propertyModel: PropertyBEModel): Observable<PropertyBEModel> { + const serverObject = {}; + serverObject[propertyModel.name] = propertyModel; + return this.http.post<PropertyBEModel>(this.baseUrl + 'services/' + componentId + '/properties', serverObject) + .map((res) => { + const property: PropertyBEModel = new PropertyBEModel(res); + return property; + }); + } + + getServiceProperties(componentId: string): Observable<PropertyBEModel[]> { + return this.http.get<any>(this.baseUrl + 'services/' + componentId + '/properties') + .map((res) => { + if (!res) { + return new Array<PropertyBEModel>(); + } + return CommonUtils.initBeProperties(res); + }); + } + + updateServiceProperties(componentId: string, properties: PropertyBEModel[]) { + return this.http.put<any>( this.baseUrl + 'services/' + componentId + '/properties', properties) + .map((res) => { + const resJson = res; + return _.map(resJson, + (resValue: PropertyBEModel) => new PropertyBEModel(resValue)); + }); + } + + deleteServiceProperty(componentId: string, property: PropertyBEModel): Observable<string> { + return this.http.delete(this.baseUrl + 'services/' + componentId + '/properties/' + property.uniqueId ) + .map((res: Response) => { + return property.uniqueId; + }); + } + + getDependencies(componentType: string, componentId: string): Observable<IDependenciesServerResponse[]> { + return this.http.get<IDependenciesServerResponse[]>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/dependencies'); + } + + automatedUpgrade(componentType: string, componentId: string, componentsIdsToUpgrade: IAutomatedUpgradeRequestObj[]): Observable<AutomatedUpgradeGenericResponse> { + return this.http.post<AutomatedUpgradeGenericResponse>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/automatedupgrade', componentsIdsToUpgrade); + } + + updateComponentInstance(componentMetaDataId: string, componentInstance:ComponentInstance): Observable<ComponentInstance> { + return this.http.post<ComponentInstance>(this.baseUrl + 'services/' + componentMetaDataId + '/resourceInstance/' + componentInstance.uniqueId, componentInstance); + } + + updateMultipleComponentInstances(componentId: string, componentType: string, instances: ComponentInstance[]): Observable<ComponentInstance[]> { + return this.http.post<ComponentInstance[]>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/multipleComponentInstance', instances) + .map((res) => { + return CommonUtils.initComponentInstances(res); + }); + } + + createRelation(componentId: string, componentType: string, link: RelationshipModel): Observable<RelationshipModel> { + return this.http.post<RelationshipModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/associate', link) + .map((res) => { + return new RelationshipModel(res); + }); + } + + deleteRelation(componentId: string, componentType: string, link: RelationshipModel): Observable<RelationshipModel> { + return this.http.put<RelationshipModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/dissociate', link) + .map((res) => { + return new RelationshipModel(res); + }); + } + + createComponentInstance(componentType: string, componentId: string, componentInstance: ComponentInstance): Observable<ComponentInstance> { + return this.http.post<ComponentInstance>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance', componentInstance) + .map((res) => { + return new ComponentInstance(res); + }); + } + + deleteComponentInstance(componentType: string, componentId: string, componentInstanceId: string): Observable<ComponentInstance> { + return this.http.delete<ComponentInstance>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + componentInstanceId) + .map((res) => { + return new ComponentInstance(res); + }); + } + + fetchRelation(componentType: string, componentId: string, linkId: string): Observable<RelationshipModel> { + return this.http.get<RelationshipModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/relationId/' + linkId) + .map((res) => { + return new RelationshipModel(res); + }); + } + + addOrUpdateArtifact = (componentType: string, componentId: string, artifact: ArtifactModel): Observable<ArtifactModel> => { + let headerObj: HttpHeaders = new HttpHeaders(); + if (artifact.payloadData) { + headerObj = headerObj.append('Content-MD5', HttpHelperService.getHeaderMd5(artifact)); + } + + let artifactID: string = ''; + if (artifact.uniqueId) { + artifactID = '/' + artifact.uniqueId; + } + return this.http.post<ArtifactModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/artifacts' + artifactID, JSON.stringify(artifact), {headers: headerObj}).map( + (res) => new ArtifactModel(res) + ); + } + + deleteArtifact = (componentId: string, componentType: string, artifactId: string, artifactLabel: string): Observable<ArtifactModel> => { + return this.http.delete<ArtifactModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/artifacts/' + artifactId + '?operation=' + artifactLabel) + .map((res) => new ArtifactModel(res)); + } + + downloadArtifact = (componentType: string, componentId: string, artifactId: string): Observable<IFileDownload> => { + return this.http.get<IFileDownload>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/artifacts/' + artifactId); + } + + // ------------------------------------------------ Properties API --------------------------------------------------// + addProperty = (componentType: string, componentId: string, property: PropertyModel):Observable<PropertyModel> => { + return this.http.post<PropertyModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/properties', property.convertToServerObject()).map((response) => { + return new PropertyModel(response[Object.keys(response)[0]]); + }); + } + + updateProperty = (componentType: string, componentId: string, property: PropertyModel): Observable<PropertyModel> => { + var propertiesList:PropertyBEModel[] = [property]; + return this.http.put<PropertyModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/properties', propertiesList) + .map((response) => { + return new PropertyModel(response[Object.keys(response)[0]]); + }); + } + + deleteProperty = (componentType: string, componentId: string, propertyId: string): Observable<PropertyModel> => { + return this.http.delete<PropertyModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/properties/' + propertyId); + } + + // ------------------------------------------------ Attributes API --------------------------------------------------// + addAttribute = (componentType: string, componentId: string, attribute: AttributeModel): Observable<AttributeModel> => { + return this.http.post<AttributeModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/attributes', attribute.convertToServerObject()) + .map((response) => { + return new AttributeModel(response); + }); + } + + updateAttribute = (componentType: string, componentId: string, attribute: AttributeModel): Observable<AttributeModel> => { + const payload = attribute.convertToServerObject(); + + return this.http.put<AttributeModel>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/attributes/' + attribute.uniqueId, payload) + .map((response) => { + return new AttributeModel(response); + }); + } + + // Async Methods + addAttributeAsync = async (componentType: string, componentId: string, attribute: AttributeModel): Promise<AttributeModel> => { + return this.addAttribute(componentType, componentId, attribute).toPromise(); + } + + updateAttributeAsync = async (componentType: string, componentId: string, attribute: AttributeModel): Promise<AttributeModel> => { + return this.updateAttribute(componentType, componentId, attribute).toPromise(); + } + + deleteAttributeAsync = async (componentType: string, componentId: string, attribute: AttributeModel): Promise<any> => { + return this.http.delete<any>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/attributes/' + attribute.uniqueId, {}).toPromise(); + } + + getArtifactsByType(componentType: string, componentId: string, artifactsType: ArtifactGroupType) { + return this.getComponentDataByFieldsName(componentType, componentId, [this.convertArtifactTypeToUrl(artifactsType)]); + } + + getServiceConsumptionData(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [ + // COMPONENT_FIELDS.COMPONENT_INSTANCES_INTERFACES, + COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES, + // COMPONENT_FIELDS.COMPONENT_INSTANCES_INPUTS, + COMPONENT_FIELDS.COMPONENT_INPUTS, + COMPONENT_FIELDS.COMPONENT_INSTANCES, + COMPONENT_FIELDS.COMPONENT_CAPABILITIES + ]); + } + + getServiceConsumptionInputs(componentMetaDataId: string, serviceInstanceId: string, interfaceId: string, operation: OperationModel): Observable<ConsumptionInput[]> { + return this.http.get<ConsumptionInput[]> + (this.baseUrl + 'services/' + componentMetaDataId + '/consumption/' + serviceInstanceId + '/interfaces/' + interfaceId + '/operations/' + operation.uniqueId + '/inputs'); + } + + createOrUpdateServiceConsumptionInputs(componentMetaDataId: string, serviceInstanceId: string, consumptionInputsList: Array<{[id: string]: ConsumptionInput[]}>): Observable<any> { + return this.http.post(this.baseUrl + 'services/' + componentMetaDataId + '/consumption/' + serviceInstanceId, consumptionInputsList); + } + + getServiceFilterConstraints(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [SERVICE_FIELDS.NODE_FILTER]); + } + + getComponentInstanceProperties(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES]); + } + + createServiceFilterConstraints(componentMetaDataId: string, componentInstanceId: string, constraint: ConstraintObject): Observable<any> { + return this.http.post<any>(this.baseUrl + 'services/' + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/nodeFilter', constraint); + } + + updateServiceFilterConstraints(componentMetaDataId: string, componentInstanceId: string, constraints: ConstraintObject[]):Observable<any> { + return this.http.put<any>(this.baseUrl + 'services/' + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/nodeFilter', constraints) + } + + deleteServiceFilterConstraints(componentMetaDataId: string, componentInstanceId: string, constraintIndex: number): Observable<any>{ + return this.http.delete<any>(this.baseUrl + 'services/' + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/nodeFilter/' + constraintIndex) + } + + deletePolicy(component: Component, policy: PolicyInstance): Observable<PolicyInstance> { + return this.http.put<PolicyInstance>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/policies/' + policy.uniqueId + '/undeclare', policy) + } + + createListInput(componentId: string, input: any, isSelf: boolean): Observable<any> { + let inputs: any; + if (isSelf) { + // change componentInstanceProperties -> serviceProperties + inputs = { + componentInstInputsMap: { + serviceProperties: input.componentInstInputsMap.componentInstanceProperties + }, + listInput: input.listInput + }; + } else { + inputs = input; + } + return this.http.post<any>(this.baseUrl + 'services/' + componentId + '/create/listInput', inputs); + } + + createPolicy(component: Component, policiesToCreate: InstancePropertiesAPIMap, isSelf: boolean): Observable<any> { + const policiesList = + isSelf ? + // tslint:disable-next-line:object-literal-key-quotes + {'componentPropertiesToPolicies': { + ...policiesToCreate.componentInstanceProperties + } + } : + // tslint:disable-next-line:object-literal-key-quotes + {'componentInstancePropertiesToPolicies': { + ...policiesToCreate.componentInstanceProperties + } + }; + return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/policies', policiesList); + } + + protected getComponentDataByFieldsName(componentType: string, componentId: string, fields: string[]): Observable<ComponentGenericResponse> { + let params: HttpParams = new HttpParams(); + _.forEach(fields, (field: string): void => { + params = params.append(API_QUERY_PARAMS.INCLUDE, field); + }); + // tslint:disable-next-line:object-literal-shorthand + return this.http.get<ComponentGenericResponse>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/filteredDataByParams', {params: params}) + .map((res) => { + return componentType === ComponentType.SERVICE ? new ServiceGenericResponse().deserialize(res) : + new ComponentGenericResponse().deserialize(res); + }); + } + + private getServerTypeUrl = (componentType: string): string => { + switch (componentType) { + case ComponentType.SERVICE: + case ComponentType.SERVICE_PROXY: + return ServerTypeUrl.SERVICES; + default: + return ServerTypeUrl.RESOURCES; + } + } + + private convertArtifactTypeToUrl = (artifactType: ArtifactGroupType): string => { + switch (artifactType) { + case ArtifactGroupType.TOSCA: + return COMPONENT_FIELDS.COMPONENT_TOSCA_ARTIFACTS; + case ArtifactGroupType.INFORMATION: + return COMPONENT_FIELDS.COMPONENT_INFORMATIONAL_ARTIFACTS; + case ArtifactGroupType.DEPLOYMENT: + return COMPONENT_FIELDS.COMPONENT_DEPLOYMENT_ARTIFACTS; + case ArtifactGroupType.SERVICE_API: + return COMPONENT_FIELDS.SERVICE_API_ARTIFACT; + } + } + + // createCapability(component: Component, capabilityData: Capability): Observable<Capability[]> { + createCapability(type: string, uniqueId: string, capabilityData: Capability): Observable<Capability[]> { + let capBEObj = { + 'capabilities': { + [capabilityData.type]: [capabilityData] + } + }; + return this.http.post<any>(this.baseUrl + type + uniqueId + '/capabilities', capBEObj); + } + + updateCapability(type: string, uniqueId: string, capabilityData: Capability): Observable<Capability[]> { + let capBEObj = { + 'capabilities': { + [capabilityData.type]: [capabilityData] + } + }; + return this.http.put<any>(this.baseUrl + type + uniqueId + '/capabilities', capBEObj); + } + + deleteCapability(component: Component, capId: string): Observable<Capability> { + return this.http.delete<Capability>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/capabilities/' + capId); + } + + createRequirement(type: string, uniqueId: string, requirementData: Requirement): Observable<any> { + let reqBEObj = { + 'requirements': { + [requirementData.capability]: [requirementData] + } + }; + return this.http.post(this.baseUrl + type + uniqueId + '/requirements', reqBEObj); + } + + updateRequirement(type: string, uniqueId: string, requirementData: Requirement): Observable<any> { + let reqBEObj = { + 'requirements': { + [requirementData.capability]: [requirementData] + } + }; + return this.http.put(this.baseUrl + type + uniqueId + '/requirements', reqBEObj); + } + + deleteRequirement(component: Component, reqId: string): Observable<Requirement> { + return this.http.delete<Requirement>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/requirements/' + reqId); + } +} @@ -16,62 +16,80 @@ * */ -import { Injectable, Inject } from '@angular/core'; -import { Http, Response } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable, Injector } from '@angular/core'; +import { IAppConfigurtaion, Plugins, PluginsConfiguration, ValidationConfiguration, Validations } from 'app/models'; +import { IApi } from 'app/models/app-config'; import 'rxjs/add/operator/toPromise'; -import {IAppConfigurtaion, ValidationConfiguration, Validations, Plugins, PluginsConfiguration} from "app/models"; -import {IApi} from "app/models/app-config"; -import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; +import { ISdcConfig, SdcConfigToken } from '../config/sdc-config.config'; +import { CacheService } from './cache.service'; @Injectable() export class ConfigService { - private baseUrl; public configuration: IAppConfigurtaion; - public api:IApi; + public api: IApi; + private baseUrl; + + constructor( + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig, + private cacheService: CacheService, + private injector: Injector, + private http: HttpClient + ) { + this.api = this.sdcConfig.api; + this.baseUrl = this.api.root + this.sdcConfig.api.component_api_root; + } - constructor(private http: Http, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { - this.api = this.sdcConfig.api; - this.baseUrl = this.api.root + this.sdcConfig.api.component_api_root; + loadSdcSetupData = (): Promise<void> => { + const url: string = this.api.root + this.api.GET_SDC_Setup_Data; + const promise: Promise<any> = this.http.get<any>(url).toPromise(); + promise.then((response) => { + this.cacheService.set('version', response.version); + this.cacheService.set('serviceCategories', response.categories.serviceCategories); + this.cacheService.set('resourceCategories', response.categories.resourceCategories); + this.cacheService.set('UIConfiguration', response.configuration); + }); + return promise; } loadValidationConfiguration(): Promise<ValidationConfiguration> { - let url: string = this.sdcConfig.validationConfigPath; - let promise: Promise<ValidationConfiguration> = this.http.get(url).map((res: Response) => res.json()).toPromise(); + const url: string = this.sdcConfig.validationConfigPath; + const promise: Promise<ValidationConfiguration> = this.http.get<ValidationConfiguration>(url).toPromise(); promise.then((validationData: Validations) => { ValidationConfiguration.validation = validationData; + this.cacheService.set('validation', validationData); }).catch((ex) => { console.error('Error loading validation.json configuration file, using fallback data', ex); - let fallback:Validations = { - "propertyValue": { - "max": 2500, - "min": 0 + const fallback = { + propertyValue: { + max: 2500, + min: 0 }, - - "validationPatterns": { - "string": "^[\\sa-zA-Z0-9+-]+$", - "comment": "^[\\sa-zA-Z0-9+-_\\{\\}\"]+$", - "integer": "^(([-+]?\\d+)|([-+]?0x[0-9a-fA-F]+))$" + validationPatterns: { + string: '^[\\sa-zA-Z0-9+-]+$', + stringOrEmpty: '^[\\sa-zA-Z0-9&-]*$', + comment: '^[\\sa-zA-Z0-9+-_\\{\\}"]+$', + integer: '^(([-+]?\\d+)|([-+]?0x[0-9a-fA-F]+))$' } }; - ValidationConfiguration.validation = fallback; - + this.cacheService.set('validation', fallback); }); return promise; } - loadPluginsConfiguration(): Promise<PluginsConfiguration> { - let url:string = this.api.no_proxy_root + this.api.GET_plugins_configuration; - let promise: Promise<any> = this.http.get(url).map((res: Response) => res.json()).toPromise(); + loadPluginsConfiguration = (): Promise<PluginsConfiguration> => { + const url: string = this.api.no_proxy_root + this.api.GET_plugins_configuration; + const promise: Promise<any> = this.http.get<PluginsConfiguration>(url).toPromise(); return new Promise<PluginsConfiguration>((resolve) => { promise.then((pluginsData: Plugins) => { PluginsConfiguration.plugins = pluginsData; resolve(); }).catch((ex) => { - console.error("Error loading plugins configuration from FE", ex); + console.error('Error loading plugins configuration from FE', ex); PluginsConfiguration.plugins = [] as Plugins; resolve(); @@ -19,7 +19,7 @@ */ import {Injectable, Inject} from '@angular/core'; -import {IAppConfigurtaion, ICookie} from "../../models/app-config"; +import {ICookie} from "../../models/app-config"; import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; @Injectable() @@ -20,9 +20,9 @@ import * as _ from "lodash"; import { Injectable } from '@angular/core'; -import { DataTypeModel, DataTypesMap, PropertyBEModel, PropertyFEModel, DerivedFEProperty, DerivedFEPropertyMap } from "app/models"; +import { DataTypeModel, DataTypesMap, PropertyFEModel, DerivedFEProperty} from "app/models"; import { DataTypesService } from "app/services/data-types-service"; -import { PROPERTY_DATA, PROPERTY_TYPES } from "app/utils"; +import { PROPERTY_DATA } from "app/utils"; /** This is a new service for NG2, to eventually replace app/services/data-types-service.ts * @@ -32,13 +32,16 @@ import { PROPERTY_DATA, PROPERTY_TYPES } from "app/utils"; @Injectable() export class DataTypeService { - private dataTypes: DataTypesMap; + public dataTypes: DataTypesMap; constructor(private dataTypeService: DataTypesService) { this.dataTypes = dataTypeService.getAllDataTypes(); //This should eventually be replaced by an NG2 call to the backend instead of utilizing Angular1 downgraded component. } public getDataTypeByTypeName(typeName: string): DataTypeModel { + if(!this.dataTypes){ + this.dataTypes = this.dataTypeService.getAllDataTypes(); + } if (!this.dataTypes[typeName]) console.log("MISSING Datatype: " + typeName); return this.dataTypes[typeName]; } @@ -47,6 +50,13 @@ export class DataTypeService { return this.dataTypes; } + public getConstraintsByParentTypeAndUniqueID(rootPropertyType, propertyName){ + // const property = this.dataTypes[rootPropertyType].properties.filter(property => + // property.name == propertyName); + // return property[0] && property[0].constraints ? property[0].constraints[0].validValues : null; + return null; + } + public getDerivedDataTypeProperties(dataTypeObj: DataTypeModel, propertiesArray: Array<DerivedFEProperty>, parentName: string) { //push all child properties to array @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {BasePubSub, IPubSubEvent} from 'sdc-pubsub'; +import {BasePubSub, IPubSubEvent} from "sdc-pubsub"; @Injectable() export class EventBusService extends BasePubSub { @@ -0,0 +1,48 @@ +import { Injectable } from "@angular/core"; +import { WindowRef } from "./window.service"; + +@Injectable() +export class FileUtilsService { + constructor(private windowRef: WindowRef){} + + public byteCharactersToBlob = (byteCharacters, contentType):any => { + contentType = contentType || ''; + let sliceSize = 1024; + let bytesLength = byteCharacters.length; + let slicesCount = Math.ceil(bytesLength / sliceSize); + let byteArrays = new Array(slicesCount); + + for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) { + let begin = sliceIndex * sliceSize; + let end = Math.min(begin + sliceSize, bytesLength); + + let bytes = new Array(end - begin); + for (let offset = begin, i = 0; offset < end; ++i, ++offset) { + bytes[i] = byteCharacters[offset].charCodeAt(0); + } + byteArrays[sliceIndex] = new Uint8Array(bytes); + } + return new Blob(byteArrays, {type: contentType}); + }; + + public base64toBlob = (base64Data, contentType):any => { + let byteCharacters = atob(base64Data); + return this.byteCharactersToBlob(byteCharacters, contentType); + }; + + public downloadFile = (blob, fileName):void=> { + let url = this.windowRef.nativeWindow.URL.createObjectURL(blob); + let downloadLink = document.createElement("a"); + + downloadLink.setAttribute('href', url); + downloadLink.setAttribute('download', fileName); + document.body.appendChild(downloadLink); + + var clickEvent = new MouseEvent("click", { + "view": window, + "bubbles": true, + "cancelable": true + }); + downloadLink.dispatchEvent(clickEvent); + } +}
\ No newline at end of file @@ -18,12 +18,13 @@ * ============LICENSE_END========================================================= */ -import {Injectable, Inject} from "@angular/core"; -import {Response} from '@angular/http'; -import {HttpService} from "./http.service"; -import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; -import {Observable} from "rxjs"; +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { Response } from '@angular/http'; +import { Observable } from 'rxjs'; +import { ISdcConfig, SdcConfigToken } from '../config/sdc-config.config'; +// tslint:disable-next-line:interface-name export interface IServerResponse { data: [{ [key: string]: string }]; } @@ -36,24 +37,25 @@ export class GabRequest { } } +// tslint:disable-next-line:max-classes-per-file @Injectable() export class GabService { baseUrl: string; gabUrl: string; - constructor(@Inject(SdcConfigToken) sdcConfig: ISdcConfig, private http: HttpService) { + constructor(@Inject(SdcConfigToken) sdcConfig: ISdcConfig, protected http: HttpClient) { this.baseUrl = sdcConfig.api.root; this.gabUrl = sdcConfig.api.POST_GAB_Search; } public getArtifact(artifactUniqueId: string, resourceId: string, columns: string[]): Observable<Response> { - let finalUrl: string = this.baseUrl + this.gabUrl; - let request: GabRequest = { + const finalUrl: string = this.baseUrl + this.gabUrl; + const request: GabRequest = { fields: columns, parentId: resourceId, - artifactUniqueId: artifactUniqueId + artifactUniqueId }; - return this.http.post(finalUrl, request); + return this.http.post<Response>(finalUrl, request); } }
\ No newline at end of file @@ -1,11 +1,11 @@ import {IZoneInstanceAssignment} from '../../models/graph/zones/zone-instance'; import {Injectable, Inject} from "@angular/core"; import {Observable} from "rxjs/Observable"; -import {HttpService} from "./http.service"; import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; import {GroupInstance} from '../../models/graph/zones/group-instance'; import {UiBaseObject} from "../../models/ui-models/ui-base-object"; import {IZoneService} from "../../models/graph/zones/zone"; +import { HttpClient } from '@angular/common/http'; @Injectable() export class GroupsService implements IZoneService { @@ -17,13 +17,13 @@ export class GroupsService implements IZoneService { 'SERVICE': 'services' } - constructor(private http:HttpService, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { + constructor(private http: HttpClient, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { this.baseUrl = sdcConfig.api.root; } - public createGroupInstance(componentType:string, componentUniqueId:string, groupType:string) { - return this.http.post(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[componentType.toUpperCase()] + '/' + componentUniqueId + '/groups/' + groupType, {}).map(resp => { - return resp.json(); + public createGroupInstance(componentType:string, componentUniqueId:string, groupType:string): Observable<GroupInstance>{ + return this.http.post<GroupInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[componentType.toUpperCase()] + '/' + componentUniqueId + '/groups/' + groupType, {}).map(resp => { + return new GroupInstance(resp); }); }; @@ -40,8 +40,7 @@ export class GroupsService implements IZoneService { } public updateGroupMembers(topologyTemplateType:string, topologyTemplateId:string, groupId:string, members:Array<string>):Observable<Array<string>> { - return this.http.post(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId + '/members', members) - .map(response => response.json()); + return this.http.post<Array<string>>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId + '/members', members); } public updateMembers(topologyTemplateType:string, topologyTemplateId:string, groupId:string, members:Array<UiBaseObject>):Observable<Array<string>> { @@ -50,22 +49,18 @@ export class GroupsService implements IZoneService { } public getSpecificGroup(topologyTemplateType:string, topologyTemplateId:string, groupId:string):Observable<GroupInstance> { - return this.http.get(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId) + return this.http.get<GroupInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId) .map(res => { - return new GroupInstance(res.json()); + return new GroupInstance(res); }); } public updateName(topologyTemplateType:string, topologyTemplateId:string, groupId:string, newName:string):Observable<GroupInstance> { - return this.http.put(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId, {name: newName}).map(resp => { - return resp.json(); - }); + return this.http.put<GroupInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId, {name: newName}); }; public deleteGroup(topologyTemplateType:string, topologyTemplateId:string, groupId:string) { - return this.http.delete(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId).map(resp => { - return resp.json(); - }); + return this.http.delete<GroupInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/groups/' + groupId); }; public updateZoneInstanceAssignments(topologyTemplateType:string, topologyTemplateId:string, policyId:string, members:Array<IZoneInstanceAssignment>):Observable<any> { @@ -75,4 +70,6 @@ export class GroupsService implements IZoneService { public deleteZoneInstance(topologyTemplateType:string, topologyTemplateId:string, policyId:string):Observable<any> { return this.deleteGroup(topologyTemplateType, topologyTemplateId, policyId); }; -}
\ No newline at end of file + + +} @@ -0,0 +1,74 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { Component, IApi, Resource, Service } from 'app/models'; +import { ComponentFactory } from 'app/utils/component-factory'; +import { Observable } from 'rxjs'; +import { ISdcConfig, SdcConfigToken } from '../config/sdc-config.config'; +import { SharingService } from './sharing.service'; + +// tslint:disable-next-line:interface-name +interface IComponentsArray { + services: Service[]; + resources: Resource[]; +} + +@Injectable() +export class HomeService { + private api: IApi; + private smallObjectAttributes = [ + 'uniqueId', 'name', 'componentType', 'resourceType', 'lastUpdateDate', 'lifecycleState', 'distributionStatus', + 'icon', 'version' + ]; + + constructor(private http: HttpClient, + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig, + private sharingService: SharingService, + private componentFactory: ComponentFactory) { + this.api = sdcConfig.api; + } + + public getAllComponents(smallObjects?: boolean): Observable<Component[]> { + return this.http.get<IComponentsArray>(this.api.root + this.api.GET_element) + .map((response) => { + const componentResponse: IComponentsArray = response; + let componentsList: Component[] = []; + + componentResponse.services && componentResponse.services.forEach((serviceResponse: Service) => { + serviceResponse = (smallObjects) ? _.pick(serviceResponse, this.smallObjectAttributes) : serviceResponse; + const component: Service = this.componentFactory.createService(serviceResponse); + componentsList.push(component); + this.sharingService.addUuidValue(component.uniqueId, component.uuid); + }); + + componentResponse.resources && componentResponse.resources.forEach((resourceResponse: Resource) => { + resourceResponse = (smallObjects) ? _.pick(resourceResponse, this.smallObjectAttributes) : resourceResponse; + const component: Resource = this.componentFactory.createResource(resourceResponse); + componentsList.push(component); + this.sharingService.addUuidValue(component.uniqueId, component.uuid); + }); + + componentsList = _.orderBy(componentsList, ['lastUpdateDate'], ['desc']); + + return componentsList; + }); + } +} @@ -0,0 +1,31 @@ +import { Injectable, Inject } from "@angular/core"; +import { Dictionary } from "../../utils/dictionary/dictionary"; +import { SharingService } from "../services/sharing.service"; +import { SdcConfigToken, ISdcConfig } from "../config/sdc-config.config"; + + +@Injectable() +export class HttpHelperService { + constructor( private sharingService: SharingService, + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig){} + + public getUuidValue = (url: string): string => { + let map: Dictionary<string, string> = this.sharingService.getUuidMap(); + if (map && url.indexOf(this.sdcConfig.api.root) > 0) { + map.forEach((key: string) => { + if (url.indexOf(key) !== -1) { + return this.sharingService.getUuidValue(key); + } + }); + } + return ''; + } + public static replaceUrlParams(url: string, urlParams: { [index: string]: any }): string { + return url.replace(/:(\w+)/g, (m, p1): string => urlParams[p1] || ''); + } + public static getHeaderMd5 = (object:any):string => { + let componentString:string = JSON.stringify(object); + let md5Result = md5(componentString).toLowerCase(); + return btoa(md5Result); + }; +}
\ No newline at end of file @@ -1,100 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * SDC - * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. 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. - * ============LICENSE_END========================================================= - */ - -import {Injectable, Inject} from '@angular/core'; -import {Http, XHRBackend, RequestOptions, Request, RequestOptionsArgs, Response, Headers} from '@angular/http'; -import {Observable} from 'rxjs/Observable'; -import {UUID} from 'angular2-uuid'; -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/catch'; -import 'rxjs/add/observable/throw'; -import {Dictionary} from "../../utils/dictionary/dictionary"; -import {SharingService, CookieService} from "app/services"; -import { ModalService } from "app/ng2/services/modal.service"; -import { ServerErrorResponse } from "app/models"; -import {ErrorMessageComponent} from "../components/ui/modal/error-message/error-message.component"; -import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; - -@Injectable() -export class HttpService extends Http { - - constructor(backend: XHRBackend, options: RequestOptions, private sharingService: SharingService, private cookieService: CookieService, private modalService: ModalService, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { - super(backend, options); - this._defaultOptions.withCredentials = true; - this._defaultOptions.headers.append(cookieService.getUserIdSuffix(), cookieService.getUserId()); - } - - request(request:string|Request, options?:RequestOptionsArgs):Observable<Response> { - /** - * For every request to the server, that the service id, or resource id is sent in the URL, need to pass UUID in the header. - * Check if the unique id exists in uuidMap, and if so get the UUID and add it to the header. - */ - if (typeof request === 'string') { // meaning we have to add the token to the options, not in url - if (!options) { - // make option object - options = {headers: new Headers()}; - } - - var uuidValue = this.getUuidValue(request); - if(uuidValue!= ''){ - options.headers['X-ECOMP-ServiceID'] = uuidValue; - - } - options.headers.set('X-ECOMP-RequestID', UUID.UUID()); - - } else { - // we have to add the token to the url object - var uuidValue = this.getUuidValue((<Request>request).url); - if(uuidValue!= ''){ - request.headers.set('X-ECOMP-ServiceID',uuidValue); - - } - request.headers.set('X-ECOMP-RequestID', UUID.UUID()); - } - return super.request(request, options).catch((err) => this.catchError(err)); - } - - private getUuidValue = (url: string) :string => { - let map:Dictionary<string, string> = this.sharingService.getUuidMap(); - if (map && url.indexOf(this.sdcConfig.api.root) > 0) { - map.forEach((key:string) => { - if (url.indexOf(key) !== -1) { - return this.sharingService.getUuidValue(key); - } - }); - } - return ''; - } - - private catchError = (response: Response): Observable<any> => { - - let modalInstance = this.modalService.createErrorModal("OK"); - let errorResponse: ServerErrorResponse = new ServerErrorResponse(response); - this.modalService.addDynamicContentToModal(modalInstance, ErrorMessageComponent, errorResponse); - modalInstance.instance.open(); - - return Observable.throw(response); - }; - - public static replaceUrlParams(url:string, urlParams:{[index:string]:any}):string { - return url.replace(/:(\w+)/g, (m, p1):string => urlParams[p1] || ''); - } - -} @@ -0,0 +1,34 @@ +import {Inject, Injectable} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {ISdcConfig, SdcConfigToken} from "../config/sdc-config.config"; +import {DisplayModule, Module} from "../../models/modules/base-module"; +import {Observable} from "rxjs/Observable"; +import {ServerTypeUrl} from "../../utils/constants"; + +@Injectable() +export class ModulesService { + + protected baseUrl; + + constructor(private http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { + this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; + } + + getComponentInstanceModule = (topologyTemplateType: string, topologyTemplateId: string, componentInstanceId: string, moduleId: string):Observable<DisplayModule> => { + return this.http.get<DisplayModule>(this.baseUrl + ServerTypeUrl.toServerTypeUrl(topologyTemplateType) + "/" + topologyTemplateId + "/resourceInstance/" + componentInstanceId + "/groupInstance/" + moduleId) + .map((response) => { + return new DisplayModule(response); + }) + }; + + getModuleForDisplay = (topologyTemplateType: string, topologyTemplateId: string, moduleId: string):Observable<DisplayModule> => { + return this.http.get<DisplayModule>(this.baseUrl + ServerTypeUrl.toServerTypeUrl(topologyTemplateType) + "/" + topologyTemplateId + "/groups/" + moduleId) + .map((response) => { + return new DisplayModule(response); + }) + }; + + public updateModuleMetadata = (topologyTemplateType: string, topologyTemplateId: string, module: Module):Observable<Module> => { + return this.http.put<Module>(this.baseUrl + ServerTypeUrl.toServerTypeUrl(topologyTemplateType) + "/" + topologyTemplateId + "/groups/" + module.uniqueId + "/metadata", module) + } +}
\ No newline at end of file @@ -0,0 +1,151 @@ +/** + * Created by rc2122 on 6/4/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ + +'use strict'; +import {Inject, Injectable} from "@angular/core"; +import {SdcConfigToken, ISdcConfig} from "app/ng2/config/sdc-config.config"; +import {Observable} from "rxjs/Observable"; +import { HttpClient, HttpResponse } from "@angular/common/http"; +import { ComponentFactory } from "../../utils/component-factory"; +import { DEFAULT_ICON, ComponentType } from "../../utils/constants"; +import { ICsarComponent } from "../../models/csar-component"; +import { IApi } from "../../models/app-config"; +import { CacheService } from "./cache.service"; +import { IComponentMetadata, ComponentMetadata } from "../../models/component-metadata"; +import { IMainCategory, ISubCategory } from "../../models/category"; +import { Resource } from "../../models/components/resource"; + +export interface OnboardingComponents { + listCount: number; + results: Array<ICsarComponent> +} + +@Injectable() +export class OnboardingService { + private api:IApi; + + constructor(protected http: HttpClient, + private cacheService:CacheService, + @Inject(SdcConfigToken) sdcConfig:ISdcConfig, + private componentFactory: ComponentFactory) { + this.api = sdcConfig.api; + } + + getOnboardingVSPs = (): Observable<Array<ICsarComponent>> =>{ + return this.http.get<OnboardingComponents>(this.api.GET_onboarding).map((onboardingVSPs) =>{ + return onboardingVSPs.results + }); + } + + getOnboardingComponents = ():Observable<Array<IComponentMetadata>> => { + return this.getOnboardingVSPs().map((onboardingComponents) => { + let componentsMetadataList: Array<IComponentMetadata> = new Array(); + onboardingComponents.forEach((obc:ICsarComponent) => { + let componentMetaData: ComponentMetadata = this.createFromCsarComponent(obc); + componentsMetadataList.push(componentMetaData); + }); + return componentsMetadataList; + }); + }; + + public createFromCsarComponent = (csar:ICsarComponent): ComponentMetadata => { + let newMetadata = new ComponentMetadata(); + newMetadata.name = csar.vspName; + + /** + * Onboarding CSAR contains category and sub category that are uniqueId. + * Need to find the category and sub category and extract the name from them. + * First concat all sub categories to one array. + * Then find the selected sub category and category. + * @type {any} + */ + let availableCategories = angular.copy(this.cacheService.get('resourceCategories')); + let allSubs = []; + _.each(availableCategories, (main:IMainCategory)=> { + if (main.subcategories) { + allSubs = allSubs.concat(main.subcategories); + } + }); + + let selectedCategory:IMainCategory = _.find(availableCategories, function (main:IMainCategory) { + return main.uniqueId === csar.category; + }); + + let selectedSubCategory:ISubCategory = _.find(allSubs, (sub:ISubCategory)=> { + return sub.uniqueId === csar.subCategory; + }); + + // Build the categories and sub categories array (same format as component category) + let categories:Array<IMainCategory> = new Array(); + let subcategories:Array<ISubCategory> = new Array(); + if (selectedCategory && selectedSubCategory) { + subcategories.push(selectedSubCategory); + selectedCategory.subcategories = subcategories; + categories.push(selectedCategory); + } + + // Fill the component with details from CSAR + + newMetadata.categories = categories; + newMetadata.vendorName = csar.vendorName; + newMetadata.vendorRelease = csar.vendorRelease; + newMetadata.csarUUID = csar.packageId; + newMetadata.csarPackageType = csar.packageType; + newMetadata.csarVersion = csar.version; + newMetadata.packageId = csar.packageId; + newMetadata.description = csar.description; + newMetadata.selectedCategory = selectedCategory && selectedSubCategory ? selectedCategory.name + "_#_" + selectedSubCategory.name : ''; + newMetadata.filterTerm = newMetadata.name + ' ' + newMetadata.description + ' ' + newMetadata.vendorName + ' ' + newMetadata.csarVersion; + newMetadata.resourceType = "VF"; + newMetadata.componentType = ComponentType.RESOURCE; + newMetadata.tags = []; + newMetadata.icon = DEFAULT_ICON; + newMetadata.iconSprite = "sprite-resource-icons"; + return newMetadata; + }; + + downloadOnboardingCsar = (packageId:string):Observable<HttpResponse<Blob>> => { + return this.http.get(this.api.GET_onboarding + "/" + packageId, {observe: 'response', responseType: 'blob'}); + }; + + getComponentFromCsarUuid = (csarUuid:string):Observable<ComponentMetadata> => { + return this.http.get<ComponentMetadata>(this.api.root + this.api.GET_component_from_csar_uuid.replace(':csar_uuid', csarUuid)) + .map((response: any) => { + // If the status is 400, this means that the component not found. + // I do not want to return error from server, because a popup will appear in client with the error. + // So returning success (200) with status 400. + if (response.status !== 400) { + let componentMetadata = new ComponentMetadata(); + componentMetadata = response; + return componentMetadata; + } + }); + }; + + //TODO remove when workspace page convert to angular5 + convertMetaDataToComponent(componentMetadata: ComponentMetadata) { + let newResource: Resource = <Resource>this.componentFactory.createEmptyComponent(ComponentType.RESOURCE); + newResource.setComponentMetadata(componentMetadata); + return newResource; + } +} @@ -1,40 +1,40 @@ -import { Injectable, Inject } from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Http, Response} from '@angular/http'; -import {IApi, IAppConfigurtaion, Plugin, Plugins, PluginsConfiguration} from "app/models"; +import {IApi, IAppConfigurtaion, Plugin, PluginsConfiguration} from "app/models"; import {ISdcConfig, SdcConfigToken} from "../config/sdc-config.config"; @Injectable() export class PluginsService { - private baseUrl; public configuration: IAppConfigurtaion; public api: IApi; + private baseUrl; - constructor(private http: Http, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { + constructor(private http: Http, @Inject(SdcConfigToken) private sdcConfig: ISdcConfig) { this.api = this.sdcConfig.api; this.baseUrl = this.api.root + this.sdcConfig.api.component_api_root; } public getPluginByStateUrl = (stateUrl: string) => { - let pluginKey: any = _.findKey(PluginsConfiguration.plugins, (pluginConfig: Plugin) =>{ - return pluginConfig.pluginStateUrl === stateUrl; + let pluginKey: any = _.findKey(PluginsConfiguration.plugins, (pluginConfig: Plugin) => { + return pluginConfig.pluginStateUrl === stateUrl; }); return PluginsConfiguration.plugins[pluginKey]; }; - public isPluginDisplayedInContext = (plugin: Plugin ,userRole: string, contextType: string) => { + public isPluginDisplayedInContext = (plugin: Plugin, userRole: string, contextType: string) => { return plugin.pluginDisplayOptions["context"] && - plugin.pluginDisplayOptions["context"].displayRoles.includes(userRole) && - plugin.pluginDisplayOptions["context"].displayContext.indexOf(contextType) !== -1 + plugin.pluginDisplayOptions["context"].displayRoles.includes(userRole) && + plugin.pluginDisplayOptions["context"].displayContext.indexOf(contextType) !== -1 }; public isPluginOnline = (pluginId: string): Observable<boolean> => { - let url:string = this.api.no_proxy_root + this.api.GET_plugin_online_state.replace(':pluginId', pluginId); + let url: string = this.api.no_proxy_root + this.api.GET_plugin_online_state.replace(':pluginId', pluginId); return this.http.get(url).map((res: Response) => { return res.json() }) - .catch(error => Observable.of(false)); + .catch(error => Observable.of(false)); } } @@ -20,13 +20,13 @@ import {Injectable, Inject} from "@angular/core"; import {Observable} from "rxjs/Observable"; -import {HttpService} from "./http.service"; import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; import {PolicyInstance, PolicyTargetsRequest} from '../../models/graph/zones/policy-instance'; import {IZoneInstanceAssignment} from "../../models/graph/zones/zone-instance"; import {IZoneService} from "../../models/graph/zones/zone"; import {TargetUiObject} from "../../models/ui-models/ui-target-object"; import {TargetOrMemberType} from "../../utils/constants"; +import { HttpClient } from "@angular/common/http"; @Injectable() @@ -38,14 +38,12 @@ export class PoliciesService implements IZoneService { 'SERVICE': 'services' } - constructor(private http:HttpService, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { + constructor(private http: HttpClient, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { this.baseUrl = sdcConfig.api.root; } - public createPolicyInstance(topologyTemplateType:string, topologyTemplateId:string, policyType:string) { - return this.http.post(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyType, {}).map(resp => { - return resp.json(); - }); + public createPolicyInstance(topologyTemplateType:string, topologyTemplateId:string, policyType:string): Observable<PolicyInstance> { + return this.http.post<PolicyInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyType, {}); } public addPolicyTarget(topologyTemplateType:string, topologyTemplateId:string, policy:PolicyInstance, targetId:string, targetType:TargetOrMemberType) { @@ -76,8 +74,8 @@ export class PoliciesService implements IZoneService { } public updatePolicyTargets(topologyTemplateType:string, topologyTemplateId:string, policyId:string, targets:PolicyTargetsRequest): Observable<PolicyInstance> { - return this.http.post(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId + '/targets', targets.requestItems) - .map(response => new PolicyInstance(response.json())); + return this.http.post<PolicyInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId + '/targets', targets.requestItems) + .map(response => new PolicyInstance(response)); } public updateTargets(topologyTemplateType:string, topologyTemplateId:string, policyId:string, targets:Array<TargetUiObject>):Observable<PolicyInstance> { @@ -94,22 +92,18 @@ export class PoliciesService implements IZoneService { } public getSpecificPolicy(topologyTemplateType:string, topologyTemplateId:string, policyId:string):Observable<PolicyInstance> { - return this.http.get(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId) + return this.http.get<PolicyInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId) .map(res => { - return new PolicyInstance(res.json()); + return new PolicyInstance(res); }); } public updateName(topologyTemplateType:string, topologyTemplateId:string, policyId:string, newName:string):Observable<any> { - return this.http.put(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId, {name: newName}).map(res => { - return res.json(); - }); + return this.http.put<PolicyInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId, {name: newName}); }; public deletePolicy(topologyTemplateType:string, topologyTemplateId:string, policyId:string) { - return this.http.delete(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId).map(resp => { - return resp.json(); - }); + return this.http.delete<PolicyInstance>(this.baseUrl + '/v1/catalog/' + this.mapApiDirections[topologyTemplateType.toUpperCase()] + '/' + topologyTemplateId + '/policies/' + policyId); }; public updateZoneInstanceAssignments(topologyTemplateType:string, topologyTemplateId:string, policyId:string, targets:Array<IZoneInstanceAssignment>):Observable<PolicyInstance>{ @@ -22,12 +22,15 @@ * Created by ob0695 on 4/18/2017. */ -import { ArtifactGroupModel, PropertyModel, PropertiesGroup, InputsGroup, AttributeModel, AttributesGroup, ComponentInstance, OperationModel, - InputBEModel, Module, ComponentMetadata, RelationshipModel, RequirementsGroup, CapabilitiesGroup, InterfaceModel} from "app/models"; +import { ArtifactGroupModel, PropertyModel, PropertiesGroup, AttributeModel, AttributesGroup, ComponentInstance, OperationModel, + InputBEModel, Module, ComponentMetadata, RelationshipModel, RequirementsGroup, CapabilitiesGroup} from "app/models"; import {CommonUtils} from "app/utils"; import {Serializable} from "../utils/serializable"; +import {PropertyBEModel} from "../../../models/properties-inputs/property-be-model"; import { PolicyInstance } from "app/models/graph/zones/policy-instance"; import { GroupInstance } from "../../../models/graph/zones/group-instance"; +import { InputsGroup } from "../../../models/inputs"; +import { InterfaceModel } from "../../../models/operation"; export class ComponentGenericResponse implements Serializable<ComponentGenericResponse> { @@ -40,7 +43,7 @@ export class ComponentGenericResponse implements Serializable<ComponentGenericR public componentInstancesAttributes:AttributesGroup; public componentInstancesRelations:Array<RelationshipModel>; public componentInstances:Array<ComponentInstance>; - public componentInstancesInterfaces:Map<string,Array<InterfaceModel>>; + public componentInstancesInterfaces: Map<string, Array<InterfaceModel>>; public inputs:Array<InputBEModel>; public capabilities:CapabilitiesGroup; public requirements:RequirementsGroup; @@ -75,7 +78,7 @@ export class ComponentGenericResponse implements Serializable<ComponentGenericR if(response.deploymentArtifacts) { this.deploymentArtifacts = new ArtifactGroupModel(response.deploymentArtifacts); } - if(response.inputs) { + if(response.inputs) { this.inputs = CommonUtils.initInputs(response.inputs); } if(response.attributes) { @@ -97,10 +100,9 @@ export class ComponentGenericResponse implements Serializable<ComponentGenericR this.toscaArtifacts = new ArtifactGroupModel(response.toscaArtifacts); } if(response.interfaces) { - this.interfaces = CommonUtils.initInterfaces(response.interfaces); this.interfaceOperations = CommonUtils.initInterfaceOperations(response.interfaces); } - if(response.componentInstancesInterfaces) { + if (response.componentInstancesInterfaces) { this.componentInstancesInterfaces = new Map(); for (let resourceId in response.componentInstancesInterfaces) { this.componentInstancesInterfaces[resourceId] = CommonUtils.initInterfaces(response.componentInstancesInterfaces[resourceId]); @@ -2,12 +2,15 @@ import * as _ from "lodash"; import {Serializable} from "../utils/serializable"; import {ComponentGenericResponse} from "./component-generic-response"; import {ForwardingPath} from "../../../models/forwarding-path"; +import {ArtifactGroupModel} from "../../../models/artifacts"; export class ServiceGenericResponse extends ComponentGenericResponse implements Serializable<ServiceGenericResponse> { public forwardingPaths: { [key:string]:ForwardingPath } = {}; + public serviceApiArtifacts: ArtifactGroupModel; deserialize (response): ServiceGenericResponse { super.deserialize(response); + this.serviceApiArtifacts = new ArtifactGroupModel(response.serviceApiArtifacts); if(response.forwardingPaths) { _.forEach(response.forwardingPaths, (pathResponse, id) => { let pathId = id; @@ -18,22 +18,25 @@ * ============LICENSE_END========================================================= */ -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; +import {Injectable} from "@angular/core"; +import {Dictionary} from "app/utils"; -@Component({ - selector: 'policy-information-tab', - templateUrl: './policy-information-tab.component.html', - styleUrls: ['./../base/base-tab.component.less'] -}) -export class PolicyInformationTabComponent { - - @Input() policy:PolicyInstance; - @Input() isViewOnly: boolean; +@Injectable() +export class SharingService { + private uuidMap:Dictionary<string, string> = new Dictionary<string,string>(); - constructor(private translateService:TranslateService) { + constructor() { } + public getUuidValue(uniqueId:string): string { + return this.uuidMap.getValue(uniqueId); + } + + public addUuidValue(uniqueId:string, uuid:string): void { + this.uuidMap.setValue(uniqueId, uuid); + } + + public getUuidMap() :Dictionary<string, string> { + return this.uuidMap; + } } @@ -14,12 +14,19 @@ * permissions and limitations under the License. */ -import {Injectable, Inject} from '@angular/core'; -import {Observable} from 'rxjs/Observable'; -import {HttpService} from './http.service'; -import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; -import {CapabilityTypesMap, NodeTypesMap, RelationshipTypesMap} from "app/models"; -import {Response} from '@angular/http'; +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { Response } from '@angular/http'; +import { + CapabilityTypeModel, + CapabilityTypesMap, + IComponentsArray, + NodeTypesMap, + RelationshipTypesMap +} from 'app/models'; +import { Observable } from 'rxjs/Observable'; +import { ISdcConfig, SdcConfigToken } from '../config/sdc-config.config'; +import 'rxjs/add/operator/toPromise'; declare var angular: angular.IAngularStatic; @@ -28,28 +35,20 @@ export class ToscaTypesServiceNg2 { protected baseUrl; - constructor(protected http: HttpService, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; } - fetchRelationshipTypes(): Observable<RelationshipTypesMap> { - return this.http.get(this.baseUrl + 'relationshipTypes') - .map((res: Response) => { - return res.json(); - }); + async fetchRelationshipTypes(): Promise<RelationshipTypesMap> { + return this.http.get<RelationshipTypesMap>(this.baseUrl + 'relationshipTypes').toPromise(); } - fetchNodeTypes(): Observable<NodeTypesMap> { - return this.http.get(this.baseUrl + 'nodeTypes') - .map((res: Response) => { - return res.json(); - }); + async fetchNodeTypes(): Promise<NodeTypesMap> { + return this.http.get<NodeTypesMap>(this.baseUrl + 'nodeTypes').toPromise(); } - fetchCapabilityTypes(): Observable<CapabilityTypesMap> { - return this.http.get(this.baseUrl + 'capabilityTypes') - .map((res: Response) => { - return res.json(); - }); + async fetchCapabilityTypes(): Promise<CapabilityTypesMap>{ + return this.http.get<CapabilityTypesMap>(this.baseUrl + 'capabilityTypes').toPromise(); } } + @@ -19,80 +19,34 @@ */ import { Injectable, Inject } from "@angular/core"; -import { Headers } from "@angular/http"; import { Observable } from "rxjs/Observable"; -import { HttpService } from "./http.service"; -import { Cookie2Service } from "./cookie.service"; import { IUserProperties } from "../../models/user"; -import {ICookie} from "../../models/app-config"; import {SdcConfigToken, ISdcConfig} from "../config/sdc-config.config"; - +import { HttpClient } from "@angular/common/http"; +import { HttpHelperService } from "./http-hepler.service"; +/** + * User Service provides CRUD for Users. See authentication service for authentication/login. + */ @Injectable() -export class UserService { +export class UserService { private url:string; - private authorizeUrl:string; - private _loggedinUser:IUserProperties; - - constructor(private httpService:HttpService, - private cookieService:Cookie2Service, + constructor(private http: HttpClient, @Inject(SdcConfigToken) private sdcConfig:ISdcConfig) { this.url = this.sdcConfig.api.root + this.sdcConfig.api.GET_user; - this.authorizeUrl = this.sdcConfig.api.root + this.sdcConfig.api.GET_user_authorize; } - public authorize() :Observable<IUserProperties> { - let cookie:ICookie = this.sdcConfig.cookie; - let authorizeHeaders:Headers = new Headers(); - authorizeHeaders.set(cookie.userFirstName, this.cookieService.getFirstName()); - authorizeHeaders.set(cookie.userLastName, this.cookieService.getLastName()); - authorizeHeaders.set(cookie.userEmail, this.cookieService.getEmail()); - authorizeHeaders.set(cookie.userIdSuffix, this.cookieService.getUserId()); - - return this.httpService.get( - this.authorizeUrl, - { headers: authorizeHeaders } - ).map(resp => resp.json()); - } public getAllUsers() :Observable<IUserProperties[]> { - return this.httpService.get( + return this.http.get( this.sdcConfig.api.root + this.sdcConfig.api.GET_all_users - ).map(resp => resp.json()); + ).map(resp => <IUserProperties[]> resp) ; } public getUser(userId:string) :Observable<IUserProperties> { - return this.httpService.get( - HttpService.replaceUrlParams(this.url, { id: userId }) - ).map(resp => resp.json()); - } - - public createUser(userData:{[index:string]: any}) :Observable<IUserProperties> { - return this.httpService.post( - this.sdcConfig.api.root + this.sdcConfig.api.POST_create_user, - userData - ).map(resp => resp.json()); + return this.http.get( + HttpHelperService.replaceUrlParams(this.url, { id: userId }) + ).map(resp => <IUserProperties> resp); } - - public deleteUser(userId:string) :Observable<IUserProperties> { - return this.httpService.delete( - HttpService.replaceUrlParams(this.sdcConfig.api.root + this.sdcConfig.api.DELETE_delete_user, { id: userId }) - ).map(resp => resp.json()); - } - - public editUserRole(userId:string, role:string) :Observable<IUserProperties> { - return this.httpService.post( - HttpService.replaceUrlParams(this.sdcConfig.api.root + this.sdcConfig.api.POST_edit_user_role, { id: userId }), - { role: role } - ).map(resp => resp.json()); - } - - public getLoggedinUser():IUserProperties { - return this._loggedinUser; - } - - public setLoggedinUser(loggedinUser:IUserProperties) { - this._loggedinUser = loggedinUser; - }; } @@ -1,14 +1,13 @@ import { Injectable, Inject } from "@angular/core"; -import { Response } from "@angular/http"; import { Observable } from "rxjs/Observable"; -import { HttpService } from "./http.service"; import { SdcConfigToken, ISdcConfig } from "../config/sdc-config.config"; +import { HttpClient } from "@angular/common/http"; import { Component, OperationModel } from "app/models"; interface WorkflowOutputParameter { - name: string, - type: string, - mandatory: boolean + name: string; + type: string; + mandatory: boolean; } interface WorkflowInputParameter extends WorkflowOutputParameter { @@ -25,40 +24,31 @@ export class WorkflowServiceNg2 { WF_STATE_ARCHIVED = 'ARCHIVED'; VERSION_STATE_CERTIFIED = 'CERTIFIED'; - constructor(private http: HttpService, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { + constructor(private http: HttpClient, @Inject(SdcConfigToken) sdcConfig:ISdcConfig) { this.baseUrl = sdcConfig.api.workflow_root; this.catalogBaseUrl = sdcConfig.api.POST_workflow_artifact; } public associateWorkflowArtifact(component: Component, operation: OperationModel): Observable<any> { - return this.http.post(this.baseUrl + '/workflows/' + operation.workflowId + '/versions/' + operation.workflowVersionId + '/artifact-deliveries', { - endpoint: this.catalogBaseUrl + '/' + component.getTypeUrl() + component.uuid + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId + '/artifacts/' + operation.implementation.artifactUUID, - method: 'POST' - }) - .map((res:Response) => { - return res.json(); + return this.http.post<any>(this.baseUrl + '/workflows/' + operation.workflowId + '/versions/' + operation.workflowVersionId + '/artifact-deliveries', { + endpoint: this.catalogBaseUrl + '/' + component.getTypeUrl() + component.uuid + '/interfaces/' + operation.interfaceId + '/operations/' + + operation.uniqueId + '/artifacts/' + operation.implementation.artifactUUID, method: 'POST' }); } public getWorkflows(filterCertified: boolean = true): Observable<any> { - return this.http.get(this.baseUrl + '/workflows' + (filterCertified ? '?versionState=' + this.VERSION_STATE_CERTIFIED : '')) - .map((res:Response) => { - return res.json().items; - }); + return this.http.get<any>(this.baseUrl + '/workflows' + (filterCertified ? '?versionState=' + this.VERSION_STATE_CERTIFIED : '')); } public getWorkflowVersions(workflowId: string, filterCertified: boolean = true): Observable<any> { - return this.http.get(this.baseUrl + '/workflows/' + workflowId + '/versions' + (filterCertified ? '?state=' + this.VERSION_STATE_CERTIFIED : '')) - .map((res:Response) => { - return _.map(res.json().items, version => version); + return this.http.get<any>(this.baseUrl + '/workflows/' + workflowId + '/versions' + (filterCertified ? '?state=' + this.VERSION_STATE_CERTIFIED : '')) + .map((res) => { + return res.items; }); } public updateWorkflowVersion(workflowId: string, versionId: string, payload: any): Observable<any> { - return this.http.put(this.baseUrl + '/workflows/' + workflowId + '/versions/' + versionId, payload) - .map((res:Response) => { - return res.json(); - }); + return this.http.put<any>(this.baseUrl + '/workflows/' + workflowId + '/versions/' + versionId, payload); } } @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="multiline-ellipsis-container" [ngClass]="className" [ngStyle]="stylesContainer" #multilineEllipsisContainer> <div class="multiline-ellipsis-content" [ngStyle]="stylesContent" #multilineEllipsisContent> <ng-content></ng-content> @@ -1,4 +1,4 @@ -import {Component, OnChanges, AfterViewChecked, ViewChild, ElementRef, Input, Output, SimpleChanges, EventEmitter} from "@angular/core"; +import {Component, OnChanges, AfterContentInit, ViewChild, ElementRef, Input, Output, SimpleChanges, EventEmitter} from "@angular/core"; import {WindowRef} from "../../services/window.service"; @Component({ @@ -6,7 +6,7 @@ import {WindowRef} from "../../services/window.service"; templateUrl: 'multiline-ellipsis.component.html', styleUrls: ['multiline-ellipsis.component.less'] }) -export class MultilineEllipsisComponent implements OnChanges, AfterViewChecked { +export class MultilineEllipsisComponent implements OnChanges, AfterContentInit { @Input() public lines: number; @Input() public lineHeight: string; @@ -30,7 +30,7 @@ export class MultilineEllipsisComponent implements OnChanges, AfterViewChecked { this.prepareStyles() } - public ngAfterViewChecked() { + public ngAfterContentInit() { const hasEllipsis = (this.elmContainer.nativeElement.offsetHeight < this.elmContent.nativeElement.offsetHeight); if (hasEllipsis !== this.hasEllipsis) { this.hasEllipsis = hasEllipsis; @@ -19,38 +19,32 @@ */ import { Pipe, PipeTransform } from '@angular/core'; -import { TranslateService, ITranslateArgs } from "./translate.service"; +import { ITranslateArgs, TranslateService } from './translate.service'; +// tslint:disable-next-line:interface-name +interface ITranslateParams { + phrase: string; + args: ITranslateArgs; + language: string; +} @Pipe({ name: 'translate', pure: false }) export class TranslatePipe implements PipeTransform { - private translated:string; - private lastParams: { - phrase: string; - args: {[index: string]: any}; - language: string; - } = { + private translated: string; + private lastParams: ITranslateParams = { phrase: undefined, args: undefined, language: undefined }; - constructor(private translateService:TranslateService) { - } - - private shouldUpdate(curParams:{[index:string]: any}) : boolean { - return ( - curParams.language !== this.lastParams.language || - curParams.args !== this.lastParams.args || - curParams.phrase !== this.lastParams.phrase - ); + constructor(private translateService: TranslateService) { } - public transform(phrase:string, args:ITranslateArgs, language:string=this.translateService.activeLanguage) : string { - const curParams = { phrase, args, language }; + public transform(phrase: string, args: ITranslateArgs, language: string = this.translateService.activeLanguage): string { + const curParams: ITranslateParams = { phrase, args, language }; if (this.shouldUpdate(curParams)) { this.lastParams = curParams; this.translated = this.translateService.translate(phrase, args, language); @@ -58,4 +52,12 @@ export class TranslatePipe implements PipeTransform { return this.translated; } + + private shouldUpdate(curParams: ITranslateParams): boolean { + return ( + curParams.language !== this.lastParams.language || + curParams.args !== this.lastParams.args || + curParams.phrase !== this.lastParams.phrase + ); + } } @@ -18,9 +18,9 @@ * ============LICENSE_END========================================================= */ -import { OpaqueToken } from "@angular/core"; +import { InjectionToken } from "@angular/core"; -export const TranslateServiceConfigToken = new OpaqueToken('TranslateServiceConfigToken'); +export const TranslateServiceConfigToken = new InjectionToken('TranslateServiceConfigToken'); export interface ITranslateServiceConfig { filePrefix:string; @@ -19,9 +19,9 @@ */ import { Injectable, Inject } from "@angular/core"; -import { Response, Http } from "@angular/http"; -import { Observable, Observer, ConnectableObservable, Subscription } from "rxjs"; import { ITranslateServiceConfig, TranslateServiceConfigToken } from "./translate.service.config"; +import { Observer, Subscription, Observable, ConnectableObservable } from 'rxjs/Rx'; +import { HttpClient } from "@angular/common/http"; export { ITranslateServiceConfig, TranslateServiceConfigToken }; @@ -145,7 +145,7 @@ export class TranslateService { private _cacheLanguagesJsons:{[index:string]:ITranslateLanguageJson} = {}; private _cacheLanguagesLoaders:{[index:string]:Observable<ITranslateLanguageJson>} = {}; - constructor(@Inject(TranslateServiceConfigToken) private config:ITranslateServiceConfig, private http:Http) { + constructor(@Inject(TranslateServiceConfigToken) private config:ITranslateServiceConfig, private http: HttpClient) { this.initLanguageObservable(); this.loadAndActivateLanguage(this.config.defaultLanguage); } @@ -176,8 +176,7 @@ export class TranslateService { if (!(language in this._cacheLanguagesLoaders)) { const filePath = `${this.config.filePrefix}${language}${this.config.fileSuffix}`; - this._cacheLanguagesLoaders[language] = this.http.get(filePath) - .map<Response, ITranslateLanguageJson>(resp => resp.json()) + this._cacheLanguagesLoaders[language] = this.http.get<ITranslateLanguageJson>(filePath) .catch(() => Observable.throw(`Failed to load language file for "${language}"`)) .publish(); (<ConnectableObservable<ITranslateLanguageJson>>this._cacheLanguagesLoaders[language]).connect(); @@ -204,12 +203,12 @@ export class TranslateService { return false; } - public loadAndActivateLanguage(language:string) : Observable<ITranslateLanguageJson> { + public loadAndActivateLanguage(language:string) : void { + const loadLanguageObservable = this.loadLanguageJsonFile(language, false); loadLanguageObservable.subscribe(() => { this.activateLanguage(language); }, () => {}); - return loadLanguageObservable; } public translate(phraseKey:string, args:ITranslateArgs={}, language:string=this._activeLanguage) : string { @@ -0,0 +1,25 @@ +/** + * Created by ob0695 + */ +import {ArtifactModel} from "../../../models/artifacts"; + +export class GetArtifactsByTypeAction { + static readonly type = '[ARTIFACTS] GetArtifactsByType'; + + constructor(public payload: {componentType:string, componentId:string, artifactType: string}) { + } +} + +export class CreateOrUpdateArtifactAction { + static readonly type = '[ARTIFACTS] CreateOrUpdateArtifactAction'; + + constructor(public payload: {componentType:string, componentId:string, artifact:ArtifactModel}) { + } +} + +export class DeleteArtifactAction { + static readonly type = '[ARTIFACTS] DeleteArtifactAction'; + + constructor(public payload: {componentType:string, componentId:string, artifact: ArtifactModel}) { + } +} @@ -0,0 +1,32 @@ +/** + * Created by ob0695 + */ +import {ArtifactModel} from "../../../models/artifacts"; + +export class GetInstanceArtifactsByTypeAction { + static readonly type = '[INSTANCE_ARTIFACTS] GetInstanceArtifactsByTypeAction'; + + constructor(public payload: { componentType: string, componentId: string, artifactType: string, instanceId: string }) { + } +} + +export class CreateInstanceArtifactAction { + static readonly type = '[INSTANCE_ARTIFACTS] CreateInstanceArtifactAction'; + + constructor(public payload: { componentType: string, componentId: string, instanceId: string, artifact: ArtifactModel }) { + } +} + +export class UpdateInstanceArtifactAction { + static readonly type = '[INSTANCE_ARTIFACTS] UpdateInstanceArtifactAction'; + + constructor(public payload: { componentType: string, componentId: string, instanceId: string, artifact: ArtifactModel }) { + } +} + +export class DeleteInstanceArtifactAction { + static readonly type = '[INSTANCE_ARTIFACTS] DeleteInstanceArtifactAction'; + + constructor(public payload: { componentType: string, componentId: string, instanceId: string, artifact: ArtifactModel }) { + } +} @@ -0,0 +1,17 @@ +/** + * Created by ob0695 on 7/17/2018. + */ + +export class UpdateIsViewOnly { + static readonly type = '[WORKSPACE] UpdateIsViewOnly'; + + constructor(public isViewOnly:boolean) { + } +} + +export class UpdateIsDesigner { + static readonly type = '[WORKSPACE] UpdateIsDesigner'; + + constructor(public isDesigner:boolean) { + } +} @@ -0,0 +1,62 @@ +import { Store } from '@ngxs/store'; +import { Observable } from 'rxjs/Rx'; +import { Mock } from 'ts-mockery'; +import { ArtifactModel } from '../../../models/artifacts'; +import { ArtifactGroupType } from '../../../utils/constants'; +import { ComponentInstanceServiceNg2 } from '../../services/component-instance-services/component-instance.service'; +import { GetInstanceArtifactsByTypeAction, UpdateInstanceArtifactAction } from '../actions/instance-artifacts.actions'; +import { InstanceArtifactsState } from './instance-artifacts.state'; + +describe('Test Artifact State', () => { + + const heat1 = Mock.of<ArtifactModel>({ + uniqueId: '1', artifactName: 'heat1', timeout: 0, artifactDisplayName: 'heat1', artifactGroupType: ArtifactGroupType.DEPLOYMENT + }); + + const heat1env = Mock.of<ArtifactModel>({ + uniqueId: '2', artifactName: 'heat1env', timeout: 0, generatedFromId: '1', artifactDisplayName: 'heat1env', artifactGroupType: ArtifactGroupType.DEPLOYMENT + }); + + const storeMock = Mock.of<Store>( { dispatch : jest.fn() }); + + const artifacts = [ + heat1, + heat1env + ]; + + /** + * NGXS Store state before we run the update + */ + const ngxsState = { + deploymentArtifacts : artifacts + }; + + /** + * The ENV artifact that we wish to update + */ + const updatedArtifact = Mock.of<ArtifactModel>({ + uniqueId: '2', artifactName: 'heat1env', timeout: 33, generatedFromId: '1', artifactDisplayName: 'heat1env-UPDATE', artifactGroupType: ArtifactGroupType.DEPLOYMENT + }); + + const componentInstanceServiceMock: ComponentInstanceServiceNg2 = Mock.of<ComponentInstanceServiceNg2>({ + updateInstanceArtifact: jest.fn().mockImplementation(() => Observable.of(updatedArtifact)), + getComponentInstanceArtifactsByGroupType: jest.fn().mockImplementation(() => Observable.of([heat1, updatedArtifact])) + }); + + const actionMock: UpdateInstanceArtifactAction = Mock.of<UpdateInstanceArtifactAction>({ + payload: { + componentType: '', + componentId: '', + instanceId: '', + artifact: updatedArtifact + } + }); + + it('Test that HEAT timeout is updated', () => { + const state: InstanceArtifactsState = new InstanceArtifactsState(storeMock, componentInstanceServiceMock); + const context = { getState: jest.fn().mockImplementation(() => ngxsState), patchState: jest.fn(), setState: jest.fn(), dispatch: jest.fn() }; + state.updateArtifact(context, actionMock ).subscribe( (v) => console.log('OK')); + expect(storeMock.dispatch).toBeCalled(); + }); + +}); @@ -0,0 +1,140 @@ +/** + * Created by ob0695 on 7/17/2018. + */ +import { Action, Selector, State, StateContext } from '@ngxs/store'; +import * as _ from 'lodash'; +import { tap } from 'rxjs/operators'; +import { ArtifactModel } from '../../../models/artifacts'; +import { ArtifactGroupType } from '../../../utils/constants'; +import { TopologyTemplateService } from '../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../services/responses/component-generic-response'; +import { ServiceGenericResponse } from '../../services/responses/service-generic-response'; +import { CreateOrUpdateArtifactAction, DeleteArtifactAction, GetArtifactsByTypeAction } from '../actions/artifacts.action'; + +export interface ArtifactsStateModel { + artifacts: ArtifactModel[]; + deploymentArtifacts: ArtifactModel[]; + toscaArtifacts: ArtifactModel[]; + serviceApiArtifacts: ArtifactModel[]; +} + +@State<ArtifactsStateModel>({ + name: 'artifacts', + defaults: { + artifacts: [], + deploymentArtifacts: [], + toscaArtifacts: [], + serviceApiArtifacts: [] + } +}) + +export class ArtifactsState { + + constructor(protected topologyTemplateService: TopologyTemplateService) { + } + + @Selector() + static getEnvArtifact(state: ArtifactsStateModel, heatEnvArtifact: ArtifactModel) { + return (heatEnvArtifact: ArtifactModel) => { + _.find(state.deploymentArtifacts, (artifact)=> { + return artifact.generatedFromId === heatEnvArtifact.uniqueId + }) + }; + } + + @Selector() + static getArtifactsByType(state: ArtifactsStateModel, type: string) { + return (type: string) => { + switch (type) { + case ArtifactGroupType.TOSCA: + return state.toscaArtifacts; + case ArtifactGroupType.INFORMATION: + return state.artifacts; + case ArtifactGroupType.DEPLOYMENT: + return state.deploymentArtifacts; + case ArtifactGroupType.SERVICE_API: + return state.serviceApiArtifacts; + } + }; + } + + private updateArtifactState = (artifactsState: ArtifactModel[], artifactToUpdate: ArtifactModel, updatedArtifact: ArtifactModel) => { + if (!artifactToUpdate.uniqueId) { // Create Artifact + return [...artifactsState, updatedArtifact] + } else { // Update Artifact + let artifactToUpdateIndex = _.findIndex(artifactsState, (artifact) => { + return artifact.uniqueId === artifactToUpdate.uniqueId + }) + let artifacts = Array.from(artifactsState); + artifacts[artifactToUpdateIndex] = updatedArtifact; + return [...artifacts]; + } + } + + @Action(GetArtifactsByTypeAction) + getArtifactsByType({getState, patchState}: StateContext<ArtifactsStateModel>, action: GetArtifactsByTypeAction) { + const state = getState(); + return this.topologyTemplateService.getArtifactsByType(action.payload.componentType, action.payload.componentId, action.payload.artifactType) + .pipe(tap((resp: ComponentGenericResponse) => { + switch (action.payload.artifactType) { + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: <ArtifactModel[]>_.values(resp.artifacts) + }); + + case ArtifactGroupType.DEPLOYMENT: + patchState({ + deploymentArtifacts: <ArtifactModel[]>_.values(resp.deploymentArtifacts) + }); + + case ArtifactGroupType.TOSCA: + patchState({ + toscaArtifacts: <ArtifactModel[]>_.values(resp.toscaArtifacts) + }); + + case ArtifactGroupType.SERVICE_API: + patchState({ + serviceApiArtifacts: <ArtifactModel[]>_.values((<ServiceGenericResponse>resp).serviceApiArtifacts) + }); + } + })); + } + + @Action(CreateOrUpdateArtifactAction) + createOrUpdateArtifact({getState, patchState}: StateContext<ArtifactsStateModel>, action: CreateOrUpdateArtifactAction) { + const state = getState(); + return this.topologyTemplateService.addOrUpdateArtifact(action.payload.componentType, action.payload.componentId, action.payload.artifact) + .pipe(tap((resp: ArtifactModel) => { + + switch (resp.artifactGroupType) { + case ArtifactGroupType.DEPLOYMENT: + patchState({ + deploymentArtifacts: this.updateArtifactState(state.deploymentArtifacts, action.payload.artifact, resp) + }); + + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: this.updateArtifactState(state.artifacts, action.payload.artifact, resp) + }); + } + })); + } + + @Action(DeleteArtifactAction) + deleteArtifact({getState, patchState}: StateContext<ArtifactsStateModel>, action: DeleteArtifactAction) { + const state = getState(); + return this.topologyTemplateService.deleteArtifact(action.payload.componentId, action.payload.componentType, action.payload.artifact.uniqueId, action.payload.artifact.artifactLabel) + .pipe(tap((resp: ArtifactModel) => { + switch (resp.artifactGroupType) { + case ArtifactGroupType.DEPLOYMENT: + patchState({ + deploymentArtifacts: state.deploymentArtifacts.filter(({uniqueId}) => uniqueId !== action.payload.artifact.uniqueId) + }); + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: state.artifacts.filter(({uniqueId}) => uniqueId !== action.payload.artifact.uniqueId) + }); + } + })); + } +}
\ No newline at end of file @@ -0,0 +1,145 @@ +/** + * Created by ob0695 on 7/17/2018. + */ +import { Action, Selector, State, StateContext, Store } from '@ngxs/store'; +import * as _ from 'lodash'; +import { tap } from 'rxjs/operators'; +import { ArtifactModel } from '../../../models/artifacts'; +import { ArtifactGroupType } from '../../../utils/constants'; +import { ComponentInstanceServiceNg2 } from '../../services/component-instance-services/component-instance.service'; +import { ComponentGenericResponse } from '../../services/responses/component-generic-response'; +import { + CreateInstanceArtifactAction, + DeleteInstanceArtifactAction, + GetInstanceArtifactsByTypeAction, + UpdateInstanceArtifactAction +} from '../actions/instance-artifacts.actions'; +import { ArtifactsStateModel } from './artifacts.state'; + +export interface InstanceArtifactsStateModel { + artifacts: ArtifactModel[]; + deploymentArtifacts: ArtifactModel[]; +} + +@State<InstanceArtifactsStateModel>({ + name: 'instance_artifacts', + defaults: { + artifacts: [], + deploymentArtifacts: [] + } +}) +export class InstanceArtifactsState { + + constructor(private store: Store, protected componentInstanceService: ComponentInstanceServiceNg2) { + } + + @Selector() + static getArtifactsByType(state: InstanceArtifactsStateModel) { + return (type: string) => { + switch (type) { + case ArtifactGroupType.INFORMATION: + return state.artifacts; + case ArtifactGroupType.DEPLOYMENT: + return state.deploymentArtifacts; + } + }; + } + + @Action(GetInstanceArtifactsByTypeAction) + getInstanceArtifactsByType({getState, patchState}: StateContext<InstanceArtifactsStateModel>, action: GetInstanceArtifactsByTypeAction) { + const state = getState(); + return this.componentInstanceService.getComponentInstanceArtifactsByGroupType(action.payload.componentType, action.payload.componentId, action.payload.instanceId, action.payload.artifactType) + .pipe(tap((resp: ComponentGenericResponse) => { + switch (action.payload.artifactType) { + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: _.values(resp) as ArtifactModel[] + }); + break; + case ArtifactGroupType.DEPLOYMENT: + patchState({ + deploymentArtifacts: _.values(resp) as ArtifactModel[] + }); + break; + } + })); + } + + @Action(CreateInstanceArtifactAction) + createArtifact({getState, patchState}: StateContext<ArtifactsStateModel>, action: CreateInstanceArtifactAction) { + const state = getState(); + return this.componentInstanceService.addInstanceArtifact(action.payload.componentType, action.payload.componentId, action.payload.instanceId, action.payload.artifact) + .pipe(tap((resp: ArtifactModel) => { + switch (resp.artifactGroupType) { + case ArtifactGroupType.DEPLOYMENT: + patchState({ + deploymentArtifacts: [...state.deploymentArtifacts, resp] + }); + break; + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: [...state.artifacts, resp] + }); + break; + } + })); + } + + @Action(UpdateInstanceArtifactAction) + updateArtifact({getState, patchState}: StateContext<ArtifactsStateModel>, action: UpdateInstanceArtifactAction) { + const state = getState(); + return this.componentInstanceService.updateInstanceArtifact(action.payload.componentType, action.payload.componentId, action.payload.instanceId, action.payload.artifact) + .pipe(tap((resp: ArtifactModel) => { + switch (resp.artifactGroupType) { + case ArtifactGroupType.DEPLOYMENT: + // We cannot simply update the updated artifact state because updating a deployment ENV file may cause an update to his parent HEAT + // file. + // Just dispatch an action to refresh the deployment artifacts list + this.store.dispatch(new GetInstanceArtifactsByTypeAction(({ + componentType: action.payload.componentType, + componentId: action.payload.componentId, + instanceId: action.payload.instanceId, + artifactType: ArtifactGroupType.DEPLOYMENT + }))); + break; + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: this.updateInstanceArtifactState(state.artifacts, action.payload.artifact, resp) + }); + break; + } + })); + } + + @Action(DeleteInstanceArtifactAction) + deleteInstanceArtifact({getState, patchState}: StateContext<ArtifactsStateModel>, action: DeleteInstanceArtifactAction) { + const state = getState(); + return this.componentInstanceService. + deleteInstanceArtifact(action.payload.componentId, action.payload.componentType, action.payload.instanceId, action.payload.artifact.uniqueId, action.payload.artifact.artifactLabel) + .pipe(tap((resp: ArtifactModel) => { + switch (resp.artifactGroupType) { + case ArtifactGroupType.DEPLOYMENT: + patchState({ + deploymentArtifacts: state.deploymentArtifacts.filter(({uniqueId}) => uniqueId !== action.payload.artifact.uniqueId) + }); + break; + case ArtifactGroupType.INFORMATION: + patchState({ + artifacts: state.artifacts.filter(({uniqueId}) => uniqueId !== action.payload.artifact.uniqueId) + }); + break; + } + })); + } + + private updateInstanceArtifactState = (artifactsState: ArtifactModel[], artifactToUpdate: ArtifactModel, updatedArtifact: ArtifactModel) => { + const artifactToUpdateIndex = _.findIndex(artifactsState, (artifact) => { + return artifact.uniqueId === artifactToUpdate.uniqueId; + }); + const artifacts = Array.from(artifactsState); + artifacts[artifactToUpdateIndex] = updatedArtifact; + const ret = [...artifacts]; + return ret; + } + +} @@ -0,0 +1,48 @@ +/** + * Created by ob0695 on 7/17/2018. + */ +import {State, Action, StateContext} from '@ngxs/store'; +import {UpdateIsDesigner, UpdateIsViewOnly} from "../actions/workspace.action"; +import {Selector} from "@ngxs/store"; + +export interface WorkspaceStateModel { + isViewOnly: boolean; + isDesigner: boolean; +} + +@State<WorkspaceStateModel>({ + name: 'workspace', + defaults: { + isViewOnly: false, + isDesigner: true + } +}) + +export class WorkspaceState { + + constructor(){} + + @Selector() static isViewOnly(state: WorkspaceStateModel):boolean { + return state.isViewOnly; + } + @Selector() static isDesigner(state: WorkspaceStateModel): boolean { + return state.isDesigner; + } + + @Action(UpdateIsViewOnly) + updateIsViewOnly({getState, setState}: StateContext<WorkspaceStateModel>, action:UpdateIsViewOnly) { + const state = getState(); + setState({ + ...state, + isViewOnly: action.isViewOnly + }); + } + + @Action(UpdateIsDesigner) + updateIsDesigner({getState, patchState}: StateContext<WorkspaceStateModel>, action:UpdateIsDesigner) { + const state = getState(); + patchState({ + isDesigner: action.isDesigner + }); + } +}
\ No newline at end of file @@ -23,13 +23,12 @@ */ import { DataTypesService } from "../../services/data-types-service"; import ICacheObject = angular.ICacheObject; -import { SharingService } from "../../services/sharing-service"; import { CookieService } from "../../services/cookie-service"; -import { CacheService } from "../../services/cache-service"; import {ComponentFactory} from "../../utils/component-factory" import { EventListenerService } from "app/services/event-listener-service"; import { ModalsHandler } from "app/utils"; import IScope = angular.IScope; +import { SharingService } from "../services/sharing.service"; /** Services we need to upgrade from angular1 to angular2 - in the future we need to rewrite them all to angular2 **/ @@ -61,10 +60,6 @@ export function scopeServiceFactory(cacheObj: ICacheObject) { return cacheObj.get('$scope'); } -export function cacheServiceFactory(cacheObj: ICacheObject) { - return cacheObj.get('Sdc.Services.CacheService'); -} - export function eventListenerServiceServiceFactory(cacheObj: ICacheObject) { return cacheObj.get('EventListenerService'); } @@ -73,6 +68,10 @@ export function notificationServiceFactory(cacheObj: ICacheObject) { return cacheObj.get('Notification'); } +export function ModalsHandlerFactory(cacheObj: ICacheObject) { + return cacheObj.get('ModalsHandler'); +} + export const ComponentFactoryProvider = { provide: ComponentFactory, @@ -80,18 +79,12 @@ export const ComponentFactoryProvider = { deps: ['$injector'] }; - -export function ModalsHandlerFactory(cacheObj: ICacheObject) { - return cacheObj.get('ModalsHandler'); -} - export const DataTypesServiceProvider = { provide: DataTypesService, useFactory: dataTypesServiceFactory, deps: ['$injector'] }; - export const SharingServiceProvider = { provide: SharingService, useFactory: sharingServiceFactory, @@ -122,17 +115,12 @@ export const StateParamsServiceFactory = { useFactory: stateParamsServiceFactory, deps: ['$injector'] }; -export const CacheServiceProvider = { - provide: CacheService, - useFactory: cacheServiceFactory, - deps: ['$injector'] -}; - -export const EventListenerServiceProvider = { - provide: EventListenerService, - useFactory: eventListenerServiceServiceFactory, - deps: ['$injector'] -}; +// +// export const EventListenerServiceProvider = { +// provide: EventListenerService, +// useFactory: eventListenerServiceServiceFactory, +// deps: ['$injector'] +// }; export const NotificationServiceProvider = { provide: 'Notification', @@ -144,4 +132,4 @@ export const ModalsHandlerProvider = { provide: ModalsHandler, useFactory: ModalsHandlerFactory, deps: ['$injector'] -} +}; @@ -0,0 +1,58 @@ +/** + * Created by ob0695 on 6/3/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +import {Injectable} from "@angular/core"; + +@Injectable() +export class QueueServiceUtils { + + private executionQueue:any; + + constructor() { + if(!this.executionQueue) { + this.executionQueue = this.getQueue(); + } + } + + private getQueue = () => new Promise((resolve, reject) => { + resolve(true); + }); + + private addMethodToQueue = (runMe:Function):void => { + this.executionQueue = this.executionQueue.then(runMe, runMe); + }; + + addNonBlockingUIAction = (update:Function, releaseUIcallBack?:Function):void => { + // releaseUIcallBack(); + this.addMethodToQueue(update); + }; + + // The Method call is responsible for releasing the UI + addBlockingUIAction = (blockingServerRequest:Function):void => { + this.addMethodToQueue(blockingServerRequest); + }; + + addBlockingUIActionWithReleaseCallback = (blockingServerRequest:Function, releaseUIcallBack:Function):void=> { + this.addMethodToQueue(blockingServerRequest); + // this.addMethodToQueue(releaseUIcallBack); + }; +} |