Add substitution filter UI support

Issue-ID: SDC-3195
Signed-off-by: aribeiro <anderson.ribeiro@est.tech>
Change-Id: Idc6301a1ab7442f8d2d59931abf0a0741cc8e410
diff --git a/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.html b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.html
new file mode 100644
index 0000000..f781e6c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.html
@@ -0,0 +1,50 @@
+<!--
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2020 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="substitution-filter">
+
+  <div *ngIf="compositeService.isService()"
+       class="i-sdc-designer-sidebar-section-content-item-rules-section">
+
+    <div class="i-sdc-designer-sidebar-section-content-item-rule" [ngClass]="{'hand': !readonly}"
+         *ngFor="let constraint of constraintObjects; let i = index">
+      <div class="rule-details" [ngClass]="{'readonly': readonly}">
+        <div class="rule-desc" (click)="!readonly && onSelectFilter(i)" tooltips
+             tooltip="{{constraint.servicePropertyName + ' ' + getSymbol(constraint.constraintOperator) + ' '
+             + (constraint.sourceName ? constraint.sourceName + ':' : '') + constraint.value}}">
+          {{constraint.servicePropertyName + ' ' + getSymbol(constraint.constraintOperator) + ' '
+        + (constraint.sourceName ? constraint.sourceName + ':' : '') + constraint.value}}
+        </div>
+        <span *ngIf="!readonly" class="sprite-new delete-btn delete-icon"
+              (click)="openDeleteModal(i)" data-tests-id="delete-input-button"></span>
+      </div>
+    </div>
+
+    <div *ngIf="!isSubstitutionFilterSet()" class="w-sdc-designer-sidebar-section-footer">
+      <button
+          class="w-sdc-designer-sidebar-section-footer-action tlv-btn blue"
+          data-tests-id="add-rule-button"
+          (click)="onAddSubstitutionFilter()"
+          [disabled]="readonly">
+        {{'ADD_SUBSTITUTION_FILTER' | translate}}
+      </button>
+    </div>
+  </div>
+</div>
diff --git a/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.less b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.less
new file mode 100644
index 0000000..2d9b264
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.less
@@ -0,0 +1,105 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2020 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 './../../../../../assets/styles/variables.less';
+@import './../../../../../assets/styles/variables-old.less';
+@import './../../../../../assets/styles/mixins_old.less';
+@import './../../../../../assets/styles/mixins.less';
+
+.substitution-filter {
+
+  /deep/ .checkbox-label-mark-as-dependent {
+    padding: 7px 18px;
+    position: relative;
+    height: 61px;
+    color: @main_color_a;
+    box-shadow: 0 2px 7px @main_color_o;
+    border-bottom: 1px solid @main_color_o;
+    .checkbox-label {
+      margin-top: 14px;
+      .checkbox-label-content {
+        font-size: 14px;
+      }
+    }
+    .checkbox-container input[type=checkbox].checkbox-hidden[disabled] ~ .checkbox-label-content {
+      opacity: 0.5;
+    }
+    .delete-btn {
+      background-position: -137px -415px;
+      width: 24px;
+      height: 24px
+    }
+
+    loader {
+      top: 20px;
+    }
+  }
+
+
+  .i-sdc-designer-sidebar-section-content-item-rules-section {
+    .i-sdc-designer-sidebar-section-content-item-rule {
+      border-bottom: 1px solid @main_color_o;
+      padding: 5px 10px 5px 18px;
+      position: relative;
+      height: 61px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .rule-details {
+        // .s_1;
+        display: flex;
+        flex: 1;
+        align-items: center;
+        justify-content: space-between;
+        margin-left: 5px;
+        width: 180px;
+        .delete-icon {
+          visibility: hidden;
+        }
+        &:not(.readonly):hover {
+          .a_1;
+        }
+        &:hover .delete-icon{
+          visibility: visible;
+        }
+        &.readonly {
+          opacity: 0.5;
+        }
+      }
+      .rule-desc {
+        .sdc-ellipsis;
+        width: 220px;
+        position: relative;
+        padding-right: 1em;
+      }
+
+    }
+  }
+  .w-sdc-designer-sidebar-section-footer {
+    margin-top: 10px;
+    text-align: center;
+    width: 100%;
+  }
+  .w-sdc-designer-sidebar-section-footer-action {
+    width: 180px;
+    margin-top: 10px;
+  }
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.ts b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.ts
new file mode 100644
index 0000000..a0fa2bb
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.component.ts
@@ -0,0 +1,296 @@
+/*
+* ============LICENSE_START=======================================================
+*  Copyright (C) 2020 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, ComponentRef, EventEmitter, Input, Output} from '@angular/core';
+import {
+  ButtonModel,
+  ComponentInstance,
+  InputBEModel,
+  ModalModel,
+  PropertyBEModel,
+} from 'app/models';
+import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component';
+import {ServiceDependenciesEditorComponent} from 'app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component';
+import {ModalService} from 'app/ng2/services/modal.service';
+import {ComponentGenericResponse} from 'app/ng2/services/responses/component-generic-response';
+import {TranslateService} from 'app/ng2/shared/translator/translate.service';
+import {ComponentMetadata} from '../../../../models/component-metadata';
+import {ServiceInstanceObject} from '../../../../models/service-instance-properties-and-interfaces';
+import {TopologyTemplateService} from '../../../services/component-services/topology-template.service';
+
+export class ConstraintObject {
+  servicePropertyName: string;
+  constraintOperator: string;
+  sourceType: string;
+  sourceName: string;
+  value: string;
+
+  constructor(input?: any) {
+    if (input) {
+      this.servicePropertyName = input.servicePropertyName;
+      this.constraintOperator = input.constraintOperator;
+      this.sourceType = input.sourceType;
+      this.sourceName = input.sourceName;
+      this.value = input.value;
+    }
+  }
+}
+
+export class ConstraintObjectUI extends ConstraintObject {
+  isValidValue: boolean;
+
+  constructor(input?: any) {
+    super(input);
+    if (input) {
+      this.isValidValue = input.isValidValue ? input.isValidValue : input.value !== '';
+    }
+  }
+
+  public updateValidity(isValidValue: boolean) {
+    this.isValidValue = isValidValue;
+  }
+
+  public isValidRule(isStatic) {
+    const isValidValue = isStatic ? this.isValidValue : true;
+    return this.servicePropertyName != null && this.servicePropertyName !== ''
+        && this.value != null && this.value !== '' && isValidValue;
+  }
+}
+
+export const OPERATOR_TYPES = {
+  EQUAL: 'equal',
+  GREATER_THAN: 'greater_than',
+  LESS_THAN: 'less_than'
+};
+
+class I18nTexts {
+  static addSubstitutionFilterTxt: string;
+  static updateSubstitutionFilterTxt: string;
+  static deleteSubstitutionFilterTxt: string;
+  static deleteSubstitutionFilterMsg: string;
+  static modalCancel: string;
+  static modalCreate: string;
+  static modalSave: string;
+  static modalDelete: string;
+
+  public static translateTexts(translateService) {
+    I18nTexts.modalCancel = translateService.translate('MODAL_CANCEL');
+    I18nTexts.modalCreate = translateService.translate('MODAL_CREATE');
+    I18nTexts.modalSave = translateService.translate('MODAL_SAVE');
+    I18nTexts.modalDelete = translateService.translate('MODAL_DELETE');
+
+    I18nTexts.addSubstitutionFilterTxt = translateService.translate('ADD_SUBSTITUTION_FILTER');
+    I18nTexts.updateSubstitutionFilterTxt = translateService.translate('UPDATE_SUBSTITUTION_FILTER');
+    I18nTexts.deleteSubstitutionFilterTxt = translateService.translate('DELETE_SUBSTITUTION_FILTER');
+    I18nTexts.deleteSubstitutionFilterMsg = translateService.translate('DELETE_SUBSTITUTION_FILTER_MSG');
+  }
+}
+
+@Component({
+  selector: 'substitution-filter',
+  templateUrl: './substitution-filter.component.html',
+  styleUrls: ['substitution-filter.component.less'],
+  providers: [ModalService, TranslateService]
+})
+
+export class SubstitutionFilterComponent {
+  modalInstance: ComponentRef<ModalComponent>;
+  isLoading: boolean;
+  parentServiceInputs: InputBEModel[] = [];
+  operatorTypes: any[];
+  constraintObjects: ConstraintObject[] = [];
+
+  @Input() readonly: boolean;
+  @Input() compositeService: ComponentMetadata;
+  @Input() currentServiceInstance: ComponentInstance;
+  @Input() selectedInstanceSiblings: ServiceInstanceObject[];
+  @Input() selectedInstanceConstraints: ConstraintObject[] = [];
+  @Input() selectedInstanceProperties: PropertyBEModel[] = [];
+  @Output() updateConstraintListEvent: EventEmitter<ConstraintObject[]> = new EventEmitter<ConstraintObject[]>();
+  @Output() loadConstraintListEvent: EventEmitter<any> = new EventEmitter();
+  @Output() hasSubstitutionFilter = new EventEmitter<boolean>();
+
+  constructor(private topologyTemplateService: TopologyTemplateService, private modalServiceNg2: ModalService, private translateService: TranslateService) {
+  }
+
+  ngOnInit() {
+    this.isLoading = false;
+    this.operatorTypes = [
+      {label: '>', value: OPERATOR_TYPES.GREATER_THAN},
+      {label: '<', value: OPERATOR_TYPES.LESS_THAN},
+      {label: '=', value: OPERATOR_TYPES.EQUAL}
+    ];
+    this.topologyTemplateService.getComponentInputsWithProperties(this.compositeService.componentType, this.compositeService.uniqueId).subscribe((result: ComponentGenericResponse) => {
+      this.parentServiceInputs = result.inputs;
+    });
+    this.loadAllInstances();
+    this.translateService.languageChangedObservable.subscribe((lang) => {
+      I18nTexts.translateTexts(this.translateService);
+    });
+  }
+
+  ngOnChanges(changes) {
+    if (changes.currentServiceInstance) {
+      this.currentServiceInstance = changes.currentServiceInstance.currentValue;
+    }
+    if (changes.selectedInstanceConstraints && changes.selectedInstanceConstraints.currentValue !== changes.selectedInstanceConstraints.previousValue) {
+      this.selectedInstanceConstraints = changes.selectedInstanceConstraints.currentValue;
+      this.loadAllInstances();
+    }
+  }
+
+  public loadAllInstances = (): void => {
+    this.topologyTemplateService.getComponentCompositionData(this.compositeService.uniqueId, this.compositeService.componentType).subscribe((response) => {
+      response.componentInstances.forEach(componentInstance => this.getSubstitutionFilter(componentInstance))
+    })
+  }
+
+  onAddSubstitutionFilter() {
+    const cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'outline white', this.modalServiceNg2.closeCurrentModal);
+    const saveButton: ButtonModel = new ButtonModel(I18nTexts.modalCreate, 'blue', this.createSubstitutionFilter, this.getDisabled);
+    const modalModel: ModalModel = new ModalModel('l', I18nTexts.addSubstitutionFilterTxt, '', [saveButton, cancelButton], 'standard');
+    this.modalInstance = this.modalServiceNg2.createCustomModal(modalModel);
+    this.modalServiceNg2.addDynamicContentToModal(
+        this.modalInstance,
+        ServiceDependenciesEditorComponent,
+        {
+          currentServiceName: this.currentServiceInstance.name,
+          operatorTypes: this.operatorTypes,
+          compositeServiceName: this.compositeService.name,
+          parentServiceInputs: this.parentServiceInputs,
+          selectedInstanceProperties: this.selectedInstanceProperties,
+          selectedInstanceSiblings: this.selectedInstanceSiblings
+        }
+    );
+    this.modalInstance.instance.open();
+  }
+
+  createSubstitutionFilter = (): void => {
+    const newSubstitutionFilter: ConstraintObject = new ConstraintObject(this.modalInstance.instance.dynamicContent.instance.currentRule);
+    this.isLoading = true;
+    this.topologyTemplateService.createSubstitutionFilterConstraints(
+        this.compositeService.uniqueId,
+        this.currentServiceInstance.uniqueId,
+        newSubstitutionFilter,
+        this.compositeService.componentType
+    ).subscribe((response) => {
+      this.updateConstraintListEvent.emit(response.properties);
+      this.isLoading = false;
+    }, () => {
+      console.error("Failed to Create Substitution Filter on the component with id: ", this.compositeService.uniqueId);
+      this.isLoading = false;
+    });
+    this.modalServiceNg2.closeCurrentModal();
+  }
+
+  onSelectFilter(index: number) {
+    const cancelButton: ButtonModel = new ButtonModel(I18nTexts.modalCancel, 'outline white', this.modalServiceNg2.closeCurrentModal);
+    const updateButton: ButtonModel = new ButtonModel(I18nTexts.modalSave, 'blue', () => this.updateSubstitutionFilter(), this.getDisabled);
+    const modalModel: ModalModel = new ModalModel('l', I18nTexts.updateSubstitutionFilterTxt, '', [updateButton, cancelButton], 'standard');
+    this.modalInstance = this.modalServiceNg2.createCustomModal(modalModel);
+    this.modalServiceNg2.addDynamicContentToModal(
+        this.modalInstance,
+        ServiceDependenciesEditorComponent,
+        {
+          serviceRuleIndex: index,
+          serviceRules: _.map(this.constraintObjects, (constraint) => new ConstraintObjectUI(constraint)),
+          currentServiceName: this.currentServiceInstance.name,
+          operatorTypes: this.operatorTypes,
+          compositeServiceName: this.compositeService.name,
+          parentServiceInputs: this.parentServiceInputs,
+          selectedInstanceProperties: this.selectedInstanceProperties,
+          selectedInstanceSiblings: this.selectedInstanceSiblings
+        }
+    );
+    this.modalInstance.instance.open();
+  }
+
+  updateSubstitutionFilter = (): void => {
+    const constraintToUpdate: ConstraintObject = this.modalInstance.instance.dynamicContent.instance.serviceRulesList.map((rule) => new ConstraintObject(rule));
+    this.isLoading = true;
+    this.topologyTemplateService.updateSubstitutionFilterConstraints(
+        this.compositeService.uniqueId,
+        this.currentServiceInstance.uniqueId,
+        constraintToUpdate,
+        this.compositeService.componentType
+    ).subscribe((response) => {
+      this.hasSubstitutionFilter.emit(this.isSubstitutionFilterSet());
+      this.updateConstraintListEvent.emit(response.properties);
+      this.isLoading = false;
+    }, () => {
+      console.error("Failed to Update Substitution Filter on the component with id: ", this.compositeService.uniqueId);
+      this.isLoading = false;
+    });
+    this.modalServiceNg2.closeCurrentModal();
+  }
+
+  onDeleteSubstitutionFilter = (index: number) => {
+    this.isLoading = true;
+    this.topologyTemplateService.deleteSubstitutionFilterConstraints(
+        this.compositeService.uniqueId,
+        this.currentServiceInstance.uniqueId,
+        index,
+        this.compositeService.componentType
+    ).subscribe((response) => {
+      console.log("on Delete - Response Properties: ", response.properties);
+      this.updateConstraintListEvent.emit(response.properties);
+      this.isLoading = false;
+    }, () => {
+      console.error("Failed to Delete Substitution Filter on the component with id: ", this.compositeService.uniqueId);
+      this.isLoading = false;
+    });
+    this.constraintObjects = [];
+    this.modalServiceNg2.closeCurrentModal();
+  }
+
+  getDisabled = (): boolean => {
+    return !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit();
+  }
+
+  getSymbol(constraintOperator) {
+    switch (constraintOperator) {
+      case OPERATOR_TYPES.LESS_THAN:
+        return '<';
+      case OPERATOR_TYPES.EQUAL:
+        return '=';
+      case OPERATOR_TYPES.GREATER_THAN:
+        return '>';
+    }
+  }
+
+  openDeleteModal = (index: number) => {
+    this.modalServiceNg2.createActionModal(I18nTexts.deleteSubstitutionFilterTxt, I18nTexts.deleteSubstitutionFilterMsg,
+        I18nTexts.modalDelete, () => this.onDeleteSubstitutionFilter(index), I18nTexts.modalCancel).instance.open();
+  }
+
+  private getSubstitutionFilter = (componentInstance: ComponentInstance): void => {
+    this.topologyTemplateService.getSubstitutionFilterConstraints(this.compositeService.componentType, this.compositeService.uniqueId).subscribe((response) => {
+      const substitutionFilter: ConstraintObject[] = response.substitutionFilterForTopologyTemplate[componentInstance.uniqueId].properties;
+      if (substitutionFilter) {
+        this.currentServiceInstance = componentInstance;
+        this.constraintObjects = substitutionFilter;
+      }
+    });
+  }
+
+  private isSubstitutionFilterSet = (): boolean => {
+    return Array.isArray(this.constraintObjects) && this.constraintObjects.length > 0;
+  }
+
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.module.ts b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.module.ts
new file mode 100644
index 0000000..9ea7c0c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/substitution-filter/substitution-filter.module.ts
@@ -0,0 +1,44 @@
+/*
+* ============LICENSE_START=======================================================
+*  Copyright (C) 2020 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 { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module';
+import { TranslateModule } from 'app/ng2/shared/translator/translate.module';
+import { SubstitutionFilterComponent } from "./substitution-filter.component";
+
+@NgModule({
+    declarations: [
+        SubstitutionFilterComponent
+    ],
+    imports: [
+        CommonModule,
+        UiElementsModule,
+        TranslateModule
+    ],
+    exports: [
+        SubstitutionFilterComponent
+    ],
+    entryComponents: [
+        SubstitutionFilterComponent
+    ],
+    providers: []
+})
+export class SubstitutionFilterModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
index 69ca3fa..5467ece 100644
--- a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
@@ -47,7 +47,7 @@
 import { WorkspaceState } from 'app/ng2/store/states/workspace.state';
 import { EventListenerService } from 'app/services';
 import { ComponentInstanceFactory, EVENTS, SdcElementType } from 'app/utils';
-import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS } from 'app/utils/constants';
+import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS , SUBSTITUTION_FILTER_EVENTS} from 'app/utils/constants';
 import * as _ from 'lodash';
 import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop';
 import { SdcUiServices } from 'onap-ui-angular';
@@ -143,6 +143,7 @@
         });
         this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT);
         this.eventListenerService.unRegisterObserver(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE);
+        this.eventListenerService.unRegisterObserver(SUBSTITUTION_FILTER_EVENTS.ON_SUBSTITUTION_FILTER_CHANGE);
     }
 
     public isViewOnly = (): boolean => {
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts
index 25a0c72..305086e 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts
@@ -157,7 +157,7 @@
         fixture.componentInstance.ngOnInit();
 
         // Expect that
-        expect (fixture.componentInstance.tabs.length).toBe(5);
+        expect (fixture.componentInstance.tabs.length).toBe(6);
         expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
         expect (fixture.componentInstance.tabs[1]).toEqual(tabs.properties);
         expect (fixture.componentInstance.tabs[2]).toEqual(tabs.reqAndCapabilities);
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts
index 348dc9f..bf8cc27 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts
@@ -38,6 +38,7 @@
 import { CompositionStateModel, GraphState } from '../common/store/graph.state';
 import { ServiceConsumptionTabComponent } from './panel-tabs/service-consumption-tab/service-consumption-tab.component';
 import { ServiceDependenciesTabComponent } from './panel-tabs/service-dependencies-tab/service-dependencies-tab.component';
+import {SubstitutionFilterTabComponent} from "./panel-tabs/substitution-filter-tab/substitution-filter-tab.component";
 
 const tabs = {
     infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'},
@@ -53,7 +54,8 @@
     inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'},
     settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'},
     consumption: {titleIcon: 'api-o', component: ServiceConsumptionTabComponent, input: {title: 'OPERATION CONSUMPTION'}, isActive: false, tooltipText: 'Service Consumption'},
-    dependencies: {titleIcon: 'archive', component: ServiceDependenciesTabComponent, input: {title: 'DIRECTIVES AND NODE FILTER'}, isActive: false, tooltipText: 'Service Dependencies'}
+    dependencies: {titleIcon: 'archive', component: ServiceDependenciesTabComponent, input: {title: 'DIRECTIVES AND NODE FILTER'}, isActive: false, tooltipText: 'Service Dependencies'},
+    substitutionFilter: {titleIcon: 'composition-o', component: SubstitutionFilterTabComponent, input: {title: 'SUBSTITUTION FILTER'}, isActive: false, tooltipText: 'Substitution Filter'}
 };
 
 @Component({
@@ -144,6 +146,7 @@
         if (component.isService() && this.selectedComponentIsServiceProxyInstance()) {
             this.tabs.push(tabs.consumption);
             this.tabs.push(tabs.dependencies);
+            this.tabs.push(tabs.substitutionFilter)
         } else if (component.isResource() && this.selectedComponentIsVfcInstance()) {
             this.tabs.push(tabs.dependencies);
         }
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts
index 0fd1e51..a89db21 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts
@@ -47,6 +47,8 @@
 import { ServiceDependenciesTabComponent } from "./panel-tabs/service-dependencies-tab/service-dependencies-tab.component";
 import { ServiceDependenciesModule } from "../../../components/logic/service-dependencies/service-dependencies.module";
 import { ServiceConsumptionModule } from "../../../components/logic/service-consumption/service-consumption.module";
+import {SubstitutionFilterTabComponent} from "./panel-tabs/substitution-filter-tab/substitution-filter-tab.component";
+import {SubstitutionFilterModule} from "../../../components/logic/substitution-filter/substitution-filter.module";
 
 
 
@@ -63,6 +65,7 @@
         ReqAndCapabilitiesTabComponent,
         ServiceConsumptionTabComponent,
         ServiceDependenciesTabComponent,
+        SubstitutionFilterTabComponent,
         RequirementListComponent,
         EnvParamsComponent
     ],
@@ -77,7 +80,8 @@
         TranslateModule,
         NgxDatatableModule,
         ServiceDependenciesModule,
-        ServiceConsumptionModule
+        ServiceConsumptionModule,
+        SubstitutionFilterModule
         // EnvParamsModule
     ],
     entryComponents: [
@@ -91,6 +95,7 @@
         ReqAndCapabilitiesTabComponent,
         ServiceConsumptionTabComponent,
         ServiceDependenciesTabComponent,
+        SubstitutionFilterTabComponent,
         RequirementListComponent,
         PanelTabComponent,
         EnvParamsComponent
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.html
new file mode 100644
index 0000000..d065695
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.html
@@ -0,0 +1,38 @@
+<!--
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2020 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=========================================================
+-->
+
+<ng2-expand-collapse state="0">
+    <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header>
+    <content>
+        <div *ngIf="isComponentInstanceSelected">
+                <substitution-filter
+                    [compositeService]="metaData"
+                    [currentServiceInstance]="component"
+                    [selectedInstanceProperties]="selectedInstanceProperties"
+                    [selectedInstanceSiblings]="selectedInstanceSiblings"
+                    [selectedInstanceConstraints]="selectedInstanceConstraints"
+                    [readonly]="isViewOnly"
+                    (hasSubstitutionFilter)="notifyDependencyEventsObserver($event)"
+                    (updateConstraintListEvent)="updateSelectedInstanceConstraints($event)"
+                    (loadConstraintListEvent)="loadConstraints()">
+                </substitution-filter>
+        </div>
+    </content>
+</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.less
new file mode 100644
index 0000000..3edfbd9
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.less
@@ -0,0 +1,23 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2020 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=========================================================
+ */
+
+:host /deep/ .expand-collapse-content {
+    padding: 0 0 10px;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.ts
new file mode 100644
index 0000000..20868e3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/substitution-filter-tab/substitution-filter-tab.component.ts
@@ -0,0 +1,114 @@
+/*
+* ============LICENSE_START=======================================================
+*  Copyright (C) 2020 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, Input } from '@angular/core';
+import { Store } from '@ngxs/store';
+import {
+    CapabilitiesGroup,
+    Capability,
+    Component as TopologyTemplate,
+    ComponentInstance,
+    FullComponentInstance,
+    InputBEModel,
+    InputsGroup,
+    InterfaceModel,
+    PropertiesGroup,
+    PropertyBEModel,
+} from 'app/models';
+import { SUBSTITUTION_FILTER_EVENTS } from 'app/utils/constants';
+import { ComponentMetadata } from '../../../../../../models/component-metadata';
+import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces';
+import { EventListenerService } from '../../../../../../services/event-listener-service';
+import { ConstraintObject } from '../../../../../components/logic/service-dependencies/service-dependencies.component';
+import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service';
+import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response';
+import { WorkspaceService } from '../../../../workspace/workspace.service';
+import { SelectedComponentType } from '../../../common/store/graph.actions';
+import { CompositionService } from '../../../composition.service';
+
+@Component({
+    selector: 'substitution-filter-tab',
+    templateUrl: 'substitution-filter-tab.component.html',
+    styleUrls: ['substitution-filter-tab.component.less']
+})
+export class SubstitutionFilterTabComponent {
+    isComponentInstanceSelected: boolean;
+
+    selectedInstanceSiblings: ServiceInstanceObject[];
+    componentInstancesConstraints: any[];
+    selectedInstanceConstraints: ConstraintObject[];
+    selectedInstanceProperties: PropertyBEModel[];
+    componentInstanceProperties: PropertiesGroup;
+    metaData: ComponentMetadata;
+
+    @Input() isViewOnly: boolean;
+    @Input() componentType: SelectedComponentType;
+    @Input() component: FullComponentInstance | TopologyTemplate;
+    @Input() input: any;
+
+    constructor(private store: Store,
+                private topologyTemplateService: TopologyTemplateService,
+                private workspaceService: WorkspaceService,
+                private compositionService: CompositionService,
+                private eventListenerService: EventListenerService) {
+    }
+
+    ngOnInit() {
+        this.metaData = this.workspaceService.metadata;
+        this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+        this.initInstancesWithProperties();
+        this.loadConstraints();
+        this.initInstancesWithProperties();
+    }
+
+    public loadConstraints = (): void => {
+        this.topologyTemplateService.getSubstitutionFilterConstraints(this.metaData.componentType, this.metaData.uniqueId).subscribe((response) => {
+            this.componentInstancesConstraints = response.substitutionFilterForTopologyTemplate;
+        });
+    }
+
+    public notifyDependencyEventsObserver = (isChecked: boolean): void => {
+        this.eventListenerService.notifyObservers(SUBSTITUTION_FILTER_EVENTS.ON_SUBSTITUTION_FILTER_CHANGE, isChecked);
+    }
+
+    public updateSelectedInstanceConstraints = (constraintsList:Array<ConstraintObject>):void => {
+        this.componentInstancesConstraints[this.component.uniqueId].properties = constraintsList;
+        this.selectedInstanceConstraints = this.componentInstancesConstraints[this.component.uniqueId].properties;
+    }
+
+    private initInstancesWithProperties = (): void => {
+        this.topologyTemplateService.getComponentInstanceProperties(this.metaData.componentType, this.metaData.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => {
+            this.componentInstanceProperties = genericResponse.componentInstancesProperties;
+            this.updateInstanceAttributes();
+        });
+    }
+
+
+    private updateInstanceAttributes = (): void => {
+        if (this.isComponentInstanceSelected && this.componentInstanceProperties) {
+            const instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({
+                id: coInstance.uniqueId,
+                name: coInstance.name,
+                properties: this.componentInstanceProperties[coInstance.uniqueId] || []
+            }));
+            this.selectedInstanceProperties = this.componentInstanceProperties[this.component.uniqueId];
+            this.selectedInstanceSiblings = instancesMappedList.filter((coInstance) => coInstance.id !== this.component.uniqueId);
+        }
+    }
+}
diff --git a/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts b/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts
index 708742a..003c6dc 100644
--- a/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts
+++ b/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts
@@ -136,9 +136,11 @@
             const selectedSourceType: UIDropDownSourceTypesElement = this.sourceTypes.find(
                 (t) => t.value === this.currentRule.sourceName && t.type === this.currentRule.sourceType
             );
-            this.listOfSourceOptions = selectedSourceType.options || [];
-            this.assignedValueLabel = selectedSourceType.assignedLabel || this.SOURCE_TYPES.STATIC.label;
-            this.filterOptionsByType();
+            if(selectedSourceType) {
+                this.listOfSourceOptions = selectedSourceType.options || [];
+                this.assignedValueLabel = selectedSourceType.assignedLabel || this.SOURCE_TYPES.STATIC.label;
+                this.filterOptionsByType();
+            }
         }
     }
 
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 095c8d6..cf30ea8 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
@@ -380,6 +380,10 @@
         return this.getComponentDataByFieldsName(componentType, componentId, [SERVICE_FIELDS.NODE_FILTER]);
     }
 
+    getSubstitutionFilterConstraints(componentType: string, componentId: string): Observable<ComponentGenericResponse> {
+        return this.getComponentDataByFieldsName(componentType, componentId, [SERVICE_FIELDS.SUBSTITUTION_FILTER]);
+    }
+
     getComponentInstanceProperties(componentType: string, componentId: string): Observable<ComponentGenericResponse> {
         return this.getComponentDataByFieldsName(componentType, componentId, [COMPONENT_FIELDS.COMPONENT_INSTANCES_PROPERTIES]);
     }
@@ -396,6 +400,18 @@
         return this.http.delete<any>(this.baseUrl + this.getServerTypeUrl(componentType) + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/nodeFilter/' + constraintIndex)
     }
 
+    createSubstitutionFilterConstraints(componentMetaDataId: string, componentInstanceId: string, constraint: ConstraintObject, componentType: string): Observable<any> {
+        return this.http.post<any>(this.baseUrl + this.getServerTypeUrl(componentType) + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/substitutionFilter', constraint);
+    }
+
+    updateSubstitutionFilterConstraints(componentMetaDataId: string, componentInstanceId: string, constraint: ConstraintObject, componentType: string): Observable<any> {
+        return this.http.put<any>(this.baseUrl + this.getServerTypeUrl(componentType) + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/substitutionFilter', constraint);
+    }
+
+    deleteSubstitutionFilterConstraints(componentMetaDataId: string, componentInstanceId: string, constraintIndex: number, componentType: string): Observable<any>{
+        return this.http.delete<any>(this.baseUrl + this.getServerTypeUrl(componentType) + componentMetaDataId + '/resourceInstances/' + componentInstanceId + '/substitutionFilter/' + constraintIndex)
+    }
+
     deletePolicy(component: Component, policy: PolicyInstance): Observable<PolicyInstance> {
         return this.http.put<PolicyInstance>(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/policies/' + policy.uniqueId + '/undeclare', policy)
     }
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 301b3a4..2c502d9 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
@@ -57,6 +57,7 @@
     public additionalInformation:any;
     public derivedList:Array<any>;
     public nodeFilterData: Array<any>;
+    public substitutionFilterForTopologyTemplate: Array<any>;
 
     deserialize (response): ComponentGenericResponse {
 
@@ -122,6 +123,9 @@
         if(response.nodeFilterData) {
             this.nodeFilterData = response.nodeFilterData;
         }
+        if(response.substitutionFilterForTopologyTemplate) {
+            this.substitutionFilterForTopologyTemplate = response.substitutionFilterForTopologyTemplate;
+        }
         return this;
     }
 }
diff --git a/catalog-ui/src/app/utils/constants.ts b/catalog-ui/src/app/utils/constants.ts
index f2efc42..a4bceb5 100644
--- a/catalog-ui/src/app/utils/constants.ts
+++ b/catalog-ui/src/app/utils/constants.ts
@@ -366,6 +366,10 @@
     static ON_DEPENDENCY_CHANGE = 'onDependencyStatusChange';
 }
 
+export class SUBSTITUTION_FILTER_EVENTS {
+    static ON_SUBSTITUTION_FILTER_CHANGE = 'onSubstitutionFilterChange';
+}
+
 
 export class COMPONENT_FIELDS {
     static COMPONENT_INSTANCES_PROPERTIES = "componentInstancesProperties";
@@ -396,6 +400,7 @@
 export class SERVICE_FIELDS {
     static FORWARDING_PATHS = "forwardingPaths";
     static NODE_FILTER = "nodeFilter";
+    static SUBSTITUTION_FILTER = "substitutionFilter";
 }
 
 export class API_QUERY_PARAMS {
diff --git a/catalog-ui/src/assets/languages/en_US.json b/catalog-ui/src/assets/languages/en_US.json
index adf5ebe..cdd580c 100644
--- a/catalog-ui/src/assets/languages/en_US.json
+++ b/catalog-ui/src/assets/languages/en_US.json
@@ -510,6 +510,12 @@
     "DIRECTIVES_AND_NODE_FILTER_UPDATE_TITLE": "Update Directives",
     "DIRECTIVES_AND_NODE_FILTER_UPDATE_TEXT": "Changing \"Directive Option\" will remove directives value and erase all the node filter. Are you sure you want to update directives?",
 
+    "============= SUBSTITUTION FILTER MANAGE TAB ======" : "",
+    "ADD_SUBSTITUTION_FILTER": "Add Substitution Filter",
+    "UPDATE_SUBSTITUTION_FILTER": "Update Substitution Filter",
+    "DELETE_SUBSTITUTION_FILTER": "Delete Substitution Filter",
+    "DELETE_SUBSTITUTION_FILTER_MSG": "Are you sure you want to delete this Substitution Filter?",
+
     "============= COMPOSITION DETAILS TAB ======" : "",
     "DETAILS_TAB_CHANGE_VERSION_MODAL_TITLE": "Change Version",
     "DETAILS_TAB_CHANGE_VERSION_MODAL_MSG": "Are you sure you want to change the version?\nIt will affect Service-Consumption and Service-Dependencies data",