diff options
author | vasraz <vasyl.razinkov@est.tech> | 2021-02-16 17:37:57 +0000 |
---|---|---|
committer | Christophe Closset <christophe.closset@intl.att.com> | 2021-02-17 15:57:55 +0000 |
commit | 26e5029d922779fd7e786c1a31b6b37492132388 (patch) | |
tree | 8e8e68a6913749e1405fce951bc7816d4fa35ba3 /catalog-ui/src/app/ng2 | |
parent | f2c0a4118c3c0b6360b639622766543bd754b59c (diff) |
Implement Attributes/Outputs FE
Change-Id: I014bb0ebc07f3fea4266a4f295172eadee546705
Signed-off-by: Vasyl Razinkov <vasyl.razinkov@est.tech>
Issue-ID: SDC-3448
Diffstat (limited to 'catalog-ui/src/app/ng2')
41 files changed, 3765 insertions, 709 deletions
diff --git a/catalog-ui/src/app/ng2/app.module.ts b/catalog-ui/src/app/ng2/app.module.ts index ac8a9b6eff..e8dde94d68 100644 --- a/catalog-ui/src/app/ng2/app.module.ts +++ b/catalog-ui/src/app/ng2/app.module.ts @@ -18,85 +18,86 @@ * ============LICENSE_END========================================================= */ -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { BrowserModule } from '@angular/platform-browser'; +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'; +import {FormsModule} from '@angular/forms'; import { forwardRef } from '@angular/core'; -import { AppComponent } from './app.component'; -import { UpgradeAdapter } from '@angular/upgrade'; -import { UpgradeModule } from '@angular/upgrade/static'; +import {AppComponent} from './app.component'; +import {UpgradeAdapter} from '@angular/upgrade'; +import {UpgradeModule} from '@angular/upgrade/static'; import { SdcUiComponentsModule, SdcUiComponents } from 'onap-ui-angular'; -import { PropertiesAssignmentModule } from './pages/properties-assignment/properties-assignment.module'; +import {PropertiesAssignmentModule} from './pages/properties-assignment/properties-assignment.module'; import { DataTypesServiceProvider, CookieServiceProvider, StateServiceFactory, StateParamsServiceFactory, ScopeServiceFactory, NotificationServiceProvider, ComponentFactoryProvider } 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 { 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 {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 {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 {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 {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 {ModalsModule} from './components/modals/modals.module'; import { SharingService, CacheService, HomeService } from 'app/services-ng2'; -import { ArtifactConfigService } from "./services/artifact-config.service"; -import { IUserProperties } from 'app/models'; -import { PluginsModule } from './pages/plugins/plugins-module'; +import {ArtifactConfigService} from "./services/artifact-config.service"; +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"; +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"; import {CapabilitiesFilterPropertiesEditorComponentModule} from "./pages/composition/capabilities-filter-properties-editor/capabilities-filter-properties-editor.module"; import {InterfaceOperationHandlerModule} from "./pages/composition/interface-operatons/operation-creator/interface-operation-handler.module"; +import {AttributesOutputsModule} from "./pages/attributes-outputs/attributes-outputs.module"; declare const __ENV__: string; @@ -105,128 +106,128 @@ export const upgradeAdapter = new UpgradeAdapter(forwardRef(() => AppModule)); export function configServiceFactory(config: ConfigService, authService: AuthenticationService, eventListener: EventListenerService) { - return () => { - 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'); - }); - }; + return () => { + 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, - HttpClientModule, - LayoutModule, - TranslateModule, - MultilineEllipsisModule, - UiElementsModule, - CompositionPanelModule, - SdcUiComponentsModule, - AutomatedUpgradeModule, + declarations: [ + AppComponent + ], + imports: [ + BrowserAnimationsModule, + BrowserModule, + UpgradeModule, + FormsModule, + HttpClientModule, + LayoutModule, + TranslateModule, + MultilineEllipsisModule, + UiElementsModule, + CompositionPanelModule, + SdcUiComponentsModule, + AutomatedUpgradeModule, - // We need to import them here since we use them in angular1 - ConnectionWizardModule, - PropertiesAssignmentModule, - PropertyCreatorModule, - DeclareListModule, - PluginFrameModule, - PluginsModule, - InterfaceOperationModule, - OperationCreatorModule, + // We need to import them here since we use them in angular1 + ConnectionWizardModule, + PropertiesAssignmentModule, + AttributesOutputsModule, + PropertyCreatorModule, + DeclareListModule, + PluginFrameModule, + PluginsModule, + InterfaceOperationModule, + OperationCreatorModule, InterfaceOperationHandlerModule, - ServicePathCreatorModule, - ServicePathsListModule, - ServicePathSelectorModule, - ServiceConsumptionCreatorModule, - ServiceDependenciesModule, - ServiceDependenciesEditorModule, - CapabilitiesFilterPropertiesEditorComponentModule, - WorkspaceModule, - ModalsModule, - CatalogModule, - HomeModule, - NgxsModule.forRoot([]), - NgxsLoggerPluginModule.forRoot({ logger: console, collapsed: false }), - NgxsReduxDevtoolsPluginModule.forRoot({ - disabled: __ENV__ === 'prod' - }) - ], - exports: [], - entryComponents: [ - ], - providers: [ - WindowRef, - httpInterceptorProviders, - DataTypesServiceProvider, - SharingService, - CacheService, - HomeService, - ArtifactConfigService, - ComponentFactoryProvider, - CookieServiceProvider, - StateServiceFactory, - StateParamsServiceFactory, - ScopeServiceFactory, - NotificationServiceProvider, - ModalsHandlerProvider, - UserService, - Cookie2Service, - ConfigService, - ComponentServiceNg2, - ComponentServiceFactoryNg2, - ModalService, - ImportVSPService, - OnboardingService, - ServiceServiceNg2, - AutomatedUpgradeService, - WorkflowServiceNg2, - ToscaTypesServiceNg2, - WorkspaceNg1BridgeService, - HttpHelperService, - AuthenticationService, - PoliciesService, - GroupsService, - ModulesService, - DynamicComponentService, - SdcConfig, - SdcMenu, - ComponentInstanceServiceNg2, - EventListenerService, - TranslationServiceConfig, - TranslateService, - PluginsService, - CatalogService, - EventBusService, - FileUtilsService, - { - provide: APP_INITIALIZER, - useFactory: configServiceFactory, - deps: [ConfigService, AuthenticationService, EventListenerService], - multi: true - }, - ], - bootstrap: [AppComponent] + ServicePathCreatorModule, + ServicePathsListModule, + ServicePathSelectorModule, + ServiceConsumptionCreatorModule, + ServiceDependenciesModule, + ServiceDependenciesEditorModule, + CapabilitiesFilterPropertiesEditorComponentModule, + WorkspaceModule, + ModalsModule, + CatalogModule, + HomeModule, + NgxsModule.forRoot([]), + NgxsLoggerPluginModule.forRoot({logger: console, collapsed: false}), + NgxsReduxDevtoolsPluginModule.forRoot({ + disabled: __ENV__ === 'prod' + }) + ], + exports: [], + entryComponents: [], + providers: [ + WindowRef, + httpInterceptorProviders, + DataTypesServiceProvider, + SharingService, + CacheService, + HomeService, + ArtifactConfigService, + ComponentFactoryProvider, + CookieServiceProvider, + StateServiceFactory, + StateParamsServiceFactory, + ScopeServiceFactory, + NotificationServiceProvider, + ModalsHandlerProvider, + UserService, + Cookie2Service, + ConfigService, + ComponentServiceNg2, + ComponentServiceFactoryNg2, + ModalService, + ImportVSPService, + OnboardingService, + ServiceServiceNg2, + AutomatedUpgradeService, + WorkflowServiceNg2, + ToscaTypesServiceNg2, + WorkspaceNg1BridgeService, + HttpHelperService, + AuthenticationService, + PoliciesService, + GroupsService, + ModulesService, + DynamicComponentService, + SdcConfig, + SdcMenu, + ComponentInstanceServiceNg2, + EventListenerService, + TranslationServiceConfig, + TranslateService, + PluginsService, + CatalogService, + EventBusService, + FileUtilsService, + { + provide: APP_INITIALIZER, + useFactory: configServiceFactory, + deps: [ConfigService, AuthenticationService, EventListenerService], + multi: true + }, + ], + bootstrap: [AppComponent] }) export class AppModule { - constructor(public upgrade: UpgradeModule) { + constructor(public upgrade: UpgradeModule) { - } + } } diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts new file mode 100644 index 0000000000..5f4b15781a --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts @@ -0,0 +1,49 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {AttributesTableComponent} from "./attributes-table.component"; +import {DynamicAttributeComponent} from "./dynamic-attribute/dynamic-attribute.component"; +import {FormsModule} from "@angular/forms"; +import {UiElementsModule} from "../../ui/ui-elements.module"; +import {CommonModule} from "@angular/common"; +import {FilterChildAttributesPipe} from "./pipes/filterChildAttributes.pipe"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {MultilineEllipsisModule} from "../../../shared/multiline-ellipsis/multiline-ellipsis.module"; +import {AttributesService} from "../../../services/attributes.service"; + +@NgModule({ + imports: [ + FormsModule, + CommonModule, + GlobalPipesModule, + UiElementsModule, + MultilineEllipsisModule + ], + declarations: [ + FilterChildAttributesPipe, + DynamicAttributeComponent, + AttributesTableComponent + ], + exports: [AttributesTableComponent, DynamicAttributeComponent], + providers: [FilterChildAttributesPipe, AttributesService] +}) +export class AttributeTableModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html new file mode 100644 index 0000000000..1115620594 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html @@ -0,0 +1,97 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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========================================================= + --> + +<div class="attributes-table"> + <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader> + <div class="table-header"> + <div class="table-cell col1" (click)="sort('name')">Attribute Name + <span *ngIf="sortBy === 'name'" class="table-header-sort-arrow" [ngClass]="{'down': reverse, 'up':!reverse}"> + </span> + </div> + <div class="table-cell col2" (click)="sort('type')" *ngIf="!hideAttributeType">Type + <span *ngIf="sortBy === 'type'" class="table-header-sort-arrow" [ngClass]="{'down': reverse, 'up':!reverse}"> + </span> + </div> + <div class="table-cell col3" (click)="sort('schema.property.type')" *ngIf="!hideAttributeType">EntrySchema + <span *ngIf="sortBy === 'schema.property.type'" class="table-header-sort-arrow" [ngClass]="{'down': reverse, 'up':!reverse}"> + </span> + </div> + <div class="table-cell valueCol">Value</div> + </div> + <div class="table-body" [ngClass]="{'view-mode': readonly}"> + <div class="no-data" *ngIf="!feAttributesMap || !(feAttributesMap | keys).length">No data to display</div> + + <ng-container *ngFor="let instanceId of feAttributesMap | 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}} + <div class="sprite-new archive-label" *ngIf="feInstanceNamesMap[instanceId].originArchived == true"></div> + </div> + + <div class="table-row" *ngFor="let property of feAttributesMap[instanceId] | searchFilter:'name':searchTerm | propertiesOrderBy:{path: path, direction: direction}; trackBy:property?.name " + (click)="onClickAttributeRow(property, instanceId, $event)" [ngClass]="{'selected': selectedAttributeId && selectedAttributeId === property.name, 'readonly': property.isDisabled || property.isDeclared}"> + + <div class="table-cell col1" [ngClass]="{'filtered':property.name === attributeNameSearchText}" [class.round-checkbox]="property.isDeclared"> + <!-- Attribute Name --> + <div class="attribute-name"> + <checkbox *ngIf="hasDeclareOption" [(checked)]="property.isSelected" [disabled]="property.isDisabled || property.isDeclared || readonly" + (checkedChange)="attributeChecked(property)" [attr.data-tests-id]="property.name"></checkbox> + <div class="inner-cell-div-multiline" tooltip="{{property.name}}"> + <multiline-ellipsis className="table-cell-multiline-ellipsis" [lines]="2">{{property.name}}</multiline-ellipsis> + </div> + </div> + <span *ngIf="property.description" class="property-description-icon sprite-new show-desc" tooltip="{{property.description}}" + tooltipDelay="0"></span> + </div> + <!-- Attribute Type --> + <div class="table-cell col2" *ngIf="!hideAttributeType"> + <div class="inner-cell-div" tooltip="{{property.type | contentAfterLastDot}}"> + <span>{{property.type | contentAfterLastDot}}</span> + </div> + </div> + <!-- Attribute ES (Entry Schema) --> + <div class="table-cell col3" *ngIf="!hideAttributeType"> + <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> + <!-- Attribute Value --> + <div class="table-cell valueCol"> + <dynamic-property + [selectedAttributeId]="selectedAttributeId" + [hasDeclareOption]="hasDeclareOption" + [canBeDeclared]="hasDeclareOption && true" + [attribute]="property" + [expandedChildId]="property.expandedChildPropertyId" + [attributeNameSearchText]="attributeNameSearchText" + [readonly]="readonly" + (attributeChanged)="onAttributeChanged(property)" + (expandChild)="property.updateExpandedChildPropertyId($event)" + (clickOnAttributeRow)="onClickAttributeInnerRow($event, instanceId)" + (checkAttribute)="attributeChecked(property, $event)" + > + </dynamic-property> + </div> + </div> + </ng-container> + + </div> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less new file mode 100644 index 0000000000..26ae0d4d74 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less @@ -0,0 +1,264 @@ +@import './../../../../../assets/styles/mixins.less'; +@import '../../../../../assets/styles/sprite'; +@smaller-screen: ~"only screen and (max-width: 1580px)"; + +:host /deep/ input { width:100%;} + +.attributes-table { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + text-align: left; + + + .inner-cell-div { + text-overflow: ellipsis; + overflow: hidden; + height: 20px; + } + + .inner-cell-div-multiline { + max-width: 100%; + } + + .table-header { + display: flex; + flex-direction:row; + flex: 0 0 auto; + font-weight:bold; + border-top: #d2d2d2 solid 1px; + background-color: #f2f2f2; + + .table-cell { + color:#191919; + font-size:13px; + .table-header-sort-arrow { + display: inline-block; + background-color: transparent; + border: none; + color: #AAA; + margin: 8px 0 0 5px; + &.up { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid; + height:5px; + } + &.down { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid; + } + } + } + } + + .table-rows-header { + border: #d2d2d2 solid 1px; + border-top:none; + display: flex; + align-items: center; + .archive-label{ + margin-left: 10px; + } + } + + .table-body { + display:flex; + flex-direction: column; + overflow-y:auto; + flex: 1; + background-color: @main_color_p; + + .no-data { + border: #d2d2d2 solid 1px; + border-top:none; + text-align: center; + height: 100%; + padding: 20px; + } + /deep/.selected{ + background-color: #e6f6fb; + color: #009fdb; + } + &.view-mode{ + /deep/ .dynamic-property-row:not(.selected){ + background-color:#f8f8f8; + } + } + .table-row { + display: flex; + flex-direction:row; + flex: 0 0 auto; + &.readonly{ + background-color: #f8f8f8; + cursor: auto; + } + + &:hover:not(.selected){ + background-color:#f8f8f8; cursor:pointer; + /deep/ .dynamic-property-row:not(.selected){ + background-color:#f8f8f8; cursor:pointer; + } + } + + .selected-row { + background-color:#e6f6fb; + } + + .table-cell.valueCol { + padding:0px; + + } + } + } + .table-cell { + font-size:13px; + flex:1; + border: #d2d2d2 solid 1px; + border-right:none; + border-top:none; + padding:10px; + text-overflow: ellipsis; + white-space: nowrap; + overflow:hidden; + display: flex; + min-height:40px; + + &:last-child { + border-right:#d2d2d2 solid 1px; + } + + // Column: Property Name + &.col1 { + flex: 1 0 190px; + max-width:300px; + display: flex; + @media @smaller-screen { flex: 0 0 25%;} + + .attribute-name { + flex: 1; + display: flex; + overflow: hidden; + //max-width: 90%; fix bug 327139 + } + + .property-description-icon { + float: right; + margin-top: 4px; + margin-left: 15px; + flex: 0 0 auto; + } + } + + // Column: Type + &.col2 { + flex: 0 0 150px; + max-width:150px; + @media @smaller-screen { flex: 0 0 20%;} + } + + // Column: ES + &.col3 { + flex:0 0 120px; + max-width:120px; + @media @smaller-screen { flex: 0 0 15%;} + } + + // Column: Value + &.valueCol { + flex: 2 0 250px; + display: flex; + @media @smaller-screen { flex: 1 0 40%;} + } + + + /deep/ .checkbox-container { + margin-right: 10px; + } + + /deep/ &.round-checkbox { + .checkbox-container input[type=checkbox].checkbox-hidden { + &:checked ~ .checkbox-icon::before { + .sprite-new; + .round-checked-icon; + } + &[disabled] ~ .checkbox-icon::before { + .sprite-new; + .round-checked-icon.disabled; + background-color:inherit; + border:none; + //animation: addDisabledCheck 4s linear; + } + } + } + } + + .delete-button-container { + max-height: 24px; + cursor: pointer; + } + + .filtered { + /deep/ .checkbox-label-content{ + background-color: yellow; + } + } + + dynamic-property { + width:100%; + &:last-child /deep/ .dynamic-property-row { + border-bottom:none; + } + } + + .table-row { + /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots { + background: linear-gradient(to right, transparent 0%, #ffffff 80%); + padding-left: 1em; + } + + &.selected /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots { + background: linear-gradient(to right, transparent 0%, #e6f6fb 80%); + padding-left: 1em; + } + + &.readonly /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots { + background: linear-gradient(to right, transparent 0%, #f8f8f8 80%); + padding-left: 1em; + } + + &:hover:not(.selected) /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots { + background: linear-gradient(to right, transparent 0%, #f8f8f8 80%); + padding-left: 1em; + } + } + + .prop-instance-icon { + vertical-align: middle; + margin-right: 7px; + &.defaulticon.small { + background-color: @main_color_q; + border-radius:14px; + } + // square icons + &.icon-group { + .square-icon(); + background-color: @main_color_a; + + &::before { + content: "G"; + } + } + &.icon-policy { + .square-icon(); + background-color: @main_color_r; + + &::before { + content: "P"; + } + } + + } +} diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts new file mode 100644 index 0000000000..e916d788ce --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts @@ -0,0 +1,190 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {NO_ERRORS_SCHEMA, SimpleChange} from '@angular/core'; +import {ComponentFixture} from '@angular/core/testing'; +import {ConfigureFn, configureTests} from '../../../../../jest/test-config.helper'; +import {ContentAfterLastDotPipe} from '../../../pipes/contentAfterLastDot.pipe'; +import {KeysPipe} from '../../../pipes/keys.pipe'; +import {SearchFilterPipe} from '../../../pipes/searchFilter.pipe'; +import {ModalService} from '../../../services/modal.service'; +import {AttributeRowSelectedEvent, AttributesTableComponent} from './attributes-table.component'; +import {AttributesService} from "../../../services/attributes.service"; +import {AttributeFEModel} from "../../../../models/attributes-outputs/attribute-fe-model"; +import {AttributeBEModel} from "app/models/attributes-outputs/attribute-be-model"; +import {DerivedFEAttribute} from "../../../../models/attributes-outputs/derived-fe-attribute"; +import {PropertiesOrderByPipe} from "app/ng2/pipes/properties-order-by.pipe"; + +describe('attributes-table component', () => { + + let fixture: ComponentFixture<AttributesTableComponent>; + let attributesServiceMock: Partial<AttributesService>; + let modalServiceMock: Partial<ModalService>; + + beforeEach( + () => { + attributesServiceMock = { + undoDisableRelatedAttributes: jest.fn(), + disableRelatedAttributes: jest.fn() + }; + modalServiceMock = {}; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [ + AttributesTableComponent, + KeysPipe, + PropertiesOrderByPipe, + SearchFilterPipe, + ContentAfterLastDotPipe + ], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: AttributesService, useValue: attributesServiceMock}, + {provide: ModalService, useValue: modalServiceMock} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributesTableComponent); + }); + } + ); + + it('When Properties assignment page is loaded, it is sorted by attribute 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(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.onAttributeChanged('testProperty'); + expect(fixture.componentInstance.emitter.emit).toHaveBeenCalledWith('testProperty'); + }); + + it('When onClickPropertyRow is called, selectedPropertyId is updated and event is emitted.', () => { + const attributeFEModel = new AttributeFEModel(new AttributeBEModel()); + attributeFEModel.name = 'attributeName'; + const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(attributeFEModel, 'instanceName'); + + spyOn(fixture.componentInstance.selectAttributeRow, 'emit'); + fixture.componentInstance.onClickAttributeRow(attributeFEModel, 'instanceName'); + + expect(fixture.componentInstance.selectedAttributeId).toBe('attributeName'); + expect(fixture.componentInstance.selectAttributeRow.emit).toHaveBeenCalledWith(attributeRowSelectedEvent); + }); + + it('When onClickPropertyInnerRow is called, event is emitted.', () => { + const derivedFEProperty = new DerivedFEAttribute(new AttributeBEModel()); + const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(derivedFEProperty, 'instanceName'); + spyOn(fixture.componentInstance.selectAttributeRow, 'emit'); + fixture.componentInstance.onClickAttributeInnerRow(derivedFEProperty, 'instanceName'); + + expect(fixture.componentInstance.selectAttributeRow.emit).toHaveBeenCalledWith(attributeRowSelectedEvent); + }); + + it('When attributeChecked is called, attributesService.undoDisableRelatedProperties is called and event is emitted.', () => { + + const attributeFEModel = new AttributeFEModel(new AttributeBEModel()); + attributeFEModel.isSelected = false; + + spyOn(fixture.componentInstance.updateCheckedAttributeCount, 'emit'); + fixture.componentInstance.attributeChecked(attributeFEModel); + expect(attributesServiceMock.undoDisableRelatedAttributes).toHaveBeenCalledWith(attributeFEModel, undefined); + expect(fixture.componentInstance.updateCheckedAttributeCount.emit).toHaveBeenCalledWith(false); + }); + + it('When attributeChecked is called, attributesService.disableRelatedProperties is called and event is emitted.', () => { + + const attributeFEModel = new AttributeFEModel(new AttributeBEModel()); + attributeFEModel.isSelected = true; + + spyOn(fixture.componentInstance.updateCheckedAttributeCount, 'emit'); + fixture.componentInstance.attributeChecked(attributeFEModel); + expect(attributesServiceMock.disableRelatedAttributes).toHaveBeenCalledWith(attributeFEModel, undefined); + expect(fixture.componentInstance.updateCheckedAttributeCount.emit).toHaveBeenCalledWith(true); + }); + +}); diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts new file mode 100644 index 0000000000..000e2cc6e9 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts @@ -0,0 +1,116 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core'; +import {InstanceFeDetails} from '../../../../models/instance-fe-details'; +import {InstanceFeAttributesMap} from "../../../../models/attributes-outputs/attribute-fe-map"; +import {AttributeFEModel} from "../../../../models/attributes-outputs/attribute-fe-model"; +import {AttributesService} from "../../../services/attributes.service"; +import {DerivedFEAttribute} from "../../../../models/attributes-outputs/derived-fe-attribute"; + +@Component({ + selector: 'attributes-table', + templateUrl: './attributes-table.component.html', + styleUrls: ['./attributes-table.component.less'] +}) +export class AttributesTableComponent implements OnChanges { + + @Input() feAttributesMap: InstanceFeAttributesMap; + @Input() feInstanceNamesMap: Map<string, InstanceFeDetails>; + @Input() selectedAttributeId: string; + @Input() attributeNameSearchText: string; + @Input() searchTerm: string; + @Input() readonly: boolean; + @Input() isLoading: boolean; + @Input() hasDeclareOption: boolean; + @Input() hideAttributeType: boolean; + @Input() showDelete: boolean; + + @Output('attributeChanged') emitter: EventEmitter<AttributeFEModel> = new EventEmitter<AttributeFEModel>(); + @Output() selectAttributeRow: EventEmitter<AttributeRowSelectedEvent> = new EventEmitter<AttributeRowSelectedEvent>(); + @Output() updateCheckedAttributeCount: EventEmitter<boolean> = new EventEmitter<boolean>(); // only for hasDeclareOption + @Output() updateCheckedChildAttributeCount: EventEmitter<boolean> = new EventEmitter<boolean>();//only for hasDeclareListOption + @Output() deleteAttribute: EventEmitter<AttributeFEModel> = new EventEmitter<AttributeFEModel>(); + + sortBy: string; + reverse: boolean; + direction: number; + path: string[]; + + readonly ascUpperLettersFirst = 1; + readonly descLowerLettersFirst = -1; + + constructor(private attributesService: AttributesService) { + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.fePropertiesMap) { + this.sortBy = ''; + this.sort('name'); + } + } + + 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('.'); + } + + onAttributeChanged = (attribute) => { + this.emitter.emit(attribute); + } + + // Click on main row (row of AttributeFEModel) + onClickAttributeRow = (attribute: AttributeFEModel, instanceName: string) => { + this.selectedAttributeId = attribute.name; + const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(attribute, instanceName); + this.selectAttributeRow.emit(attributeRowSelectedEvent); + } + + // Click on inner row (row of DerivedFEAttribute) + onClickAttributeInnerRow = (attribute: DerivedFEAttribute, instanceName: string) => { + const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(attribute, instanceName); + this.selectAttributeRow.emit(attributeRowSelectedEvent); + } + + attributeChecked = (attrib: AttributeFEModel, childAttribName?: string) => { + const isChecked: boolean = (!childAttribName) ? attrib.isSelected : attrib.flattenedChildren.find((attrib) => attrib.attributesName == childAttribName).isSelected; + + if (isChecked) { + this.attributesService.disableRelatedAttributes(attrib, childAttribName); + } else { + this.attributesService.undoDisableRelatedAttributes(attrib, childAttribName); + } + this.updateCheckedAttributeCount.emit(isChecked); + + } + +} + +export class AttributeRowSelectedEvent { + attributeModel: AttributeFEModel | DerivedFEAttribute; + instanceName: string; + + constructor(attributeModel: AttributeFEModel | DerivedFEAttribute, instanceName: string) { + this.attributeModel = attributeModel; + this.instanceName = instanceName; + } +} diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html new file mode 100644 index 0000000000..7f271af4e1 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html @@ -0,0 +1,102 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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========================================================= + --> + +<div *ngIf="!attribute.hidden" class="dynamic-property-row nested-level-{{nestedLevel}}" [@fadeIn] + [ngClass]="{'selected': selectedAttributeId && selectedAttributeId === attribute.propertiesName, 'readonly': attribute.isDisabled ||attribute.isDeclared}" + [class.with-top-border]="attribute.isChildOfListOrMap" + (click)="onClickPropertyRow(attribute, $event)"> + <!-- LEFT CELL --> + <ng-container *ngIf="!isAttributeFEModel"> + <div class="table-cell" *ngIf="canBeDeclared" [ngClass]="{'filtered':attribute.name === attributeNameSearchText}" [class.round-checkbox]="attribute.isDeclared"> <!-- simple children of complex type [@checkEffect]="property.isDeclared"--> + <checkbox *ngIf="hasDeclareOption" [(checked)]="attribute.isSelected" [disabled]="attribute.isDisabled ||attribute.isDeclared || readonly" (checkedChange)="checkAttribute.emit(attribute.propertiesName)" ></checkbox> + <div class="inner-cell-div" tooltip="{{attribute.name}}"><span>{{attribute.name}}</span></div> + </div> + <div class="table-cell" *ngIf="!canBeDeclared && !attribute.isChildOfListOrMap"> + <div class="inner-cell-div" tooltip="{{attribute.name}}"><span>{{attribute.name}}</span></div> + </div> <!-- simple children of complex type within map or list --> + <div class="table-cell map-entry" *ngIf="attribute.isChildOfListOrMap && attribType == derivedAttributeType.MAP"><!-- map left cell --> + <dynamic-element #mapKeyInput + class="value-input" + pattern="validationUtils.getValidationPattern(string)" + [value]="attribute.mapKey" + type="string" + [name]="attribute.name" + (elementChanged)="mapKeyChanged.emit($event.value)" + [readonly]="readonly" + [testId]="'prop-key-' + attributeTestsId" + ></dynamic-element> + </div> + </ng-container> + <!-- RIGHT CELL OR FULL WIDTH CELL--> + <ng-container *ngIf="attribType == derivedAttributeType.SIMPLE || attribute.isDeclared || (attribute.isChildOfListOrMap && attribType == derivedAttributeType.MAP && attribute.schema.property.isSimpleType)"> + <div class="table-cell"> + <dynamic-element class="value-input" + pattern="validationUtils.getValidationPattern(property.type)" + [value]="attribute.isDeclared ? attribute.value : attribute.valueObj" + [type]="attribute.isDeclared ? 'string' : attribute.type" + [name]="attribute.name" + [path]="attribute.propertiesName" + (elementChanged)="onElementChanged($event)" + [readonly]="readonly || attribute.isDeclared || attribute.isDisabled" + [testId]="'prop-' + attributeTestsId" + [declared] = "attribute.isDeclared" + [constraints] = "constraints" + ></dynamic-element> + </div> + </ng-container> + <ng-container *ngIf="!isAttributeFEModel && attribType != derivedAttributeType.SIMPLE && !attribute.isDeclared"> <!-- right cell for complex elements, or list complex --> + <div class="table-cell" *ngIf="attribType == derivedAttributeType.COMPLEX">{{attribute.type | contentAfterLastDot }}</div> + <div class="table-cell" *ngIf="attribType == derivedAttributeType.MAP && !attribute.schema.property.isSimpleType">{{attribute.schema.property.type | contentAfterLastDot }}</div> + </ng-container> + <ng-container *ngIf="isAttributeFEModel && (attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && !attribute.isDeclared"><!-- empty, full-width table cell - for PropertyFEModel of type list or map --> + <div class="table-cell empty"></div> + </ng-container> + <!-- ICONS: add, delete, and expand --> + <ng-container *ngIf="!attribute.isDeclared"> + <a *ngIf="(attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && !attribute.isChildOfListOrMap" class="property-icon add-item" (click)="createNewChildProperty();" [ngClass]="{'disabled':readonly || preventInsertItem(attribute)}" [attr.data-tests-id]="'add-to-list-' + attributeTestsId">Add value to list</a> + <span *ngIf="attribute.isChildOfListOrMap" (click)="deleteItem.emit(attribute);" class="property-icon sprite-new delete-item-icon" [ngClass]="{'disabled':readonly}" [attr.data-tests-id]="'delete-from-list-' + attributeTestsId"></span> + <span *ngIf="!isAttributeFEModel && (attribType == derivedAttributeType.COMPLEX || ((attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && hasChildren))" (click)="expandChildById(attribPath)" class="property-icon sprite-new round-expand-icon" [class.open]="expandedChildId.indexOf(attribPath) == 0" [attr.data-tests-id]="'expand-' + attributeTestsId" ></span> + </ng-container> + +</div> +<!-- FLAT CHILDREN --> +<div class="flat-children-container" *ngIf="isAttributeFEModel && !attribute.isDeclared"> + <ng-container *ngFor="let prop of attribute.flattenedChildren | filterChildAttributes: expandedChildId; trackBy:prop?.propertiesName"> + <dynamic-property + [selectedAttributeId]="selectedAttributeId" + [hasDeclareOption]="hasDeclareOption" + [canBeDeclared]="hasDeclareOption && prop.canBeDeclared" + [attribute]="prop" + [rootAttribute]="rootAttribute || attribute" + [expandedChildId]="expandedChildId" + [attributeNameSearchText]="attributeNameSearchText" + [readonly]="readonly" + [hasChildren]="getHasChildren(prop)" + (propertyChanged)="childValueChanged(prop)" + (mapKeyChanged)="updateChildKeyInParent(prop, $event)" + (expandChild)="expandChildById($event)" + (deleteItem)="deleteListOrMapItem($event)" + (clickOnAttributeRow)="onClickPropertyRow($event)" + (checkAttribute)="checkedChange($event)" + (addChildAttribsToParent)="addChildProps($event, prop.propertiesName)" + > + </dynamic-property> + </ng-container> +</div> diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less new file mode 100644 index 0000000000..fd572b0f2d --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less @@ -0,0 +1,82 @@ +@import '../../../../../../assets/styles/variables.less'; +.flat-children-container { + .dynamic-property-row { + /*create nested left border classes for up to 10 levels of nesting*/ + .nested-border-loop(@i) when (@i > 0) { + @size: (@i - 1) *2; + &.nested-level-@{i} .table-cell:first-child { + border-left: ~"solid @{size}px #009fdb"; + } + .nested-border-loop(@i - 1) + } + .nested-border-loop(10); + } + dynamic-property { + &:first-child .dynamic-property-row.with-top-border { + border-top:solid 1px #d2d2d2; + } + &:not(:last-child) .dynamic-property-row { + border-bottom:solid 1px #d2d2d2; + } + } +} +.dynamic-property-row { + display:flex; + flex-direction:row; + align-items: stretch; + + &.readonly{ + background-color: @tlv_color_t; + cursor: auto; + } + //for the case that the parent is disabled but the child is enabled + &:not(.readonly){ + background-color: @main_color_p; + } + + .table-cell { + flex: 1; + padding:9px; + justify-content: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:first-child { + flex: 0 0 50%; + border-right:#d2d2d2 solid 1px; + &:only-of-type { + flex: 1 1 100%; + border-right:none; + } + } + &.empty { + height:40px; + } + } + .property-icon { + flex: 0 0 auto; + margin-right:10px; + align-self:center; + cursor:pointer; + } +} + +.filtered { + /deep/ .checkbox-label-content{ + background-color: yellow; + } +} +.inner-cell-div{ + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + display: inline; + padding-left: 8px; +} +.error { + border: solid 1px @func_color_q; + color: @func_color_q; + outline: none; + box-sizing: border-box; +} diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts new file mode 100644 index 0000000000..39faac9283 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts @@ -0,0 +1,232 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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, EventEmitter, Input, Output, ViewChild} from "@angular/core"; +import {PROPERTY_TYPES} from 'app/utils'; +import {DataTypeService} from "../../../../services/data-type.service"; +import {animate, style, transition, trigger} from '@angular/animations'; +import {IUiElementChangeEvent} from "../../../ui/form-components/ui-element-base.component"; +import {DynamicElementComponent} from "../../../ui/dynamic-element/dynamic-element.component"; +import {DerivedAttributeType} from "../../../../../models/attributes-outputs/attribute-be-model"; +import {AttributeFEModel} from "app/models/attributes-outputs/attribute-fe-model"; +import {DerivedFEAttribute} from "../../../../../models/attributes-outputs/derived-fe-attribute"; +import {AttributesUtils} from "../../../../pages/attributes-outputs/services/attributes.utils"; + +@Component({ + selector: 'dynamic-property', + templateUrl: './dynamic-attribute.component.html', + styleUrls: ['./dynamic-attribute.component.less'], + animations: [trigger('fadeIn', [transition(':enter', [style({opacity: '0'}), animate('.7s ease-out', style({opacity: '1'}))])])] +}) +export class DynamicAttributeComponent { + + derivedAttributeType = DerivedAttributeType; + attribType: DerivedAttributeType; + attribPath: string; + isAttributeFEModel: boolean; + nestedLevel: number; + attributeTestsId: string; + constraints: string[]; + + @Input() canBeDeclared: boolean; + @Input() attribute: AttributeFEModel | DerivedFEAttribute; + @Input() expandedChildId: string; + @Input() selectedAttributeId: string; + @Input() attributeNameSearchText: string; + @Input() readonly: boolean; + @Input() hasChildren: boolean; + @Input() hasDeclareOption: boolean; + @Input() rootAttribute: AttributeFEModel; + + @Output('attributeChanged') emitter: EventEmitter<void> = new EventEmitter<void>(); + @Output() expandChild: EventEmitter<string> = new EventEmitter<string>(); + @Output() checkAttribute: EventEmitter<string> = new EventEmitter<string>(); + @Output() deleteItem: EventEmitter<string> = new EventEmitter<string>(); + @Output() clickOnAttributeRow: EventEmitter<AttributeFEModel | DerivedFEAttribute> = new EventEmitter<AttributeFEModel | DerivedFEAttribute>(); + @Output() mapKeyChanged: EventEmitter<string> = new EventEmitter<string>(); + @Output() addChildAttribsToParent: EventEmitter<Array<DerivedFEAttribute>> = new EventEmitter<Array<DerivedFEAttribute>>(); + + @ViewChild('mapKeyInput') public mapKeyInput: DynamicElementComponent; + + constructor(private attributesUtils: AttributesUtils, private dataTypeService: DataTypeService) { + } + + ngOnInit() { + this.isAttributeFEModel = this.attribute instanceof AttributeFEModel; + this.attribType = this.attribute.derivedDataType; + this.attribPath = (this.attribute instanceof AttributeFEModel) ? this.attribute.name : this.attribute.attributesName; + this.nestedLevel = (this.attribute.attributesName.match(/#/g) || []).length; + this.rootAttribute = (this.rootAttribute) ? this.rootAttribute : <AttributeFEModel>this.attribute; + this.attributeTestsId = this.getAttributeTestsId(); + + this.initConstraintsValues(); + + } + + initConstraintsValues() { + let primitiveProperties = ['string', 'integer', 'float', 'boolean']; + + if (this.attribute.constraints) { + this.constraints = this.attribute.constraints[0].validValues + } + + //Complex Type + else if (primitiveProperties.indexOf(this.rootAttribute.type) == -1 && primitiveProperties.indexOf(this.attribute.type) >= 0) { + this.constraints = this.dataTypeService.getConstraintsByParentTypeAndUniqueID(this.rootAttribute.type, this.attribute.name); + } else { + this.constraints = null; + } + + } + + onClickPropertyRow = (property, event) => { + // Because DynamicAttributeComponent is recursive second time the event is fire event.stopPropagation = undefined + event && event.stopPropagation && event.stopPropagation(); + this.clickOnAttributeRow.emit(property); + } + + expandChildById = (id: string) => { + this.expandedChildId = id; + this.expandChild.emit(id); + } + + checkedChange = (propName: string) => { + this.checkAttribute.emit(propName); + } + + getHasChildren = (property: DerivedFEAttribute): boolean => {// enter to this function only from base property (AttributeFEModel) and check for child property if it has children + return _.filter((<AttributeFEModel>this.attribute).flattenedChildren, (prop: DerivedFEAttribute) => { + return _.startsWith(prop.attributesName + '#', property.attributesName); + }).length > 1; + } + + getAttributeTestsId = () => { + return [this.rootAttribute.name].concat(this.rootAttribute.getParentNamesArray(this.attribute.attributesName, [], true)).join('.'); + }; + + onElementChanged = (event: IUiElementChangeEvent) => { + this.attribute.updateValueObj(event.value, event.isValid); + this.emitter.emit(); + }; + + createNewChildProperty = (): void => { + + let newProps: Array<DerivedFEAttribute> = this.attributesUtils.createListOrMapChildren(this.attribute, "", null); + this.attributesUtils.assignFlattenedChildrenValues(this.attribute.valueObj, [newProps[0]], this.attribute.attributesName); + if (this.attribute instanceof AttributeFEModel) { + this.addChildProps(newProps, this.attribute.name); + } else { + this.addChildAttribsToParent.emit(newProps); + } + } + + addChildProps = (newProps: Array<DerivedFEAttribute>, childPropName: string) => { + + if (this.attribute instanceof AttributeFEModel) { + let insertIndex: number = this.attribute.getIndexOfChild(childPropName) + this.attribute.getCountOfChildren(childPropName); //insert after parent prop and existing children + this.attribute.flattenedChildren.splice(insertIndex, 0, ...newProps); //using ES6 spread operator + this.expandChildById(newProps[0].attributesName); + + this.updateMapKeyValueOnMainParent(newProps); + this.emitter.emit(); + } + } + + updateMapKeyValueOnMainParent(childrenProps: Array<DerivedFEAttribute>) { + if (this.attribute instanceof AttributeFEModel) { + const attributeFEModel: AttributeFEModel = <AttributeFEModel>this.attribute; + //Update only if all this attributeFEModel parents has key name + if (attributeFEModel.getParentNamesArray(childrenProps[0].attributesName, []).indexOf('') === -1) { + angular.forEach(childrenProps, (prop: DerivedFEAttribute): void => { //Update parent AttributeFEModel with value for each child, including nested props + attributeFEModel.childPropUpdated(prop); + if (prop.isChildOfListOrMap && prop.mapKey !== undefined) { + attributeFEModel.childPropMapKeyUpdated(prop, prop.mapKey, true); + } + }, this); + //grab the cumulative value for the new item from parent AttributeFEModel and assign that value to DerivedFEProp[0] (which is the list or map parent with UUID of the set we just added) + let parentNames = (<AttributeFEModel>attributeFEModel).getParentNamesArray(childrenProps[0].attributesName, []); + childrenProps[0].valueObj = _.get(attributeFEModel.valueObj, parentNames.join('.'), null); + } + } + } + + childValueChanged = (property: DerivedFEAttribute) => { //value of child property changed + + if (this.attribute instanceof AttributeFEModel) { // will always be the case + if (this.attribute.getParentNamesArray(property.attributesName, []).indexOf('') === -1) {//If one of the parents is empty key -don't save + this.attribute.childPropUpdated(property); + this.emitter.emit(); + } + } + } + + deleteListOrMapItem = (item: DerivedFEAttribute) => { + if (this.attribute instanceof AttributeFEModel) { + this.removeValueFromParent(item); + this.attribute.flattenedChildren.splice(this.attribute.getIndexOfChild(item.attributesName), this.attribute.getCountOfChildren(item.attributesName)); + this.expandChildById(item.attributesName); + } + } + + removeValueFromParent = (item: DerivedFEAttribute) => { + if (this.attribute instanceof AttributeFEModel) { + let itemParent = (item.parentName == this.attribute.name) + ? this.attribute : this.attribute.flattenedChildren.find(prop => prop.attributesName == item.parentName); + if (!itemParent) { + return; + } + + if (item.derivedDataType == DerivedAttributeType.MAP) { + const oldKey = item.getActualMapKey(); + delete itemParent.valueObj[oldKey]; + if (itemParent instanceof AttributeFEModel) { + delete itemParent.valueObjValidation[oldKey]; + itemParent.valueObjIsValid = itemParent.calculateValueObjIsValid(); + } + this.attribute.childPropMapKeyUpdated(item, null); // remove map key + } else { + const itemIndex: number = this.attribute.flattenedChildren.filter(prop => prop.parentName == item.parentName).map(prop => prop.attributesName).indexOf(item.attributesName); + itemParent.valueObj.splice(itemIndex, 1); + if (itemParent instanceof AttributeFEModel) { + itemParent.valueObjValidation.splice(itemIndex, 1); + itemParent.valueObjIsValid = itemParent.calculateValueObjIsValid(); + } + } + if (itemParent instanceof AttributeFEModel) { //direct child + this.emitter.emit(); + } else { //nested child - need to update parent prop by getting flattened name (recurse through parents and replace map/list keys, etc) + this.childValueChanged(itemParent); + } + } + } + + updateChildKeyInParent(childProp: DerivedFEAttribute, newMapKey: string) { + if (this.attribute instanceof AttributeFEModel) { + this.attribute.childPropMapKeyUpdated(childProp, newMapKey); + this.emitter.emit(); + } + } + + preventInsertItem = (property: DerivedFEAttribute): boolean => { + return property.type == PROPERTY_TYPES.MAP && Object.keys(property.valueObj).indexOf('') > -1; + } + +} diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts new file mode 100644 index 0000000000..b2098d8478 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts @@ -0,0 +1,38 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {DerivedFEAttribute} from "../../../../../models/attributes-outputs/derived-fe-attribute"; + +@Pipe({ + name: 'filterChildAttributes', +}) +export class FilterChildAttributesPipe implements PipeTransform { + public transform(childAttributes: Array<DerivedFEAttribute>, parentId: string) { + if (!parentId || !childAttributes) return childAttributes; + + let validParents: Array<string> = [parentId]; + while (parentId.lastIndexOf('#') > 0) { + parentId = parentId.substring(0, parentId.lastIndexOf('#')); + validParents.push(parentId); + } + return childAttributes.filter(derivedAttrib => validParents.indexOf(derivedAttrib.parentName) > -1); + } +} diff --git a/catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts b/catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts new file mode 100644 index 0000000000..fc6c73f2f6 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts @@ -0,0 +1,32 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {BrowserModule} from '@angular/platform-browser' +import {HierarchyNavigationComponent} from "./hierarchy-navigation.component"; + +@NgModule({ + imports: [BrowserModule], + declarations: [HierarchyNavigationComponent], + bootstrap: [], + exports: [HierarchyNavigationComponent] +}) +export class HierarchyNavigationModule { +} diff --git a/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html new file mode 100644 index 0000000000..fbae0e45e6 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html @@ -0,0 +1,97 @@ +<!-- +============LICENSE_START======================================================= +* Copyright (C) 2021 Nordix Foundation +* ================================================================================ +* 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. +* +* SPDX-License-Identifier: Apache-2.0 +* ============LICENSE_END========================================================= +--> + +<div class="output-attributes-table"> + <loader [display]="isLoading" [size]="'large'" [relative]="true"></loader> + <div class="table-header"> + <div class="table-cell col-output-attribute-name" (click)="sort('name')">Output Name + <span *ngIf="sortBy === 'name'" class="table-header-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"></span> + </div> + <div class="table-cell col-output-attribute-instance" (click)="sort('instanceUniqueId')">From + Instance + <span *ngIf="sortBy === 'instanceUniqueId'" class="table-header-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"></span> + </div> + <div class="table-cell col-output-attribute-type" (click)="sort('type')">Type + <span *ngIf="sortBy === 'type'" class="table-header-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"> + </span> + </div> + <div class="table-cell col-output-attribute-required" (click)="sort('required')" + *ngIf="componentType == 'SERVICE'"> + <span tooltip="Required in Runtime" tooltipDelay="400">Req. in RT</span> + </div> + <div class="table-cell col-output-attribute-value">Value</div> + </div> + <div class="table-body"> + <div class="no-data" *ngIf="!outputs || !outputs.length">No data to display</div> + <div> + <div class="table-row" *ngFor="let output of outputs"> + <!-- attribute Name --> + <div class="table-cell col-output-attribute-name"> + <div class="output-inner-cell-div"> + <span class="attribute-name" tooltip="{{output.name}}">{{output.name}}</span> + </div> + <span *ngIf="output.description" + class="attribute-description-icon sprite-new show-desc" + tooltip="{{output.description}}" tooltipDelay="0"></span> + </div> + <!-- From Instance --> + <div class="table-cell col-output-attribute-instance"> + <div class="output-inner-cell-div" + tooltip="{{instanceNamesMap[output.instanceUniqueId]?.name}}"> + <span>{{instanceNamesMap[output.instanceUniqueId]?.name}}</span> + </div> + </div> + <!-- Type --> + <div class="table-cell col-output-attribute-type"> + <div class="output-inner-cell-div" tooltip="{{output.type | contentAfterLastDot}}"> + <span>{{output.type | contentAfterLastDot}}</span> + </div> + </div> + <!-- Required in runtime --> + <div class="table-cell col-output-attribute-required" *ngIf="componentType == 'SERVICE'"> + <sdc-checkbox [(checked)]="output.required" + (checkedChange)="onRequiredChanged(output, $event)" + [disabled]="readonly"></sdc-checkbox> + </div> + <!-- Value --> + <div class="table-cell col-output-attribute-value output-value-col" + [class.inner-table-container]="!output.isSimpleType"> + <dynamic-element class="value-output" + *ngIf="checkInstanceFeAttributesMapIsFilled() && output.isSimpleType" + pattern="null" + [value]="output.value" + [type]="'string'" + [name]="output.name" + (elementChanged)="onOutputChanged(output, $event)" + [readonly]="true" + [testId]="'output-' + output.name" + [constraints]="getConstraints(output)"> + </dynamic-element> + <div class="delete-button-container"> + <span *ngIf="output.instanceUniqueId && !readonly" class="sprite-new delete-btn" + (click)="openDeleteModal(output)"></span> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less new file mode 100644 index 0000000000..56ed502943 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less @@ -0,0 +1,189 @@ +@import './../../../../../assets/styles/variables.less'; + +:host /deep/ output { + width: 100%; +} + +.output-attributes-table { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + text-align: left; + + .output-inner-cell-div { + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + height: 20px; + } + + + .table-header { + font-weight: bold; + border-top: #d2d2d2 solid 1px; + background-color: #eaeaea; + color: #191919; + + .table-cell { + font-size: 13px; + + .table-header-sort-arrow { + display: inline-block; + background-color: transparent; + border: none; + color: #AAA; + margin: 8px 0 0 5px; + + &.up { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid; + } + + &.down { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid; + } + } + } + + .output-value-col { + justify-content: flex-start; + padding: 10px; + } + } + + .table-header, .table-row { + display: flex; + flex-direction: row; + flex: 0 0 auto; + } + + .table-body { + display: flex; + flex-direction: column; + overflow-y: auto; + flex: 1; + + .no-data { + border: #d2d2d2 solid 1px; + border-top: none; + text-align: center; + height: 100%; + padding: 20px; + } + + /deep/ .selected { + background-color: #e6f6fb; + color: #009fdb; + } + } + + .table-row { + &:hover { + background-color: #f8f8f8; + cursor: pointer; + } + + &:last-child { + flex: 1 0 auto; + } + + .selected-row { + background-color: #e6f6fb; + } + } + + .table-cell { + font-size: 13px; + flex: 1; + border: #d2d2d2 solid 1px; + border-right: none; + border-top: none; + padding: 10px; + text-overflow: ellipsis; + white-space: nowrap; + + + &:last-child { + border-right: #d2d2d2 solid 1px; + } + + &.col-output-attribute-name { + flex: 1 0 130px; + max-width: 250px; + + justify-content: space-between; + + .property-name { + flex: 1; + } + + .property-description-icon { + float: right; + margin-top: 4px; + margin-left: 5px; + flex: 0 0 auto; + } + } + + &.col-output-attribute-type { + flex: 0 0 140px; + max-width: 140px; + } + + &.col-output-attribute-instance { + flex: 0 0 120px; + max-width: 120px; + } + + &.col-output-attribute-required { + flex: 0 0 80px; + max-width: 80px; + text-align: center; + } + + &.col-output-attribute-value { + .value-output { + flex: 1; + border: none; + background-color: inherit; + + &:focus, &:active { + border: none; + outline: none; + } + } + + .delete-btn { + flex: 0 0 auto; + } + + .delete-button-container { + max-height: 24px; + } + + &.inner-table-container { + padding: 0; + + .delete-button-container { + padding: 0 8px 0 0; + } + } + } + + &.output-value-col { + padding: 8px; + } + + } + + .filtered { + /deep/ .checkbox-label-content { + background-color: yellow; + } + } + +} diff --git a/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts new file mode 100644 index 0000000000..a7caeaa9fb --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts @@ -0,0 +1,141 @@ +/* +* ============LICENSE_START======================================================= +* Copyright (C) 2021 Nordix Foundation. 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. +* +* SPDX-License-Identifier: Apache-2.0 +* ============LICENSE_END========================================================= +*/ + +import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; +import {InstanceFeDetails} from "../../../../models/instance-fe-details"; +import {ModalService} from "../../../services/modal.service"; +import {DataTypeService} from "../../../services/data-type.service"; +import {TranslateService} from "../../../shared/translator/translate.service"; +import {InstanceFeAttributesMap} from "app/models/attributes-outputs/attribute-fe-map"; +import {Select} from "@ngxs/store"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import {OutputFEModel} from "app/models/attributes-outputs/output-fe-model"; +import {InputFEModel} from "../../../../models/properties-inputs/input-fe-model"; + +@Component({ + selector: 'outputs-table', + templateUrl: './outputs-table.component.html', + styleUrls: ['./outputs-table.component.less'] +}) +export class OutputsTableComponent implements OnInit { + @Select(WorkspaceState.isViewOnly) + isViewOnly$: boolean; + + @ViewChild('componentOutputsTable') + private table: any; + + @Input() outputs: Array<OutputFEModel>; + @Input() instanceNamesMap: Map<string, InstanceFeDetails>; + @Input() readonly: boolean; + @Input() isLoading: boolean; + @Input() componentType: string; + @Output() outputChanged: EventEmitter<any> = new EventEmitter<any>(); + @Output() deleteOutput: EventEmitter<any> = new EventEmitter<any>(); + @Input() feAttributesMap: InstanceFeAttributesMap; + + deleteMsgTitle: string; + deleteMsgBodyTxt: string; + modalDeleteBtn: string; + modalCancelBtn: string; + sortBy: string; + reverse: boolean; + selectedOutputToDelete: OutputFEModel; + + constructor(private modalService: ModalService, + private dataTypeService: DataTypeService, + private translateService: TranslateService) { + } + + ngOnInit() { + this.translateService.languageChangedObservable.subscribe((lang) => { + this.deleteMsgTitle = this.translateService.translate('DELETE_OUTPUT_TITLE'); + this.modalDeleteBtn = this.translateService.translate('MODAL_DELETE'); + this.modalCancelBtn = this.translateService.translate('MODAL_CANCEL'); + + }); + } + + sort = (sortBy) => { + this.reverse = (this.sortBy === sortBy) ? !this.reverse : true; + let reverse = this.reverse ? 1 : -1; + this.sortBy = sortBy; + let instanceNameMapTemp = this.instanceNamesMap; + let itemIdx1Val = ""; + let itemIdx2Val = ""; + this.outputs.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 { + itemIdx1Val = itemIdx1[sortBy]; + itemIdx2Val = itemIdx2[sortBy]; + } + if (itemIdx1Val < itemIdx2Val) { + return -1 * reverse; + } else if (itemIdx1Val > itemIdx2Val) { + return 1 * reverse; + } else { + return 0; + } + }); + }; + + onOutputChanged = (output, event) => { + output.updateDefaultValueObj(event.value, event.isValid); + this.outputChanged.emit(output); + }; + + onRequiredChanged = (output: OutputFEModel, event) => { + this.outputChanged.emit(output); + } + + onDeleteOutput = () => { + this.deleteOutput.emit(this.selectedOutputToDelete); + this.modalService.closeCurrentModal(); + }; + + openDeleteModal = (output: OutputFEModel) => { + this.selectedOutputToDelete = output; + this.modalService.createActionModal("Delete Output", "Are you sure you want to delete this output?", "Delete", this.onDeleteOutput, "Close").instance.open(); + } + + getConstraints(output: OutputFEModel): string[] { + if (output.outputPath) { + const pathValuesName = output.outputPath.split('#'); + const rootPropertyName = pathValuesName[0]; + const propertyName = pathValuesName[1]; + let filteredRootPropertyType = _.values(this.feAttributesMap)[0].filter(property => + property.name == rootPropertyName); + if (filteredRootPropertyType.length > 0) { + let rootPropertyType = filteredRootPropertyType[0].type; + return this.dataTypeService.getConstraintsByParentTypeAndUniqueID(rootPropertyType, propertyName); + } else { + return null; + } + + } else { + return null; + } + } + + checkInstanceFeAttributesMapIsFilled() { + return _.keys(this.feAttributesMap).length > 0 + } + +} diff --git a/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts b/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts index e499b3786b..b79bec0942 100644 --- a/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts +++ b/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts @@ -81,7 +81,6 @@ export class PropertiesTableComponent implements OnChanges { // Click on main row (row of propertyFEModel) onClickPropertyRow = (property: PropertyFEModel, instanceName: string, event?) => { - // event && event.stopPropagation(); this.selectedPropertyId = property.name; const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(property, instanceName); this.selectPropertyRow.emit(propertyRowSelectedEvent); diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html new file mode 100644 index 0000000000..9687d1e151 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html @@ -0,0 +1,66 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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========================================================= +--> + +<div class="attribute-creator"> + <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader> + <form class="w-sdc-form"> + + <div class="side-by-side"> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label required">Name</label> + <input class="i-sdc-form-input" + type="text" + name="propertyName" + data-tests-id="property-name" + [(ngModel)]="attributeModel.name" + [ngModelOptions]="{ debounce: 200 }" + autofocus/> + </div> + <!-- Type --> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label required">Type</label> + <ui-element-dropdown [testId]="'property-type'" + class="cell link-selector" + [values]="typesAttributes" + [(value)]="attributeModel.type"></ui-element-dropdown> + </div> + <div class="i-sdc-form-item attributeSchemaType" *ngIf="showSchema()"> + <label class="i-sdc-form-label required">Schema Type</label> + <ui-element-dropdown [testId]="'property-type'" + class="cell link-selector" + [values]="typesSchemaAttributes" + [(value)]="attributeModel.schema.property.type"></ui-element-dropdown> + </div> + </div> + + <!-- Description --> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label">Description</label> + <textarea class="i-sdc-form-textarea" + [pattern]="validation.commentValidationPattern" + name="propertyDescription" + [(ngModel)]="attributeModel.description" + [ngModelOptions]="{ debounce: 200 }" + data-tests-id="property-description" + ></textarea> + </div> + + </form> +</div> diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less new file mode 100644 index 0000000000..8b5152b06a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less @@ -0,0 +1,36 @@ +@import '../../../../../assets/styles/variables.less'; + +.attribute-creator { + font-family: @font-opensans-regular; + user-select: none; + padding-top: 12px; + padding-bottom: 20px; + + .i-sdc-form-label { + font-size: 12px; + } + + .w-sdc-form .i-sdc-form-item { + margin-bottom: 15px; + } + + .side-by-side { + display: flex; + + .i-sdc-form-item { + flex-basis: 100%; + + &:first-child { + flex-basis: 50%; + margin-right: 10px; + } + + } + + .attributeSchemaType { + margin-left: 10px; + } + } + + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts new file mode 100644 index 0000000000..5fc3d5b5ac --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {DataTypesMap} 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'; +import {AttributeBEModel} from "../../../../models/attributes-outputs/attribute-be-model"; +import {Validation} from "../../../../view-models/workspace/tabs/general/general-view-model"; + +@Component({ + selector: 'attribute-creator', + templateUrl: './attribute-creator.component.html', + styleUrls: ['./attribute-creator.component.less'], +}) + +export class AttributeCreatorComponent { + + validation:Validation; + typesAttributes: DropdownValue[]; + typesSchemaAttributes: DropdownValue[]; + attributeModel: AttributeBEModel; + dataTypes: DataTypesMap; + isLoading: boolean; + + constructor(protected dataTypeService: DataTypeService) { + } + + ngOnInit() { + this.attributeModel = new AttributeBEModel(); + this.attributeModel.type = ''; + this.attributeModel.schema.property.type = ''; + 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.typesAttributes = _.map(PROPERTY_DATA.TYPES, + (type: string) => new DropdownValue(type, type) + ); + const typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, + (type: string) => new DropdownValue(type, type) + ); + const nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, + (type: string) => new DropdownValue(type, + type.replace('org.openecomp.datatypes.heat.', '')) + ) + .sort((a, b) => a.label.localeCompare(b.label)); + this.typesAttributes = _.concat(this.typesAttributes, nonPrimitiveTypesValues); + this.typesSchemaAttributes = _.concat(typesSimpleProperties, nonPrimitiveTypesValues); + this.typesAttributes.unshift(new DropdownValue('', 'Select Type...')); + this.typesSchemaAttributes.unshift(new DropdownValue('', 'Select Schema Type...')); + + } + + checkFormValidForSubmit() { + const showSchema: boolean = this.showSchema(); + const isSchemaValid: boolean = (!(showSchema && !this.attributeModel.schema.property.type)); + if (!showSchema) { + this.attributeModel.schema.property.type = ''; + } + return this.attributeModel.name && this.attributeModel.type && isSchemaValid; + } + + showSchema(): boolean { + return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.attributeModel.type) > -1; + } + + onSchemaTypeChange(): void { + if (this.attributeModel.type === PROPERTY_TYPES.MAP) { + this.attributeModel.value = JSON.stringify({'': null}); + } else if (this.attributeModel.type === PROPERTY_TYPES.LIST) { + this.attributeModel.value = JSON.stringify([]); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts new file mode 100644 index 0000000000..5779731b63 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {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 {AttributeCreatorComponent} from './attribute-creator.component'; + +@NgModule({ + declarations: [ + AttributeCreatorComponent, + ], + imports: [ + CommonModule, + FormsModule, + FormElementsModule, + UiElementsModule, + TranslateModule + ], + exports: [], + entryComponents: [ + AttributeCreatorComponent + ], + providers: [] +}) + +export class AttributeCreatorModule { +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts new file mode 100644 index 0000000000..5543832934 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts @@ -0,0 +1,65 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {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 {TranslateModule} from "../../shared/translator/translate.module"; +import {TopologyTemplateService} from "../../services/component-services/topology-template.service"; +import {AttributesOutputsComponent} from "./attributes-outputs.page.component"; +import {TabModule} from "../../components/ui/tabs/tabs.module"; +import {UiElementsModule} from "../../components/ui/ui-elements.module" +import {HierarchyNavigationModule} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.module"; +import {AttributesService} from "../../services/attributes.service"; +import {HierarchyNavService} from "./services/hierarchy-nav.service"; +import {AttributesUtils} from "./services/attributes.utils"; +import {OutputsUtils} from "./services/outputs.utils"; +import { OutputsTableComponent } from "app/ng2/components/logic/outputs-table/outputs-table.component"; +import {AttributeTableModule} from "../../components/logic/attributes-table/attribute-table.module"; + +@NgModule({ + declarations: [ + AttributesOutputsComponent, + OutputsTableComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule, + TabModule, + HierarchyNavigationModule, + UiElementsModule, + TranslateModule, + AttributeTableModule + ], + exports: [ + AttributesOutputsComponent + ], + entryComponents: [ + AttributesOutputsComponent + ], + providers: [TopologyTemplateService, AttributesService, HierarchyNavService, AttributesUtils, OutputsUtils] +}) + +export class AttributesOutputsModule { +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html new file mode 100644 index 0000000000..778d5458c6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html @@ -0,0 +1,103 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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========================================================= +--> + +<div class="attributes-outputs-page"> + + <div class="main-content"> + <div class="left-column"> + <div class="main-tabs-section"> + <tabs #attributeOutputTabs tabStyle="round-tabs" (tabChanged)="tabChanged($event)" + [hideIndicationOnTabChange]="true"> + <tab tabTitle="Attributes"> + <attributes-table class="attributes-table" + [feAttributesMap]="instanceFeAttributesMap" + [feInstanceNamesMap]="componentInstanceNamesMap" + [selectedAttributeId]="selectedFlatAttribute.path" + [readonly]="isReadonly || resourceIsReadonly" + [isLoading]="loadingAttributes || savingChangedData" + [hasDeclareOption]="true" + (attributeChanged)="dataChanged($event)" + (selectAttributeRow)="selectAttributeRow($event)" + (updateCheckedAttributeCount)="updateCheckedAttributeCount($event)"> + </attributes-table> + </tab> + + <tab tabTitle="Outputs"> + <outputs-table class="attributes-table" + [feAttributesMap]="instanceFeAttributesMap" + [readonly]="isReadonly" + [outputs]="outputs | searchFilter:'name':searchQuery" + [instanceNamesMap]="componentInstanceNamesMap" + [isLoading]="loadingOutputs" + [componentType]="component.componentType" + (deleteOutput)="deleteOutput($event)" + (outputChanged)="dataChanged($event)"> + </outputs-table> + </tab> + </tabs> + <div class="header"> + <button class="tlv-btn blue declare-button" [disabled]="!checkedAttributesCount || isReadonly || hasChangedData" (click)="declareAttributes()" data-tests-id="declare-button declare-output">Declare Output</button> + </div> + </div> + </div> + <div class="right-column"> + <tabs #hierarchyNavTabs tabStyle="simple-tabs" class="gray-border"> + <tab tabTitle="Composition"> + <div class="hierarchy-nav-container"> + <loader [display]="loadingInstances" [size]="'medium'" [relative]="true" + [loaderDelay]="500"></loader> + <div class="hierarchy-header white-sub-header"> + <span tooltip="{{component.name}}">{{component.name}}</span> + </div> + <div + *ngIf="!instancesNavigationData || instancesNavigationData.length === 0 || isOutputsTabSelected"> + No data to display + </div> + <hierarchy-navigation class="hierarchy-nav" + (updateSelected)="onInstanceSelectedUpdate($event)" + [displayData]="isOutputsTabSelected ? []: instancesNavigationData" + [selectedItem]="selectedInstanceData?.uniqueId" + [displayOptions]="hierarchyInstancesDisplayOptions"></hierarchy-navigation> + </div> + </tab> + <tab tabTitle="Attribute Structure"> + <div class="hierarchy-nav-container"> + <div class="hierarchy-header white-sub-header" + [class.selected]="selectedFlatAttribute.path == attributeStructureHeader"> + <span + tooltip="{{isAttributesTabSelected ? attributeStructureHeader : ''}}">{{isAttributesTabSelected + ? (attributeStructureHeader || "No Attribute Selected") + : "No Attribute Selected"}}</span> + </div> + <div + *ngIf="!attributesNavigationData || attributesNavigationData.length === 0 || isOutputsTabSelected "> + No data to display + </div> + <hierarchy-navigation class="hierarchy-nav" + (updateSelected)="onAttributeSelectedUpdate($event)" + [displayData]="isOutputsTabSelected ? [] : attributesNavigationData" + [selectedItem]="selectedFlatAttribute.path" + [displayOptions]="hierarchyAttributesDisplayOptions"></hierarchy-navigation> + </div> + </tab> + </tabs> + </div> + </div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less new file mode 100644 index 0000000000..386d243976 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less @@ -0,0 +1,234 @@ +@import '../../../../assets/styles/variables'; +@import '../../../../assets/styles/mixins'; + +@ng2-shadow-gray: #f8f8f8; +@ng2-light-gray: #eaeaea; +@ng2-medium-gray: #d2d2d2; +@ng2-med-dark-gray: #999999; +@ng2-dark-gray: #5a5a5a; +@ng2-shadow-blue: #e6f6fb; +@ng2-bold-blue: #009fdb; +@ng2-success-green: #4ca90c; +@ng2-title-font-size: 16px; +@ng2-text-font-size: 14px; + +:host { + display: block; + height: 100%; +} + +/deep/ tabs { + display: flex; + flex-direction: column; + height: 100%; +} + +.attributes-outputs-page { + height: 100%; + font-family: @font-opensans-regular; + + .main-content { + display: flex; + flex-direction: row; + height: 100%; + } + + .left-column { + flex: 1 0 500px; + position: relative; + min-width: 930px; + + /deep/ .tabs { + width: 33%; + text-align: center; + } + + /deep/ .tab { + padding: 12px; + flex: 1; + font-family: @font-opensans-regular; + + &.active { + color: #009fdb; + border-color: #d2d2d2; + border-top: solid 4px #009fdb; + background-color: white; + padding-top: 9px; + font-family: @font-opensans-medium; + } + + .tab-indication { + background-color: #4ca90c; + border: solid 2px #fff; + border-radius: 50%; + font-size: 12px; + } + } + + .header { + position: absolute; + top: 0; + right: 0; + display: flex; + } + + .search-filter-container { + display: flex; + flex-direction: row; + + .search-box { + border: 1px solid @ng2-medium-gray; + border-radius: 3px; + height: 32px; + margin: 0; + padding: 2px 20px 4px 10px; + outline: none; + font-style: italic; + color: @ng2-med-dark-gray; + width: auto; + + &::-moz-placeholder { + color: @ng2-med-dark-gray; + } + + &::-webkit-input-placeholder { + color: @ng2-med-dark-gray; + } + } + + .search-icon { + background-position: -48px -3137px; + width: 14px; + height: 14px; + position: absolute; + left: 170px; + top: 8px; + } + + &.without-filter { + margin-right: 10px; + + .search-icon { + right: 4px; + } + } + + } + + .clear-filter { + cursor: pointer; + color: #009fdb; + font-family: @font-opensans-medium-italic; + text-decoration: underline; + padding-right: 10px; + font-size: 12px; + } + + .declare-button { + &:not(:last-of-type) { + margin-right: 10px; + } + } + + .main-tabs-section { + position: relative; + + .main-tabs-section-buttons { + position: absolute; + top: 45px; + right: 0; + padding: 4px; + } + } + } + + .right-column { + display: flex; + flex: 0 0 350px; + flex-direction: column; + margin: 0px 0 0 1em; + overflow-x: auto; + + .add-btn { + + align-self: flex-end; + margin-top: 10px; + margin-bottom: 19px; + } + + /deep/ .tabs { + border-bottom: solid 1px #d0d0d0; + } + + /deep/ .tab { + flex: none; + padding: 8px 20px 0; + font-size: 14px; + line-height: 30px; + font-family: @font-opensans-regular; + + &.active { + font-family: @font-opensans-medium; + } + } + } + + .hierarchy-tabs { + flex: 0 0 40px; + } + + .gray-border { + border: 1px solid #ddd; + } + + /deep/ .white-sub-header { + background-color: #fffefe; + box-shadow: 0px 1px 0.99px 0.01px rgba(34, 31, 31, 0.15); + border-bottom: #d2d2d2 solid 1px; + font-size: 14px; + text-align: left; + flex: 0 0 auto; + padding: 10px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + .a_13_r; + text-transform: uppercase; + + &.hierarchy-header { + padding-left: 20px; + + &.selected { + background-color: #e6f6fb; + } + } + + } + + .hierarchy-nav-container { + display: flex; + flex-direction: column; + height: 100%; + } + + .hierarchy-header { + + span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 290px; + } + } + + .hierarchy-nav { + flex: 1; + overflow: auto; + display: grid; + margin-top: 1em; + margin-left: 1em; + font-size: 12px; + padding-top: 1em; + } +} + diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts new file mode 100644 index 0000000000..d7db8f3c82 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts @@ -0,0 +1,719 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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, ViewChild} from '@angular/core'; +import { + ButtonModel, + Component as ComponentData, + ComponentInstance, + ModalModel, + ToscaPresentationData +} from 'app/models'; +import {SdcUiCommon, SdcUiServices} from 'onap-ui-angular'; +import {TopologyTemplateService} from "../../services/component-services/topology-template.service"; +import {Tab, Tabs} from "../../components/ui/tabs/tabs.component"; +import * as _ from 'lodash'; +import {OutputFEModel} from "../../../models/attributes-outputs/output-fe-model"; +import {OutputBEModel} from "../../../models/attributes-outputs/output-be-model"; +import {EVENTS, ResourceType, WorkspaceMode} from "../../../utils/constants"; +import {ComponentModeService} from "../../services/component-services/component-mode.service"; +import {EventListenerService} from "app/services"; +import {HierarchyNavService} from "./services/hierarchy-nav.service"; +import {ComponentServiceNg2} from "../../services/component-services/component.service"; +import {ComponentInstanceServiceNg2} from "../../services/component-instance-services/component-instance.service"; +import {KeysPipe} from "../../pipes/keys.pipe"; +import { + InstanceAttributesAPIMap, + InstanceBeAttributesMap, + InstanceFeAttributesMap +} from "app/models/attributes-outputs/attribute-fe-map"; +import {ModalService} from "../../services/modal.service"; +import {InstanceFeDetails} from "../../../models/instance-fe-details"; +import {HierarchyDisplayOptions} from "../../components/logic/hierarchy-navigtion/hierarchy-display-options"; +import {UnsavedChangesComponent} from "../../components/ui/forms/unsaved-changes/unsaved-changes.component"; +import {SimpleFlatAttribute} from "app/models/attributes-outputs/simple-flat-attribute"; +import {AttributeFEModel} from "../../../models/attributes-outputs/attribute-fe-model"; +import {AttributesUtils} from "./services/attributes.utils"; +import {OutputsUtils} from "app/ng2/pages/attributes-outputs/services/outputs.utils"; +import {AttributesService} from "app/ng2/services/attributes.service"; +import {DerivedFEAttribute} from "../../../models/attributes-outputs/derived-fe-attribute"; +import {AttributeBEModel} from "../../../models/attributes-outputs/attribute-be-model"; +import {AttributeCreatorComponent} from "app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component"; +import {AttributeRowSelectedEvent} from "app/ng2/components/logic/attributes-table/attributes-table.component"; + +const SERVICE_SELF_TITLE = "SELF"; + +@Component({ + selector: 'attributes-outputs', + templateUrl: './attributes-outputs.page.component.html', + styleUrls: ['./attributes-outputs.page.component.less', '../../../../assets/styles/table-style.less'] +}) +export class AttributesOutputsComponent { + title = "Attributes & Outputs"; + + @ViewChild('componentAttributesTable') + private table: any; + + component: ComponentData; + componentInstanceNamesMap: Map<string, InstanceFeDetails> = new Map<string, InstanceFeDetails>();//instanceUniqueId, {name, iconClass} + + attributesNavigationData = []; + instancesNavigationData = []; + + instanceFeAttributesMap: InstanceFeAttributesMap; + outputs: Array<OutputFEModel> = []; + instances: Array<ComponentInstance> = []; + searchQuery: string; + attributeStructureHeader: string; + + selectedFlatAttribute: SimpleFlatAttribute = new SimpleFlatAttribute(); + selectedInstanceData: ComponentInstance = null; + checkedAttributesCount: number = 0; + + hierarchyAttributesDisplayOptions: HierarchyDisplayOptions = new HierarchyDisplayOptions('path', 'name', 'childrens'); + hierarchyInstancesDisplayOptions: HierarchyDisplayOptions = new HierarchyDisplayOptions('uniqueId', 'name', 'archived', null, 'iconClass'); + searchAttributeName: string; + currentMainTab: Tab; + isOutputsTabSelected: boolean; + isAttributesTabSelected: boolean; + isReadonly: boolean; + resourceIsReadonly: boolean; + loadingInstances: boolean = false; + loadingOutputs: boolean = false; + loadingAttributes: boolean = false; + changedData: Array<AttributeFEModel | OutputFEModel>; + hasChangedData: boolean; + isValidChangedData: boolean; + savingChangedData: boolean; + stateChangeStartUnregister: Function; + serviceBeAttributesMap: InstanceBeAttributesMap; + + @ViewChild('hierarchyNavTabs') hierarchyNavTabs: Tabs; + @ViewChild('attributeOutputTabs') attributeOutputTabs: Tabs; + + + constructor(private attributesService: AttributesService, + private hierarchyNavService: HierarchyNavService, + private attributesUtils: AttributesUtils, + private outputsUtils: OutputsUtils, + private componentServiceNg2: ComponentServiceNg2, + private componentInstanceServiceNg2: ComponentInstanceServiceNg2, + @Inject("$stateParams") _stateParams, + @Inject("$scope") private $scope: ng.IScope, + @Inject("$state") private $state: ng.ui.IStateService, + @Inject("Notification") private Notification: any, + private componentModeService: ComponentModeService, + private EventListenerService: EventListenerService, + private ModalServiceSdcUI: SdcUiServices.ModalService, + private ModalService: ModalService, + private keysPipe: KeysPipe, + private topologyTemplateService: TopologyTemplateService) { + + this.instanceFeAttributesMap = new InstanceFeAttributesMap(); + /* 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; + this.EventListenerService.registerObserverCallback(EVENTS.ON_LIFECYCLE_CHANGE, this.onCheckout); + this.updateViewMode(); + + this.changedData = []; + this.updateHasChangedData(); + this.isValidChangedData = true; + } + + ngOnInit() { + this.loadingOutputs = true; + this.loadingInstances = true; + this.loadingAttributes = true; + this.topologyTemplateService + .getComponentOutputsWithAttributes(this.component.componentType, this.component.uniqueId) + .subscribe(response => { + if (response.outputs) { + response.outputs.forEach(output => { + const newOutput: OutputFEModel = new OutputFEModel(output); + this.outputsUtils.resetOutputDefaultValue(newOutput, output.defaultValue); + this.outputs.push(newOutput); + }); + } + }, error => { + this.Notification.error({ + message: 'Failed to Initialize:' + error, + title: 'Failure' + }); + }, () => { + this.loadingOutputs = false; + }); + this.componentServiceNg2 + .getComponentResourceAttributesData(this.component) + .subscribe(response => { + this.instances = []; + this.instances.push(...response.componentInstances); + + // add the service self instance to the top of the list. + const serviceInstance = new ComponentInstance(); + serviceInstance.name = SERVICE_SELF_TITLE; + serviceInstance.uniqueId = this.component.uniqueId; + this.instances.unshift(serviceInstance); + if (this.instances) { + this.instances.forEach(instance => { + this.instancesNavigationData.push(instance); + this.componentInstanceNamesMap[instance.uniqueId] = <InstanceFeDetails>{ + name: instance.name, + iconClass: instance.iconClass, + originArchived: instance.originArchived + }; + }); + } + this.selectFirstInstanceByDefault(); + }, (error) => { + this.Notification.error({ + message: 'Failed to Initialize:' + error, + title: 'Failure' + }); + }, () => { + this.loadingInstances = false; + this.loadingAttributes = false; + }); + + this.stateChangeStartUnregister = this.$scope.$on('$stateChangeStart', (event, toState, toParams) => { + // stop if has changed attributes + if (this.hasChangedData) { + event.preventDefault(); + this.showUnsavedChangesAlert().then(() => { + this.$state.go(toState, toParams); + }, () => { + }); + } + }); + } + + ngOnDestroy() { + this.EventListenerService.unRegisterObserver(EVENTS.ON_LIFECYCLE_CHANGE); + this.stateChangeStartUnregister(); + } + + selectFirstInstanceByDefault = () => { + if (this.instancesNavigationData.length > 0) { + this.onInstanceSelectedUpdate(this.instancesNavigationData[0]); + } + }; + + updateViewMode = () => { + this.isReadonly = this.componentModeService.getComponentMode(this.component) === WorkspaceMode.VIEW; + } + + onCheckout = (component: ComponentData) => { + this.component = component; + this.updateViewMode(); + } + + isSelf = (): boolean => { + return this.selectedInstanceData && this.selectedInstanceData.uniqueId == this.component.uniqueId; + } + + getServiceAttributes() { + this.loadingAttributes = true; + this.topologyTemplateService + .getServiceAttributes(this.component.uniqueId) + .subscribe((response) => { + this.serviceBeAttributesMap = new InstanceBeAttributesMap(); + this.serviceBeAttributesMap[this.component.uniqueId] = response; + this.processInstanceAttributesResponse(this.serviceBeAttributesMap, false); + }, (error) => { + this.Notification.error({ + message: 'Failed to get Service Attribute:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } + + onInstanceSelectedUpdate = (instance: ComponentInstance) => { + // stop if has changed attributes + if (this.hasChangedData) { + this.showUnsavedChangesAlert().then(() => { + this.changeSelectedInstance(instance) + }); + return; + } + this.changeSelectedInstance(instance); + }; + + changeSelectedInstance = (instance: ComponentInstance) => { + this.selectedInstanceData = instance; + this.loadingAttributes = true; + if (instance instanceof ComponentInstance) { + let instanceBeAttributesMap: InstanceBeAttributesMap = new InstanceBeAttributesMap(); + if (this.isOutput(instance.originType)) { + this.componentInstanceServiceNg2 + .getComponentInstanceOutputs(this.component, instance) + .subscribe(response => { + instanceBeAttributesMap[instance.uniqueId] = response; + this.processInstanceAttributesResponse(instanceBeAttributesMap, true); + }, error => { + this.Notification.error({ + message: 'Failed to change Selected Instance:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } else if (this.isSelf()) { + this.getServiceAttributes(); + } else { + this.componentInstanceServiceNg2 + .getComponentInstanceAttributes(this.component, instance.uniqueId) + .subscribe(response => { + instanceBeAttributesMap[instance.uniqueId] = response; + this.processInstanceAttributesResponse(instanceBeAttributesMap, false); + }, error => { + this.Notification.error({ + message: 'Failed to change Selected Instance:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } + + this.resourceIsReadonly = (instance.componentName === "vnfConfiguration"); + } else { + this.loadingAttributes = false; + } + + //clear selected attribute from the navigation + this.selectedFlatAttribute = new SimpleFlatAttribute(); + this.attributesNavigationData = []; + }; + + /** + * Entry point handling response from server + */ + processInstanceAttributesResponse = (instanceBeAttributesMap: InstanceBeAttributesMap, originTypeIsVF: boolean) => { + this.instanceFeAttributesMap = this.attributesUtils.convertAttributesMapToFEAndCreateChildren(instanceBeAttributesMap, originTypeIsVF, this.outputs); //create flattened children, disable declared attribs, and init values + this.checkedAttributesCount = 0; + }; + + + /*** VALUE CHANGE EVENTS ***/ + dataChanged = (item: AttributeFEModel | OutputFEModel) => { + let itemHasChanged; + if (this.isAttributesTabSelected && item instanceof AttributeFEModel) { + itemHasChanged = item.hasValueObjChanged(); + } else if (this.isOutputsTabSelected && item instanceof OutputFEModel) { + itemHasChanged = item.hasChanged(); + } + + const dataChangedIdx = this.changedData.findIndex((changedItem) => changedItem === item); + if (itemHasChanged) { + if (dataChangedIdx === -1) { + this.changedData.push(item); + } + } else { + if (dataChangedIdx !== -1) { + this.changedData.splice(dataChangedIdx, 1); + } + } + + if (this.isAttributesTabSelected) { + this.isValidChangedData = this.changedData.every((changedItem) => (<AttributeFEModel>changedItem).valueObjIsValid); + } else if (this.isOutputsTabSelected) { + this.isValidChangedData = this.changedData.every((changedItem) => (<OutputFEModel>changedItem).defaultValueObjIsValid); + } + this.updateHasChangedData(); + }; + + + /*** HEIRARCHY/NAV RELATED FUNCTIONS ***/ + + /** + * Handle select node in navigation area, and select the row in table + */ + onAttributeSelectedUpdate = ($event) => { + this.selectedFlatAttribute = $event; + let parentAttribute: AttributeFEModel = this.attributesService.getParentAttributeFEModelFromPath(this.instanceFeAttributesMap[this.selectedFlatAttribute.instanceName], this.selectedFlatAttribute.path); + parentAttribute.expandedChildAttributeId = this.selectedFlatAttribute.path; + }; + + /** + * When user select row in table, this will prepare the hierarchy object for the tree. + */ + selectAttributeRow = (attributeRowSelectedEvent: AttributeRowSelectedEvent) => { + let attribute = attributeRowSelectedEvent.attributeModel; + let instanceName = attributeRowSelectedEvent.instanceName; + this.attributeStructureHeader = null; + + // Build hierarchy tree for the navigation and update attributesNavigationData with it. + if (!(this.selectedInstanceData instanceof ComponentInstance) || this.selectedInstanceData.originType !== ResourceType.VF) { + let simpleFlatAttributes: Array<SimpleFlatAttribute>; + if (attribute instanceof AttributeFEModel) { + simpleFlatAttributes = this.hierarchyNavService.getSimpleAttributesTree(attribute, instanceName); + } else if (attribute instanceof DerivedFEAttribute) { + // Need to find parent AttributeFEModel + let parentAttributeFEModel: AttributeFEModel = _.find(this.instanceFeAttributesMap[instanceName], (tmpFeAttribute): boolean => { + return attribute.attributesName.indexOf(tmpFeAttribute.name) === 0; + }); + simpleFlatAttributes = this.hierarchyNavService.getSimpleAttributesTree(parentAttributeFEModel, instanceName); + } + this.attributesNavigationData = simpleFlatAttributes; + } + + // Update the header in the navigation tree with attribute name. + this.attributeStructureHeader = (attribute.attributesName.split('#'))[0]; + + // Set selected attribute in table + this.selectedFlatAttribute = this.hierarchyNavService.createSimpleFlatAttribute(attribute, instanceName); + this.hierarchyNavTabs.triggerTabChange('Attribute Structure'); + }; + + tabChanged = (event) => { + // stop if has changed attributes + if (this.hasChangedData) { + this.attributeOutputTabs.triggerTabChange(this.currentMainTab.title); + this.showUnsavedChangesAlert().then(() => { + this.attributeOutputTabs.selectTab(this.attributeOutputTabs.tabs.find((tab) => tab.title === event.title)); + }, () => { + }); + return; + } + + this.currentMainTab = this.attributeOutputTabs.tabs.find((tab) => tab.title === event.title); + this.isAttributesTabSelected = this.currentMainTab.title === "Attributes"; + this.isOutputsTabSelected = this.currentMainTab.title === "Outputs"; + this.attributeStructureHeader = null; + this.searchQuery = ''; + }; + + + /*** DECLARE ATTRIBUTES/OUTPUTS ***/ + declareAttributes = (): void => { + let selectedComponentInstancesAttributes: InstanceBeAttributesMap = new InstanceBeAttributesMap(); + let selectedComponentInstancesOutputs: InstanceBeAttributesMap = new InstanceBeAttributesMap(); + let instancesIds = this.keysPipe.transform(this.instanceFeAttributesMap, []); + + angular.forEach(instancesIds, (instanceId: string): void => { + let selectedInstanceData: any = this.instances.find(instance => instance.uniqueId == instanceId); + if (selectedInstanceData instanceof ComponentInstance) { + if (!this.isOutput(selectedInstanceData.originType)) { + // convert Attribute FE model -> Attribute BE model, extract only checked + selectedComponentInstancesAttributes[instanceId] = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId]); + } else { + selectedComponentInstancesOutputs[instanceId] = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId]); + } + } + }); + + let outputsToCreate: InstanceAttributesAPIMap = new InstanceAttributesAPIMap(selectedComponentInstancesOutputs, selectedComponentInstancesAttributes); + this.topologyTemplateService + .createOutput(this.component, outputsToCreate, this.isSelf()) + .subscribe((response) => { + this.setOutputTabIndication(response.length); + this.checkedAttributesCount = 0; + response.forEach((output: OutputBEModel) => { + const newOutput: OutputFEModel = new OutputFEModel(output); + this.outputsUtils.resetOutputDefaultValue(newOutput, output.defaultValue); + this.outputs.push(newOutput); + this.updateAttributeValueAfterDeclare(newOutput); + }); + }); + }; + + saveChangedData = (): Promise<(AttributeBEModel | OutputBEModel)[]> => { + return new Promise((resolve, reject) => { + if (!this.isValidChangedData) { + reject('Changed data is invalid - cannot save!'); + return; + } + if (!this.changedData.length) { + resolve([]); + return; + } + + // make request and its handlers + let request; + let handleSuccess, handleError; + if (this.isAttributesTabSelected) { + this.changedData.map((changedAttrib) => { + changedAttrib = <AttributeFEModel>changedAttrib; + const attribBE = new AttributeBEModel(changedAttrib); + attribBE.toscaPresentation = new ToscaPresentationData(); + attribBE.toscaPresentation.ownerId = changedAttrib.parentUniqueId; + attribBE.value = changedAttrib.getJSONValue(); + attribBE.name = changedAttrib.origName || changedAttrib.name; + delete attribBE.origName; + return attribBE; + }); + } else if (this.isOutputsTabSelected) { + const changedOutputs: OutputBEModel[] = this.changedData.map((changedOutput) => { + changedOutput = <OutputFEModel>changedOutput; + const outputBE = new OutputBEModel(changedOutput); + outputBE.defaultValue = changedOutput.getJSONDefaultValue(); + return outputBE; + }); + request = this.componentServiceNg2 + .updateComponentOutputs(this.component, changedOutputs); + handleSuccess = (response) => { + // reset each changed attribute with new value and remove it from changed attributes list + response.forEach((resOutput) => { + const changedOutput = <OutputFEModel>this.changedData.shift(); + this.outputsUtils.resetOutputDefaultValue(changedOutput, resOutput.defaultValue); + changedOutput.required = resOutput.required; + }); + } + this.savingChangedData = true; + request.subscribe( + (response) => { + this.savingChangedData = false; + handleSuccess && handleSuccess(response); + this.updateHasChangedData(); + resolve(response); + }, + (error) => { + this.savingChangedData = false; + handleError && handleError(error); + this.updateHasChangedData(); + reject(error); + } + ); + } + + }); + }; + + + reverseChangedData = (): void => { + // make reverse item handler + let handleReverseItem; + if (this.isAttributesTabSelected) { + handleReverseItem = (changedItem) => { + changedItem = <AttributeFEModel>changedItem; + this.attributesUtils.resetAttributeValue(changedItem, changedItem.value); + this.checkedAttributesCount = 0; + }; + } else if (this.isOutputsTabSelected) { + handleReverseItem = (changedItem) => { + changedItem = <OutputFEModel>changedItem; + this.outputsUtils.resetOutputDefaultValue(changedItem, changedItem.defaultValue); + changedItem.required = changedItem.requiredOrig; + }; + } + + this.changedData.forEach(handleReverseItem); + this.changedData = []; + this.updateHasChangedData(); + }; + + updateHasChangedData = (): boolean => { + const curHasChangedData: boolean = (this.changedData.length > 0); + if (curHasChangedData !== this.hasChangedData) { + this.hasChangedData = curHasChangedData; + if (this.hasChangedData) { + this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_UNSAVED_CHANGES, this.hasChangedData, this.showUnsavedChangesAlert); + } else { + this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_UNSAVED_CHANGES, false); + } + } + return this.hasChangedData; + }; + + doSaveChangedData = (onSuccessFunction?: Function, onError?: Function): void => { + this.saveChangedData().then( + () => { + this.Notification.success({ + message: 'Successfully saved changes', + title: 'Saved' + }); + if (onSuccessFunction) onSuccessFunction(); + if (this.isAttributesTabSelected) { + this.checkedAttributesCount = 0; + } + }, + () => { + this.Notification.error({ + message: 'Failed to save changes!', + title: 'Failure' + }); + if (onError) onError(); + } + ); + }; + + showUnsavedChangesAlert = (): Promise<any> => { + let modalTitle: string; + if (this.isAttributesTabSelected) { + modalTitle = `Unsaved attributes for ${this.selectedInstanceData.name}`; + } else if (this.isOutputsTabSelected) { + modalTitle = `Unsaved outputs for ${this.component.name}`; + } + + return new Promise<any>((resolve, reject) => { + this.ModalServiceSdcUI.openCustomModal( + { + title: modalTitle, + size: 'sm', + type: SdcUiCommon.ModalType.custom, + testId: "navigate-modal", + + buttons: [ + { + 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}); + }); + + } + + updateAttributeValueAfterDeclare = (output: OutputFEModel) => { + const attributeList = this.instanceFeAttributesMap[output.instanceUniqueId]; + if (attributeList) { + const instanceName = output.instanceUniqueId.slice(output.instanceUniqueId.lastIndexOf('.') + 1); + const attributeForUpdatingVal = attributeList.find((feAttribute: AttributeFEModel) => { + return feAttribute.name == output.relatedAttributeName && + (feAttribute.name == output.relatedAttributeName || output.name === instanceName.concat('_').concat(feAttribute.name.replace(/[.]/g, '_'))); + }); + const outputPath = (output.outputPath && output.outputPath != attributeForUpdatingVal.name) ? output.outputPath : undefined; + attributeForUpdatingVal.setAsDeclared(outputPath); //set attribute as declared before assigning value + // this.attributesService.disableRelatedAttributes(attributeForUpdatingVal, outputPath); + this.attributesUtils.resetAttributeValue(attributeForUpdatingVal, output.relatedAttributeValue, outputPath); + } + } + + //used for declare button, to keep count of newly checked attributes (and ignore declared attributes) + updateCheckedAttributeCount = (increment: boolean): void => { + this.checkedAttributesCount += (increment) ? 1 : -1; + }; + + setOutputTabIndication = (numOutputs: number): void => { + this.attributeOutputTabs.setTabIndication('Outputs', numOutputs); + }; + + + resetUnsavedChangesForOutput = (output: OutputFEModel) => { + this.outputsUtils.resetOutputDefaultValue(output, output.defaultValue); + this.changedData = this.changedData.filter((changedItem) => changedItem.uniqueId !== output.uniqueId); + this.updateHasChangedData(); + } + + deleteOutput = (output: OutputFEModel) => { + //reset any unsaved changes to the output before deleting it + this.resetUnsavedChangesForOutput(output); + + let outputToDelete = new OutputBEModel(output); + + this.componentServiceNg2 + .deleteOutput(this.component, outputToDelete) + .subscribe(response => { + this.outputs = this.outputs.filter(output => output.uniqueId !== response.uniqueId); + + //Reload the whole instance for now - TODO: CHANGE THIS after the BE starts returning attributes within the response, use commented code below instead! + this.changeSelectedInstance(this.selectedInstanceData); + }, error => { + this.Notification.error({ + message: 'Failed to delete Output:' + error, + title: 'Failure' + }); + }); + }; + + deleteAttribute = (attribute: AttributeFEModel) => { + const attributeToDelete = new AttributeFEModel(attribute); + this.loadingAttributes = true; + const feMap = this.instanceFeAttributesMap; + this.topologyTemplateService + .deleteServiceAttribute(this.component.uniqueId, attributeToDelete) + .subscribe((response) => { + const attribs = feMap[this.component.uniqueId]; + attribs.splice(attribs.findIndex(p => p.uniqueId === response), 1); + }, (error) => { + this.Notification.error({ + message: 'Failed to delete Attribute:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } + + addAttribute = () => { + let modalTitle = 'Add Attribute'; + let modal = this.ModalService.createCustomModal(new ModalModel( + 'sm', + modalTitle, + null, + [ + new ButtonModel('Save', 'blue', () => { + modal.instance.dynamicContent.instance.isLoading = true; + const newAttribute: AttributeBEModel = modal.instance.dynamicContent.instance.attributeModel; + this.topologyTemplateService.createServiceAttribute(this.component.uniqueId, newAttribute) + .subscribe((response) => { + modal.instance.dynamicContent.instance.isLoading = false; + const newAttrib: AttributeFEModel = this.attributesUtils.convertAddAttributeBEToAttributeFE(response); + this.instanceFeAttributesMap[this.component.uniqueId].push(newAttrib); + modal.instance.close(); + }, (error) => { + modal.instance.dynamicContent.instance.isLoading = false; + this.Notification.error({ + message: 'Failed to add Attribute:' + error, + title: 'Failure' + }); + }); + }, () => !modal.instance.dynamicContent.instance.checkFormValidForSubmit()), + new ButtonModel('Cancel', 'outline grey', () => { + modal.instance.close(); + }), + ], + null + )); + this.ModalService.addDynamicContentToModal(modal, AttributeCreatorComponent, {}); + modal.instance.open(); + } + + private isOutput = (instanceType: string): boolean => { + return instanceType === ResourceType.VF || instanceType === ResourceType.PNF || instanceType === ResourceType.CVFC || instanceType === ResourceType.CR; + } + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts new file mode 100644 index 0000000000..51b1823ce2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts @@ -0,0 +1,198 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 } from '@angular/core'; +import { DataTypeService } from "app/ng2/services/data-type.service"; +import { PROPERTY_TYPES } from "app/utils"; +import { AttributesService } from "app/ng2/services/attributes.service"; +import { InstanceBeAttributesMap, InstanceFeAttributesMap } from "app/models/attributes-outputs/attribute-fe-map"; +import {OutputFEModel} from "../../../../models/attributes-outputs/output-fe-model"; +import { AttributeBEModel, DerivedAttributeType } from "app/models/attributes-outputs/attribute-be-model"; +import { AttributeFEModel } from "app/models/attributes-outputs/attribute-fe-model"; +import { DerivedFEAttribute } from "app/models/attributes-outputs/derived-fe-attribute"; +import { DataTypeModel } from "app/models"; + +@Injectable() +export class AttributesUtils { + + constructor(private dataTypeService:DataTypeService, private attributesService: AttributesService) {} + + /** + * Entry point when getting attributes from server + * For each instance, loop through each property, and: + * 1. Create flattened children + * 2. Check against outputs to see if any props are declared and disable them + * 3. Initialize valueObj (which also creates any new list/map flattened children as needed) + * Returns InstanceFeAttributesMap + */ + public convertAttributesMapToFEAndCreateChildren = (instanceAttributesMap:InstanceBeAttributesMap, isVF:boolean, outputs?:Array<OutputFEModel>): InstanceFeAttributesMap => { + let instanceFeAttributesMap:InstanceFeAttributesMap = new InstanceFeAttributesMap(); + angular.forEach(instanceAttributesMap, (attributes:Array<AttributeBEModel>, instanceId:string) => { + let propertyFeArray: Array<AttributeFEModel> = []; + _.forEach(attributes, (property: AttributeBEModel) => { + + if (this.dataTypeService.getDataTypeByTypeName(property.type)) { // if type not exist in data types remove property from list + + let newFEAttrib: AttributeFEModel = new AttributeFEModel(property); //Convert property to FE + + this.initValueObjectRef(newFEAttrib); //initialize valueObj AND creates flattened children + propertyFeArray.push(newFEAttrib); + newFEAttrib.updateExpandedChildAttributeId(newFEAttrib.name); //display only the first level of children + + //if this prop (or any children) are declared, set isDeclared and disable checkbox on parents/children + if (newFEAttrib.getOutputValues && newFEAttrib.getOutputValues.length) { + newFEAttrib.getOutputValues.forEach(propOutputDetail => { + let outputPath = propOutputDetail.outputPath; + if (!outputPath) { //TODO: this is a workaround until Marina adds outputPath + let output = outputs.find(output => output.uniqueId == propOutputDetail.outputId); + if (!output) { console.log("CANNOT FIND INPUT FOR " + propOutputDetail.outputId); return; } + else outputPath = output.outputPath; + } + if (outputPath == newFEAttrib.name) outputPath = undefined; // if not complex we need to remove the outputPath from FEModel so we not look for a child + newFEAttrib.setAsDeclared(outputPath); //if a path is sent, its a child prop. this param is optional + this.attributesService.disableRelatedAttributes(newFEAttrib, outputPath); + }); + } + } + }); + instanceFeAttributesMap[instanceId] = propertyFeArray; + + }); + return instanceFeAttributesMap; + } + + public convertAddAttributeBEToAttributeFE = (property: AttributeBEModel): AttributeFEModel => { + const newFEProp: AttributeFEModel = new AttributeFEModel(property); //Convert property to FE + this.initValueObjectRef(newFEProp); + newFEProp.updateExpandedChildAttributeId(newFEProp.name); //display only the first level of children + return newFEProp; + } + + public createListOrMapChildren = (property:AttributeFEModel | DerivedFEAttribute, key: string, valueObj: any): Array<DerivedFEAttribute> => { + let newProps: Array<DerivedFEAttribute> = []; + let parentProp = new DerivedFEAttribute(property, property.attributesName, true, key, valueObj); + newProps.push(parentProp); + + if (!property.schema.property.isSimpleType) { + let additionalChildren:Array<DerivedFEAttribute> = this.createFlattenedChildren(property.schema.property.type, parentProp.attributesName); + this.assignFlattenedChildrenValues(parentProp.valueObj, additionalChildren, parentProp.attributesName); + additionalChildren.forEach(prop => prop.canBeDeclared = false); + newProps.push(...additionalChildren); + } + return newProps; + } + + /** + * Creates derivedFEAttributes of a specified type and returns them. + */ + private createFlattenedChildren = (type: string, parentName: string):Array<DerivedFEAttribute> => { + let tempProps: Array<DerivedFEAttribute> = []; + let dataTypeObj: DataTypeModel = this.dataTypeService.getDataTypeByTypeName(type); + this.dataTypeService.getDerivedDataTypeAttributes(dataTypeObj, tempProps, parentName); + return _.sortBy(tempProps, ['propertiesName']); + } + + /* Sets the valueObj of parent property and its children. + * Note: This logic is different than assignflattenedchildrenvalues - here we merge values, there we pick either the parents value, props value, or default value - without merging. + */ + public initValueObjectRef = (attribute: AttributeFEModel): void => { + attribute.resetValueObjValidation(); + if (attribute.isDeclared) { //if attribute is declared, it gets a simple output instead. List and map values and pseudo-children will be handled in attribute component + attribute.valueObj = attribute.value || attribute.defaultValue || null; // use null for empty value object + if (attribute.valueObj && typeof attribute.valueObj == 'object') { + attribute.valueObj = JSON.stringify(attribute.valueObj); + } + } else { + attribute.valueObj = attribute.getValueObj(); + if (attribute.derivedDataType == DerivedAttributeType.LIST || attribute.derivedDataType == DerivedAttributeType.MAP) { + attribute.flattenedChildren = []; + Object.keys(attribute.valueObj).forEach((key) => { + attribute.flattenedChildren.push(...this.createListOrMapChildren(attribute, key, attribute.valueObj[key])) + }); + } else if (attribute.derivedDataType === DerivedAttributeType.COMPLEX) { + attribute.flattenedChildren = this.createFlattenedChildren(attribute.type, attribute.name); + this.assignFlattenedChildrenValues(attribute.valueObj, attribute.flattenedChildren, attribute.name); + attribute.flattenedChildren.forEach((childProp) => { + attribute.childPropUpdated(childProp); + }); + } + } + attribute.updateValueObjOrig(); + }; + + /* + * Loops through flattened attributes array and to assign values + * Then, convert any neccessary strings to objects, and vis-versa + * For list or map property, creates new children props if valueObj has values + */ + public assignFlattenedChildrenValues = (parentValueJSON: any, derivedPropArray: Array<DerivedFEAttribute>, parentName: string) => { + if (!derivedPropArray || !parentName) return; + let propsToPushMap: Map<number, Array<DerivedFEAttribute>> = new Map<number, Array<DerivedFEAttribute>>(); + derivedPropArray.forEach((prop, index) => { + + let propNameInObj = prop.attributesName.substring(prop.attributesName.indexOf(parentName) + parentName.length + 1).split('#').join('.'); //extract everything after parent name + prop.valueObj = _.get(parentValueJSON, propNameInObj, prop.value || prop.defaultValue || null); //assign value -first value of parent if exists. If not, prop.value if not, prop.defaultvalue + prop.value = (prop.valueObj !== null && (typeof prop.valueObj) != 'string') ? JSON.stringify(prop.valueObj) : prop.valueObj; + + if ((prop.isDeclared || prop.type == PROPERTY_TYPES.STRING || prop.type == PROPERTY_TYPES.JSON)) { //Stringify objects of items that are declared or from type string/json + prop.valueObj = (prop.valueObj !== null && typeof prop.valueObj == 'object') ? JSON.stringify(prop.valueObj) : prop.valueObj; + } else if(prop.type == PROPERTY_TYPES.INTEGER || prop.type == PROPERTY_TYPES.FLOAT || prop.type == PROPERTY_TYPES.BOOLEAN){ //parse ints and non-string simple types + prop.valueObj = (prop.valueObj !== null && typeof prop.valueObj == PROPERTY_TYPES.STRING) ? JSON.parse(prop.valueObj) : prop.valueObj; + } else { //parse strings that should be objects + if (prop.derivedDataType == DerivedAttributeType.COMPLEX) { + prop.valueObj = (prop.valueObj === null || typeof prop.valueObj != 'object') ? JSON.parse(prop.valueObj || '{}') : prop.valueObj; + } else if (prop.derivedDataType == DerivedAttributeType.LIST) { + prop.valueObj = (prop.valueObj === null || typeof prop.valueObj != 'object') ? JSON.parse(prop.valueObj || '[]') : prop.valueObj; + } else if (prop.derivedDataType == DerivedAttributeType.MAP) { + if (!prop.isChildOfListOrMap || !prop.schema.property.isSimpleType) { + prop.valueObj = (prop.valueObj === null || typeof prop.valueObj != 'object') ? JSON.parse(prop.valueObj || '{}') : prop.valueObj; + } + } + if ((prop.derivedDataType == DerivedAttributeType.LIST || prop.derivedDataType == DerivedAttributeType.MAP) && typeof prop.valueObj == 'object' && prop.valueObj !== null && Object.keys(prop.valueObj).length) { + let newProps: Array<DerivedFEAttribute> = []; + Object.keys(prop.valueObj).forEach((key) => { + newProps.push(...this.createListOrMapChildren(prop, key, prop.valueObj[key]));//create new children, assign their values, and then add to array + }); + propsToPushMap[index + 1] = newProps; + } + } + + prop.valueObj = AttributeFEModel.cleanValueObj(prop.valueObj); + }); + + //add props after we're done looping (otherwise our loop gets messed up). Push in reverse order, so we dont mess up indexes. + Object.keys(propsToPushMap).reverse().forEach((indexToInsert) => { + derivedPropArray.splice(+indexToInsert, 0, ...propsToPushMap[indexToInsert]); //slacker parsing + }); + } + + public resetAttributeValue = (attribute: AttributeFEModel, newValue: string, nestedPath?: string): void => { + attribute.value = newValue; + if (nestedPath) { + let newAttrib = attribute.flattenedChildren.find(attrib => attrib.attributesName == nestedPath); + newAttrib && this.assignFlattenedChildrenValues(JSON.parse(newValue), [newAttrib], attribute.name); + attribute.updateValueObjOrig(); + } else { + this.initValueObjectRef(attribute); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts new file mode 100644 index 0000000000..cef2eb2793 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts @@ -0,0 +1,84 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {AttributeFEModel} from "../../../../models/attributes-outputs/attribute-fe-model"; +import {SimpleFlatAttribute} from "app/models/attributes-outputs/simple-flat-attribute"; +import {DerivedFEAttribute} from "../../../../models/attributes-outputs/derived-fe-attribute"; + +@Injectable() +export class HierarchyNavService { + /** + * Build hierarchy structure for the tree when user selects on table row. + * First create Array<SimpleFlatAttribute> and insert also the parent (AttributeFEModel) to this array. + * The Array is flat and contains SimpleFlatAttribute that has parentName and uniqueId. + * Now we build hierarchy from this Array (that includes children) and return it for the tree + * + * @argument attribute: AttributeFEModel - attribute contains flattenedChildren array of DerivedFEAttribute + * @returns Array<SimpleFlatAttribute> - containing children Array<SimpleFlatAttribute>, augmantin children to SimpleFlatAttribute. + */ + public getSimpleAttributesTree(attribute: AttributeFEModel, instanceName: string): Array<SimpleFlatAttribute> { + // Build Array of SimpleFlatAttribute before unflatten function + let flatAttributes: Array<SimpleFlatAttribute> = []; + flatAttributes.push(this.createSimpleFlatAttribute(attribute, instanceName)); // Push the root attribute + attribute.flattenedChildren.forEach((child: DerivedFEAttribute): void => { + if (child.isChildOfListOrMap && child.schema.property.isSimpleType) return; //do not display non-complex children of list or map + flatAttributes.push(this.createSimpleFlatAttribute(child, instanceName)); + }); + + let tree = this.unflatten(flatAttributes, '', []); + return tree[0].childrens; // Return the childrens without the root. + } + + public createSimpleFlatAttribute = (attribute: AttributeFEModel | DerivedFEAttribute, instanceName: string): SimpleFlatAttribute => { + if (attribute instanceof AttributeFEModel) { + return new SimpleFlatAttribute(attribute.uniqueId, attribute.name, attribute.name, '', instanceName); + } else { + let attribName: string = (attribute.isChildOfListOrMap) ? attribute.mapKey : attribute.name; + return new SimpleFlatAttribute(attribute.uniqueId, attribute.attributesName, attribName, attribute.parentName, instanceName); + } + + } + + /** + * Unflatten Array<SimpleFlatAttribute> and build hierarchy. + * The result will be Array<SimpleFlatAttribute> that augmantin with children for each SimpleFlatAttribute. + */ + private unflatten(array: Array<SimpleFlatAttribute>, parent: any, tree?: any): any { + tree = typeof tree !== 'undefined' ? tree : []; + parent = typeof parent !== 'undefined' && parent !== '' ? parent : {path: ''}; + + var children = array.filter((child: SimpleFlatAttribute): boolean => { + return child.parentName == parent.path; + }); + + if (children && children.length) { + if (parent.path == '') { + tree = children; + } else { + parent['children'] = children; + } + children.forEach((child): void => { + this.unflatten(array, child); + }); + } + return tree; + } +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts new file mode 100644 index 0000000000..c2df55e6a4 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts @@ -0,0 +1,40 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {OutputFEModel} from "../../../../models/attributes-outputs/output-fe-model"; + +@Injectable() +export class OutputsUtils { + + constructor() {} + + public initDefaultValueObject = (output: OutputFEModel): void => { + output.resetDefaultValueObjValidation(); + output.defaultValueObj = output.getDefaultValueObj(); + output.updateDefaultValueObjOrig(); + }; + + public resetOutputDefaultValue = (output: OutputFEModel, newDefaultValue: string): void => { + output.defaultValue = newDefaultValue; + this.initDefaultValueObject(output); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts index 3def63e0d2..0d676ed950 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts @@ -17,8 +17,7 @@ * limitations under the License. * ============LICENSE_END========================================================= */ -import { NgModule } from "@angular/core"; -import {HierarchyNavigationComponent} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.component"; +import {NgModule} from "@angular/core"; import {FormsModule} from "@angular/forms"; import {PropertyTableModule} from "../../components/logic/properties-table/property-table.module"; import {UiElementsModule} from "../../components/ui/ui-elements.module"; @@ -36,29 +35,30 @@ import {InputsUtils} from "./services/inputs.utils"; import {ComponentModeService} from "../../services/component-services/component-mode.service"; import {SdcUiComponentsModule} from "onap-ui-angular"; import {ModalFormsModule} from "app/ng2/components/ui/forms/modal-forms.module"; +import {HierarchyNavigationModule} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.module"; @NgModule({ - declarations: [ - PropertiesAssignmentComponent, - InputsTableComponent, - HierarchyNavigationComponent, - FilterPropertiesAssignmentComponent - ], - imports: [ - BrowserModule, - FormsModule, - GlobalPipesModule, - PropertyTableModule, - PoliciesTableModule, - UiElementsModule, - SdcUiComponentsModule, - ModalFormsModule], + declarations: [ + PropertiesAssignmentComponent, + InputsTableComponent, + FilterPropertiesAssignmentComponent + ], + imports: [ + BrowserModule, + FormsModule, + GlobalPipesModule, + PropertyTableModule, + PoliciesTableModule, + HierarchyNavigationModule, + UiElementsModule, + SdcUiComponentsModule, + ModalFormsModule], - entryComponents: [PropertiesAssignmentComponent], - exports: [ - PropertiesAssignmentComponent - ], - providers: [PropertiesService, HierarchyNavService, PropertiesUtils, InputsUtils, DataTypeService, ComponentModeService] + entryComponents: [PropertiesAssignmentComponent], + exports: [ + PropertiesAssignmentComponent + ], + providers: [PropertiesService, HierarchyNavService, PropertiesUtils, InputsUtils, DataTypeService, ComponentModeService] }) export class PropertiesAssignmentModule { diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts index 5053d52cc8..8167caa959 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts @@ -6,6 +6,7 @@ import { DataTypeService } from 'app/ng2/services/data-type.service'; import { PROPERTY_DATA } from 'app/utils'; import * as _ from 'lodash'; import { PROPERTY_TYPES } from '../../../../utils'; +import {Validation} from "../../../../view-models/workspace/tabs/general/general-view-model"; @Component({ selector: 'property-creator', @@ -15,12 +16,10 @@ import { PROPERTY_TYPES } from '../../../../utils'; export class PropertyCreatorComponent { + validation:Validation; 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; diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts index b0a7651809..426ed4063e 100644 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts @@ -11,7 +11,7 @@ import { AttributeOptions } from './attributes-options'; @Component({ selector: 'attribute-modal', templateUrl: './attribute-modal.component.html', - styleUrls: ['./attributes.component.less'] + styleUrls: ['../../../../view-models/workspace/tabs/attributes/attributes.component.less'] }) export class AttributeModalComponent implements OnInit { diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html deleted file mode 100644 index 6d50bbe11b..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html +++ /dev/null @@ -1,93 +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="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._default}} - </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> diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less deleted file mode 100644 index 3e91ae4689..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less +++ /dev/null @@ -1,36 +0,0 @@ -.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 diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts deleted file mode 100644 index f676e2b4d9..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -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(); - }); -}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts deleted file mode 100644 index bc47f1456b..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts +++ /dev/null @@ -1,188 +0,0 @@ -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); - } - -} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts index 5abb952e37..f85d5298f9 100644 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts @@ -2,7 +2,7 @@ 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 { AttributesComponent } from '../../../../view-models/workspace/tabs/attributes/attributes.component'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; import { AttributeModalComponent } from './attribute-modal.component'; diff --git a/catalog-ui/src/app/ng2/services/attributes.service.ts b/catalog-ui/src/app/ng2/services/attributes.service.ts new file mode 100644 index 0000000000..5086fbfec5 --- /dev/null +++ b/catalog-ui/src/app/ng2/services/attributes.service.ts @@ -0,0 +1,92 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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 {AttributeFEModel} from "../../models/attributes-outputs/attribute-fe-model"; +import {AttributeBEModel} from "app/models/attributes-outputs/attribute-be-model"; +import {DerivedFEAttribute} from "../../models/attributes-outputs/derived-fe-attribute"; +import {AttributeDeclareAPIModel} from "app/models/attributes-outputs/attribute-declare-api-model"; + +@Injectable() +export class AttributesService { + + constructor() { + } + + public getParentAttributeFEModelFromPath = (attributes: Array<AttributeFEModel>, path: string) => { + let parent: AttributeFEModel = attributes.find((property: AttributeFEModel): boolean => { + return property.name === path.substring(0, path.indexOf('#')); + }); + return parent; + } + + //undo disabling of parent and child props= + public undoDisableRelatedAttributes = (property: AttributeFEModel, childPath?: string): void => { + property.isDisabled = false; + if (!childPath) { + property.isSelected = false; + property.flattenedChildren && property.flattenedChildren.map(child => child.isDisabled = false); + } else { //QND - unselect everything and then re-do the disabling of declared props. TODO: put a flag on propertyFEModel instead to indicate who's causing them to be disabled instead + property.flattenedChildren.filter(child => child.isDisabled && !child.isDeclared).forEach(child => child.isDisabled = false); + property.flattenedChildren.filter(child => child.isDeclared || child.isSelected).forEach((childProp) => { //handle brothers who are selected - redo their disabled relatives as well + this.disableRelatedAttributes(property, childProp.attributesName); + }); + } + } + + //disable parents and children of prop + public disableRelatedAttributes = (property: AttributeFEModel, childPath?: string): void => { + if (!childPath) { //selecting the parent property + property.isSelected = true; + property.flattenedChildren && property.flattenedChildren.map(child => { + child.isSelected = false; + child.isDisabled = true; + }); + } else { + property.isSelected = false; + property.isDisabled = true; + property.flattenedChildren.filter((childProp: DerivedFEAttribute) => { + return (childProp.attributesName.indexOf(childPath + "#") === 0 //is child of prop to disable + || childPath.indexOf(childProp.attributesName + "#") === 0); //is parent of prop to disable + }).forEach((child: DerivedFEAttribute) => { + child.isSelected = false; + child.isDisabled = true; + }); + } + } + + public getCheckedAttributes = (attributes: Array<AttributeFEModel>): Array<AttributeBEModel> => { + let selectedProps: Array<AttributeDeclareAPIModel> = []; + attributes.forEach(attrib => { + if (attrib.isSelected && !attrib.isDeclared && !attrib.isDisabled) { + selectedProps.push(new AttributeDeclareAPIModel(attrib)); + } else if (attrib.flattenedChildren) { + attrib.flattenedChildren.forEach((child) => { + if (child.isSelected && !child.isDeclared && !child.isDisabled) { + let childProp = new AttributeDeclareAPIModel(attrib, child); //create it from the parent + selectedProps.push(childProp); + } + }) + } + }); + return selectedProps; + } + +} diff --git a/catalog-ui/src/app/ng2/services/component-instance-services/component-instance.service.ts b/catalog-ui/src/app/ng2/services/component-instance-services/component-instance.service.ts index 74ced7d4bf..5ae2918805 100644 --- a/catalog-ui/src/app/ng2/services/component-instance-services/component-instance.service.ts +++ b/catalog-ui/src/app/ng2/services/component-instance-services/component-instance.service.ts @@ -27,6 +27,8 @@ 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'; +import {AttributeBEModel} from "../../../models/attributes-outputs/attribute-be-model"; +import {OutputBEModel} from "../../../models/attributes-outputs/output-be-model"; @Injectable() export class ComponentInstanceServiceNg2 { @@ -52,6 +54,13 @@ export class ComponentInstanceServiceNg2 { }) } + getComponentInstanceAttributes(component: Component, componentInstanceId: string): Observable<Array<AttributeBEModel>> { + return this.http.get<Array<AttributeBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstanceId + '/attributes') + .map(res => { + return CommonUtils.initBeAttributes(res); + }) + } + getComponentInstanceInputs(component: Component, componentInstance: ComponentInstance): Observable<Array<PropertyBEModel>> { return this.http.get<Array<InputBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstance.uniqueId + '/' + componentInstance.componentUid + '/inputs') .map(res => { @@ -59,6 +68,13 @@ export class ComponentInstanceServiceNg2 { }) } + getComponentInstanceOutputs(component: Component, componentInstance: ComponentInstance): Observable<Array<AttributeBEModel>> { + return this.http.get<Array<OutputBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/componentInstances/' + componentInstance.uniqueId + '/' + componentInstance.componentUid + '/outputs') + .map(res => { + return CommonUtils.initOutputs(res); + }) + } + 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) @@ -68,7 +84,7 @@ export class ComponentInstanceServiceNg2 { }; 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)); }; @@ -99,6 +115,18 @@ export class ComponentInstanceServiceNg2 { }); } + updateInstanceAttributes(componentType:string, componentId:string, componentInstanceId: string, attributes: AttributeBEModel[]) { + + return this.http.post<Array<AttributeModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/resourceInstance/' + componentInstanceId + '/attributes', attributes) + .map((res) => { + return res.map((resAttribute) => { + let newAttrib = new AttributeModel(resAttribute); + newAttrib.resourceInstanceUniqueId = componentInstanceId; + return newAttrib; + }); + }); + } + getInstanceCapabilityProperties(componentType: string, componentId: string, componentInstanceId: string, capability: Capability): Observable<Array<PropertyModel>> { return this.http.get<Array<PropertyModel>>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/componentInstances/' + componentInstanceId + '/capability/' + capability.type + @@ -138,6 +166,14 @@ export class ComponentInstanceServiceNg2 { }); } + updateInstanceOutputs(component: Component, componentInstanceId: string, outputs: AttributeBEModel[]): Observable<AttributeBEModel[]> { + + return this.http.post<Array<AttributeModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/resourceInstance/' + componentInstanceId + '/outputs', outputs) + .map((res) => { + return res.map((resOutput) => new AttributeBEModel(resOutput)); + }); + } + getComponentGroupInstanceProperties(component: Component, groupInstanceId: string): Observable<Array<PropertyBEModel>> { return this.http.get<Array<PropertyBEModel>>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/groups/' + groupInstanceId + '/properties') .map((res) => { diff --git a/catalog-ui/src/app/ng2/services/component-services/component.service.ts b/catalog-ui/src/app/ng2/services/component-services/component.service.ts index 3093e632fc..d406cf05f6 100644 --- a/catalog-ui/src/app/ng2/services/component-services/component.service.ts +++ b/catalog-ui/src/app/ng2/services/component-services/component.service.ts @@ -43,6 +43,7 @@ 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"; +import { OutputBEModel } from "app/models/attributes-outputs/output-be-model"; /* PLEASE DO NOT USE THIS SERVICE IN ANGULAR2! Use the topology-template.service instead @@ -59,7 +60,7 @@ export class ComponentServiceNg2 { protected getComponentDataByFieldsName(componentType:string, componentId:string, fields:Array<string>):Observable<ComponentGenericResponse> { let params: HttpParams = new HttpParams(); - _.forEach(fields, (field:string):void => { + fields.forEach((field:string):void => { params = params.append(API_QUERY_PARAMS.INCLUDE, field); }); @@ -109,6 +110,10 @@ export class ComponentServiceNg2 { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_POLICIES, COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_GROUPS]); } + getComponentResourceAttributesData(component:Component):Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_NON_EXCLUDED_GROUPS]); + } + getComponentResourceInstances(component:Component):Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INSTANCES]); } @@ -208,8 +213,15 @@ export class ComponentServiceNg2 { return this.http.get<any>(this.baseUrl + 'interfaceLifecycleTypes') .map((res: any) => { const interfaceMap = {}; - _.forEach(res, (interf: any) => { - interfaceMap[interf.toscaPresentation.type] = _.keys(interf.toscaPresentation.operations); + if (!res) { + return interfaceMap; + } + Object.keys(res).forEach(interfaceName => { + const interface1 = res[interfaceName]; + if (!interface1.toscaPresentation.operations) { + return; + } + interfaceMap[interface1.toscaPresentation.type] = Object.keys(interface1.toscaPresentation.operations); }); return interfaceMap; }); @@ -297,7 +309,6 @@ export class ComponentServiceNg2 { 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') @@ -306,6 +317,14 @@ export class ComponentServiceNg2 { }) } + deleteOutput(component:Component, output:OutputBEModel):Observable<OutputBEModel> { + + return this.http.delete<OutputBEModel>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/delete/' + output.uniqueId + '/output') + .map((res) => { + return new OutputBEModel(res); + }) + } + updateComponentInputs(component:Component, inputs:InputBEModel[]):Observable<InputBEModel[]> { return this.http.post<InputBEModel[]>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/update/inputs', inputs) @@ -314,9 +333,26 @@ export class ComponentServiceNg2 { }) } + updateComponentOutputs(component:Component, outputs:OutputBEModel[]):Observable<OutputBEModel[]> { + + return this.http.post<OutputBEModel[]>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/update/outputs', outputs) + .map((res) => { + return res.map((output) => new OutputBEModel(output)); + }) + } + filterComponentInstanceProperties(component:Component, filterData:FilterPropertiesAssignmentData):Observable<InstanceBePropertiesMap> {//instance-property-be-map let params: HttpParams = new HttpParams(); - _.forEach(filterData.selectedTypes, (type:string) => { + filterData.selectedTypes.forEach((type:string) => { + params = params.append('resourceType', type); + }); + + return this.http.get<InstanceBePropertiesMap>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/filteredproperties/' + filterData.propertyName, {params: params}); + } + + filterComponentInstanceAttributes(component:Component, filterData:FilterPropertiesAssignmentData):Observable<InstanceBePropertiesMap> {//instance-property-be-map + let params: HttpParams = new HttpParams(); + filterData.selectedTypes.forEach((type:string) => { params = params.append('resourceType', type); }); diff --git a/catalog-ui/src/app/ng2/services/component-services/service.service.ts b/catalog-ui/src/app/ng2/services/component-services/service.service.ts index 9460a32323..05384c9eec 100644 --- a/catalog-ui/src/app/ng2/services/component-services/service.service.ts +++ b/catalog-ui/src/app/ng2/services/component-services/service.service.ts @@ -89,7 +89,9 @@ export class ServiceServiceNg2 extends ComponentServiceNg2 { COMPONENT_FIELDS.COMPONENT_INSTANCES_INTERFACES, COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES, COMPONENT_FIELDS.COMPONENT_INSTANCES_INPUTS, + COMPONENT_FIELDS.COMPONENT_INSTANCES_OUTPUTS, COMPONENT_FIELDS.COMPONENT_INPUTS, + COMPONENT_FIELDS.COMPONENT_OUTPUTS, COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_CAPABILITIES ]); diff --git a/catalog-ui/src/app/ng2/services/component-services/topology-template.service.ts b/catalog-ui/src/app/ng2/services/component-services/topology-template.service.ts index 953f0a1960..0249912862 100644 --- a/catalog-ui/src/app/ng2/services/component-services/topology-template.service.ts +++ b/catalog-ui/src/app/ng2/services/component-services/topology-template.service.ts @@ -70,6 +70,8 @@ import { ComponentInstanceInterfaceModel, InterfaceOperationModel } from "../../../models/interfaceOperation"; +import {AttributeBEModel} from "../../../models/attributes-outputs/attribute-be-model"; +import {InstanceAttributesAPIMap} from "../../../models/attributes-outputs/attribute-fe-map"; /* 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*/ @@ -126,6 +128,11 @@ export class TopologyTemplateService { [COMPONENT_FIELDS.COMPONENT_INPUTS, COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES, COMPONENT_FIELDS.COMPONENT_PROPERTIES]); } + getComponentOutputsWithAttributes(componentType: string, componentId: string): Observable<ComponentGenericResponse> { + return this.getComponentDataByFieldsName(componentType, componentId, + [COMPONENT_FIELDS.COMPONENT_OUTPUTS, COMPONENT_FIELDS.COMPONENT_INSTANCES, COMPONENT_FIELDS.COMPONENT_INSTANCES_ATTRIBUTES, COMPONENT_FIELDS.COMPONENT_ATTRIBUTES,COMPONENT_FIELDS.COMPONENT_INSTANCES_OUTPUTS]); + } + getComponentDeploymentArtifacts(component: Component): Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_DEPLOYMENT_ARTIFACTS]); } @@ -164,6 +171,11 @@ export class TopologyTemplateService { return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/inputs', inputs); } + createOutput(component: Component, outputsToCreate: InstanceAttributesAPIMap, isSelf: boolean): Observable<any> { + const outputs = isSelf ? { serviceProperties: outputsToCreate.componentInstanceAttributes } : outputsToCreate; + return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/create/outputs', outputs); + } + restoreComponent(componentType: string, componentId: string) { return this.http.post(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/restore', {}); } @@ -206,6 +218,16 @@ export class TopologyTemplateService { }); } + createServiceAttribute(componentId: string, attributeModel: AttributeBEModel): Observable<AttributeBEModel> { + const serverObject = {}; + serverObject[attributeModel.name] = attributeModel; + return this.http.post<AttributeBEModel>(this.baseUrl + 'services/' + componentId + '/attributes', serverObject) + .map((res) => { + const attribute: AttributeBEModel = new AttributeBEModel(res); + return attribute; + }); + } + getServiceProperties(componentId: string): Observable<PropertyBEModel[]> { return this.http.get<any>(this.baseUrl + 'services/' + componentId + '/properties') .map((res) => { @@ -216,6 +238,16 @@ export class TopologyTemplateService { }); } + getServiceAttributes(componentId: string): Observable<AttributeBEModel[]> { + return this.http.get<any>(this.baseUrl + 'services/' + componentId + '/attributes') + .map((res) => { + if (!res) { + return new Array<AttributeBEModel>(); + } + return CommonUtils.initAttributes(res); + }); + } + updateServiceProperties(componentId: string, properties: PropertyBEModel[]) { return this.http.put<any>( this.baseUrl + 'services/' + componentId + '/properties', properties) .map((res) => { @@ -225,6 +257,15 @@ export class TopologyTemplateService { }); } + updateServiceAttributes(componentId: string, attributes: AttributeBEModel[]) { + return this.http.put<any>( this.baseUrl + 'services/' + componentId + '/attributes', attributes) + .map((res) => { + const resJson = res; + return _.map(resJson, + (resValue: AttributeBEModel) => new AttributeBEModel(resValue)); + }); + } + deleteServiceProperty(componentId: string, property: PropertyBEModel): Observable<string> { return this.http.delete(this.baseUrl + 'services/' + componentId + '/properties/' + property.uniqueId ) .map((res: Response) => { @@ -242,6 +283,13 @@ export class TopologyTemplateService { }); } + deleteServiceAttribute(componentId: string, attribute: AttributeBEModel): Observable<string> { + return this.http.delete(this.baseUrl + 'services/' + componentId + '/attributes/' + attribute.uniqueId ) + .map((res: Response) => { + return attribute.uniqueId; + }); + } + getDependencies(componentType: string, componentId: string): Observable<IDependenciesServerResponse[]> { return this.http.get<IDependenciesServerResponse[]>(this.baseUrl + this.getServerTypeUrl(componentType) + componentId + '/dependencies'); } diff --git a/catalog-ui/src/app/ng2/services/data-type.service.ts b/catalog-ui/src/app/ng2/services/data-type.service.ts index 0559f35ae2..30eb6f0c77 100644 --- a/catalog-ui/src/app/ng2/services/data-type.service.ts +++ b/catalog-ui/src/app/ng2/services/data-type.service.ts @@ -23,6 +23,7 @@ import { Injectable } from '@angular/core'; import { DataTypeModel, DataTypesMap, PropertyFEModel, DerivedFEProperty} from "app/models"; import { DataTypesService } from "app/services/data-types-service"; import { PROPERTY_DATA } from "app/utils"; +import {DerivedFEAttribute} from "../../models/attributes-outputs/derived-fe-attribute"; /** This is a new service for NG2, to eventually replace app/services/data-types-service.ts * @@ -57,7 +58,6 @@ export class DataTypeService { return null; } - public getDerivedDataTypeProperties(dataTypeObj: DataTypeModel, propertiesArray: Array<DerivedFEProperty>, parentName: string) { //push all child properties to array if (!dataTypeObj) return; @@ -76,6 +76,24 @@ export class DataTypeService { } } + public getDerivedDataTypeAttributes(dataTypeObj: DataTypeModel, attributesArray: Array<DerivedFEAttribute>, parentName: string) { + //push all child properties to array + if (!dataTypeObj) return; + if (dataTypeObj.attributes) { + dataTypeObj.attributes.forEach((derivedAttribute) => { + if(dataTypeObj.name !== PROPERTY_DATA.OPENECOMP_ROOT || derivedAttribute.name !== PROPERTY_DATA.SUPPLEMENTAL_DATA){//The requirement is to not display the property supplemental_data + attributesArray.push(new DerivedFEAttribute(derivedAttribute, parentName)); + } + let derivedDataTypeObj: DataTypeModel = this.getDataTypeByTypeName(derivedAttribute.type); + this.getDerivedDataTypeAttributes(derivedDataTypeObj, attributesArray, parentName + "#" + derivedAttribute.name); + }); + } + //recurse parent (derivedFrom), in case one of parents contains properties + if (dataTypeObj.derivedFrom && PROPERTY_DATA.ROOT_DATA_TYPE !== dataTypeObj.derivedFrom.name) { + this.getDerivedDataTypeAttributes(dataTypeObj.derivedFrom, attributesArray, parentName); + } + } + /** * Checks for custom behavior for a given data type by checking if a function exists within data-type.service with that name * Additional custom behavior can be added by adding a function with the given dataType name diff --git a/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts b/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts index c6be9c369a..09ace56965 100644 --- a/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts +++ b/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts @@ -26,11 +26,11 @@ import { ArtifactGroupModel, PropertyModel, PropertiesGroup, AttributeModel, Att 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"; +import { OutputBEModel } from "app/models/attributes-outputs/output-be-model"; export class ComponentGenericResponse implements Serializable<ComponentGenericResponse> { @@ -45,6 +45,7 @@ export class ComponentGenericResponse implements Serializable<ComponentGenericR public componentInstances:Array<ComponentInstance>; public componentInstancesInterfaces: Map<string, Array<InterfaceModel>>; public inputs:Array<InputBEModel>; + public outputs:Array<OutputBEModel>; public capabilities:CapabilitiesGroup; public requirements:RequirementsGroup; public properties:Array<PropertyModel>; @@ -82,6 +83,9 @@ export class ComponentGenericResponse implements Serializable<ComponentGenericR if(response.inputs) { this.inputs = CommonUtils.initInputs(response.inputs); } + if (response.outputs) { + this.outputs = CommonUtils.initOutputs(response.outputs); + } if(response.attributes) { this.attributes = CommonUtils.initAttributes(response.attributes); } |