This is an automated email from the ASF dual-hosted git repository. wiener pushed a commit to branch edge-extensions in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git
commit 88e204d140f11b6835a101b904ff1d9b79044922 Author: Patrick Wiener <[email protected]> AuthorDate: Fri Apr 2 15:02:07 2021 +0200 [WIP] add new node to cluster --- .../aarch64.Dockerfile => build-docker.sh | 28 ++-- streampipes-node-controller/aarch64.Dockerfile | 8 +- .../orchestrator/docker/utils/DockerUtils.java | 1 + .../controller/container/storage/MapDBImpl.java | 4 + .../StreamPipesExternalDataProcessor.java | 3 +- ui/src/app/configuration/configuration.module.ts | 11 +- .../node-add-details.component.html | 177 ++++++++++++++++++++ .../node-add-details.component.scss | 126 ++++++++++++++ .../node-add-details/node-add-details.component.ts | 183 +++++++++++++++++++++ .../node-configuration.component.html | 110 ++++++++----- .../node-configuration.component.scss | 22 ++- .../node-configuration.component.ts | 21 ++- .../dashboard/registry/widget-config-builder.ts | 4 +- .../node-tag-selector.component.html | 29 ++++ .../node-tag-selector.component.scss | 40 +++++ .../node-tag-selector.component.ts | 79 +++++++++ .../save-pipeline/save-pipeline.component.html | 24 ++- .../save-pipeline/save-pipeline.component.ts | 77 +++++++-- ui/src/app/editor/editor.module.ts | 6 +- 19 files changed, 861 insertions(+), 92 deletions(-) diff --git a/streampipes-node-controller/aarch64.Dockerfile b/build-docker.sh old mode 100644 new mode 100755 similarity index 62% copy from streampipes-node-controller/aarch64.Dockerfile copy to build-docker.sh index b8c87b0..41ba9fd --- a/streampipes-node-controller/aarch64.Dockerfile +++ b/build-docker.sh @@ -1,3 +1,4 @@ +#!/bin/sh # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. @@ -13,21 +14,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -ARG BASE_IMAGE=arm64v8/openjdk:11-jre-slim -FROM $BASE_IMAGE +#!/usr/bin/env bash -MAINTAINER [email protected] +repo=apachestreampipes +version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) -EXPOSE 7077 -ENV CONSUL_LOCATION consul +docker_build(){ + docker build --no-cache --pull \ + -t $repo/$1:$version \ + -f $2/Dockerfile $2 +} -COPY qemu-aarch64-static /usr/bin -RUN set -ex; \ - apt -y update; \ - apt -y --no-install-recommends install curl; \ - apt clean; \ - rm -rf /tmp/apache-* /var/lib/apt/lists/* - -COPY target/streampipes-node-controller.jar /streampipes-node-controller.jar - -ENTRYPOINT ["java", "-jar", "/streampipes-node-controller.jar"] +echo "Start Docker builds ..." +docker_build backend streampipes-backend +docker_build node-controller streampipes-node-controller +docker_build ui ui \ No newline at end of file diff --git a/streampipes-node-controller/aarch64.Dockerfile b/streampipes-node-controller/aarch64.Dockerfile index b8c87b0..856f64a 100644 --- a/streampipes-node-controller/aarch64.Dockerfile +++ b/streampipes-node-controller/aarch64.Dockerfile @@ -14,14 +14,18 @@ # limitations under the License. ARG BASE_IMAGE=arm64v8/openjdk:11-jre-slim -FROM $BASE_IMAGE +FROM arm64v8/ubuntu:18.04 as build-dev +RUN apt -y update; \ + apt -y --no-install-recommends install qemu-user-static + +FROM $BASE_IMAGE MAINTAINER [email protected] EXPOSE 7077 ENV CONSUL_LOCATION consul -COPY qemu-aarch64-static /usr/bin +COPY --from=build-dev /usr/bin/qemu-aarch64-static /usr/bin RUN set -ex; \ apt -y update; \ apt -y --no-install-recommends install curl; \ diff --git a/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/management/orchestrator/docker/utils/DockerUtils.java b/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/management/orchestrator/docker/utils/DockerUtils.java index 4bad83f..5ee6a2b 100644 --- a/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/management/orchestrator/docker/utils/DockerUtils.java +++ b/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/management/orchestrator/docker/utils/DockerUtils.java @@ -183,6 +183,7 @@ public class DockerUtils { .portBindings(portBindings) .networkMode(network) .restartPolicy(HostConfig.RestartPolicy.unlessStopped()) + //.extraHosts() .build(); } diff --git a/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/storage/MapDBImpl.java b/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/storage/MapDBImpl.java index f07302c..6824c87 100644 --- a/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/storage/MapDBImpl.java +++ b/streampipes-node-controller/src/main/java/org/apache/streampipes/node/controller/container/storage/MapDBImpl.java @@ -42,6 +42,7 @@ public class MapDBImpl implements CRUDStorage { } else { db = DBMaker .fileDB(DB_STORAGE_PATH + dbFile) + .transactionEnable() .closeOnJvmShutdown() .make(); } @@ -52,6 +53,7 @@ public class MapDBImpl implements CRUDStorage { @Override public <T> void create(String id, T value) { map.put(id, value); + db.commit(); } @Override @@ -67,11 +69,13 @@ public class MapDBImpl implements CRUDStorage { @Override public <T> void update(String id, T value) { map.put(id, value); + db.commit(); } @Override public void delete(String id) { map.remove(id); + db.commit(); } public void close() { diff --git a/streampipes-wrapper-standalone/src/main/java/org/apache/streampipes/wrapper/standalone/StreamPipesExternalDataProcessor.java b/streampipes-wrapper-standalone/src/main/java/org/apache/streampipes/wrapper/standalone/StreamPipesExternalDataProcessor.java index b3c73c2..1992905 100644 --- a/streampipes-wrapper-standalone/src/main/java/org/apache/streampipes/wrapper/standalone/StreamPipesExternalDataProcessor.java +++ b/streampipes-wrapper-standalone/src/main/java/org/apache/streampipes/wrapper/standalone/StreamPipesExternalDataProcessor.java @@ -36,6 +36,7 @@ public abstract class StreamPipesExternalDataProcessor extends StandaloneExterna implements ExternalEventProcessor<ProcessorParams> { // endpoint of Python processor runs here + private static final String HTTP_PROTOCOL = "http://"; private static final String PYTHON_ENDPOINT = "localhost:5000"; private String invocationId; @@ -88,7 +89,7 @@ public abstract class StreamPipesExternalDataProcessor extends StandaloneExterna String responseString = null; try { - responseString = Request.Post(PYTHON_ENDPOINT + "/" + endpoint) + responseString = Request.Post(HTTP_PROTOCOL + PYTHON_ENDPOINT + "/" + endpoint) .bodyString(payload, ContentType.APPLICATION_JSON) .connectTimeout(1000) .socketTimeout(100000) diff --git a/ui/src/app/configuration/configuration.module.ts b/ui/src/app/configuration/configuration.module.ts index 373fd05..ec904ca 100644 --- a/ui/src/app/configuration/configuration.module.ts +++ b/ui/src/app/configuration/configuration.module.ts @@ -25,7 +25,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; import { FlexLayoutModule } from '@angular/flex-layout'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import { ConfigurationComponent } from './configuration.component'; import { ConfigurationService } from './shared/configuration.service'; @@ -44,6 +44,8 @@ import { DatalakeRestService } from '../core-services/datalake/datalake-rest.ser import { NodeConfigurationComponent } from "./node-configuration/node-configuration.component"; import { NodeConfigurationDetailsComponent } from './node-configuration/node-configuration-details/node-configuration-details.component'; import {MatChipsModule} from "@angular/material/chips"; +import { NodeAddDetailsComponent } from './node-configuration/node-add-details/node-add-details.component'; +import {MatDatepickerModule} from "@angular/material/datepicker"; @NgModule({ imports: [ @@ -58,7 +60,9 @@ import {MatChipsModule} from "@angular/material/chips"; MatTooltipModule, FormsModule, DragDropModule, - MatChipsModule + MatChipsModule, + ReactiveFormsModule, + MatDatepickerModule ], declarations: [ ConfigurationComponent, @@ -73,7 +77,8 @@ import {MatChipsModule} from "@angular/material/chips"; NodeConfigurationComponent, MessagingConfigurationComponent, DatalakeConfigurationComponent, - NodeConfigurationDetailsComponent + NodeConfigurationDetailsComponent, + NodeAddDetailsComponent ], providers: [ ConfigurationService, diff --git a/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.html b/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.html new file mode 100644 index 0000000..c05a54d --- /dev/null +++ b/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.html @@ -0,0 +1,177 @@ +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You 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="sp-dialog-container"> + <div class="sp-dialog-content padding-20"> + <div fxFlex="100" fxLayout="column"> + + <form [formGroup]="nodeMetadataFormGroup"> + <div fxFlex="100" fxLayout="column"> + <mat-form-field> + <mat-label>Backend host (IP/DNS)</mat-label> + <input [formControlName]="'backendHost'" matInput required> + </mat-form-field> + + <mat-form-field> + <mat-label>Node host (IP/DNS)</mat-label> + <input [formControlName]="'nodeHost'" matInput required> + </mat-form-field> + + <mat-form-field> + <mat-label>API token</mat-label> + <input [formControlName]="'token'" matInput required> + </mat-form-field> + </div> + </form> + + <mat-form-field class="example-chip-list"> + <mat-label>Node tags</mat-label> + <mat-chip-list #chipList aria-label="Node location tag"> + <mat-chip *ngFor="let tag of tmpTags" [selectable]="selectable" + [removable]="removable" (removed)="remove(tag)"> + {{tag}} + <mat-icon matChipRemove *ngIf="removable">cancel</mat-icon> + </mat-chip> + <input placeholder="New tag..." + [matChipInputFor]="chipList" + [matChipInputSeparatorKeyCodes]="separatorKeysCodes" + [matChipInputAddOnBlur]="addOnBlur" + (matChipInputTokenEnd)="add($event)"> + </mat-chip-list> + </mat-form-field> + + <mat-slide-toggle color="primary" [(ngModel)]="advancedSettings"> + Configure advanced options + </mat-slide-toggle> + + <div style="margin-top: 1em"> + <mat-accordion class="example-headers-align"> + <mat-expansion-panel [expanded]="advancedSettings"> + <mat-expansion-panel-header> + <mat-panel-title> + Settings + </mat-panel-title> + <mat-panel-description> + Configure advanced options + </mat-panel-description> + </mat-expansion-panel-header> + + <div> + <div fxFlex="100" fxLayout="row"> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="left center"> + <span>Storage path</span> + </div> + </div> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="start center"> + <mat-form-field> + <mat-label></mat-label> + <input [disabled]="!advancedSettings" matInput [value]="storagePath"> + </mat-form-field> + </div> + </div> + + <div> + <div fxFlex="100" fxLayout="row"> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="left center"> + <span>Docker pruning interval (sec)</span> + </div> + </div> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="start center"> + <mat-form-field> + <mat-label></mat-label> + <input [disabled]="!advancedSettings" matInput [value]="dockerNodePruningFreq"> + </mat-form-field> + </div> + </div> + + <div> + <div fxFlex="100" fxLayout="row"> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="left center"> + <span>Resource update interval (sec)</span> + </div> + </div> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="start center"> + <mat-form-field> + <mat-label></mat-label> + <input [disabled]="!advancedSettings"matInput [value]="resourceUpdateFreq"> + </mat-form-field> + </div> + </div> + + <div> + <div fxFlex="100" fxLayout="row"> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="left center"> + <span>Event buffer size (#events)</span> + </div> + </div> + <div fxFlex="50" fxLayout="row" fxLayoutAlign="start center"> + <mat-form-field> + <mat-label></mat-label> + <input [disabled]="!advancedSettings" matInput [value]="eventBufferSize"> + </mat-form-field> + </div> + </div> + </mat-expansion-panel> + </mat-accordion> + + <mat-vertical-stepper *ngIf="dockerCommandCreated" #stepper> + <mat-step [stepControl]="firstFormGroup"> + <form [formGroup]="firstFormGroup"> + <ng-template matStepLabel>Create docker network</ng-template> + <div class="flex-container"> + <div *ngIf="dockerCommandCreated" fxFlex="100" fxLayout="row" fxLayoutAlign="left center" + class="docker-command-container"> + docker network create spnet + </div> + </div> + <div> + <button mat-button matStepperNext>Next</button> + </div> + </form> + </mat-step> + <mat-step [stepControl]="secondFormGroup"> + <form [formGroup]="secondFormGroup"> + <ng-template matStepLabel>Start node controller container</ng-template> + <div class="flex-container"> + <div *ngIf="dockerCommandCreated" fxFlex="100" fxLayout="row" fxLayoutAlign="left center" + class="docker-command-container"> + {{dockerRunCommand}} + </div> + </div> + <div> + <button mat-button (click)="hide()">Done</button> + </div> + </form> + </mat-step> + </mat-vertical-stepper> + + + </div> + </div> + </div> + <mat-divider></mat-divider> + <div class="sp-dialog-actions" fxLayoutAlign="left center"> + <button [disabled]="!nodeMetadataFormGroup.valid || added" mat-button mat-raised-button + color="primary" (click)="createDocker()" style="margin-right:10px;"> + Create Docker Command + </button> + <button mat-button mat-raised-button class="mat-basic" (click)="hide()"> + {{added ? 'Close' : 'Cancel'}} + </button> + </div> +</div> diff --git a/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.scss b/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.scss new file mode 100644 index 0000000..77add90 --- /dev/null +++ b/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.scss @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +@import '../../../../scss/sp/sp-dialog.scss'; + +.sp-dialog-container { + width: 520px; +} + +.customize-section { + display:flex; + flex: 1 1 auto; + padding: 20px; +} + +.padding-20 { + padding: 20px; +} + +.mb-10 { + margin-bottom: 10px; +} + +::ng-deep .pipeline-radio-group .mat-radio-label { + padding: 0; +} + +.status-text { + font-size: 14pt; + margin-top:10px; +} + +.status-subtext { + font-size: 12pt; +} + +mat-slider { + width: 300px; +} + +.execution-policy-radio-group { + display: flex; + flex-direction: column; + //margin: 15px 0; +} + +.execution-policy-radio-button { + margin: 5px; +} + + +.execution-policy-action-buttons { + padding-bottom: 20px; +} + +.example-headers-align .mat-expansion-panel-header-title, +.example-headers-align .mat-expansion-panel-header-description { + flex-basis: 0; +} + +.example-headers-align .mat-expansion-panel-header-description { + justify-content: space-between; + align-items: center; +} + +.example-headers-align .mat-form-field + .mat-form-field { + margin-left: 8px; +} + +.mat-form-field[dense] { + .mat-form-field-flex > .mat-form-field-infix { + padding: 0.4em 0px !important; + } + .mat-form-field-label-wrapper { + top: -1.5em; + } + &.mat-form-field-appearance-outline.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label { + transform: translateY(-1.1em) scale(.75); + } + .mat-form-field-wrapper{ + padding-bottom: 0; + } +} + +.flex-container { + margin-top: 1em; + width: 100% +} + +.new-docker-command { + width: 200px; + margin-top: 10px; + margin-bottom: 10px; +} + +.docker-tag { + margin-right: 10px; + font-weight: bold; + padding: 10px; +} + +.docker-command-container { + background: #f6f6f6; + border: 1px solid #cccccc; + padding: 10px; + color: black; + //margin-left: 10px; + //margin-right: 10px; + overflow-x: auto; + white-space: nowrap; +} diff --git a/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.ts b/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.ts new file mode 100644 index 0000000..b41bd7b --- /dev/null +++ b/ui/src/app/configuration/node-configuration/node-add-details/node-add-details.component.ts @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, OnInit } from '@angular/core'; +import {NodeService} from "../../../platform-services/apis/node.service"; +import {DialogRef} from "../../../core-ui/dialog/base-dialog/dialog-ref"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; +import {STEPPER_GLOBAL_OPTIONS} from "@angular/cdk/stepper"; +import {COMMA, ENTER} from "@angular/cdk/keycodes"; +import {MatChipInputEvent} from "@angular/material/chips"; +import {VersionInfo} from "../../../info/versions/service/version-info.model"; +import {RestApi} from "../../../services/rest-api.service"; + +@Component({ + selector: 'node-add-details', + templateUrl: './node-add-details.component.html', + styleUrls: ['./node-add-details.component.scss'], + // providers: [{ + // provide: STEPPER_GLOBAL_OPTIONS, useValue: {showError: true} + // }] +}) +export class NodeAddDetailsComponent implements OnInit { + + nodeMetadataFormGroup: FormGroup = new FormGroup({}); + + added: boolean = false; + advancedSettings: boolean; + dockerCommandCreated: boolean; + errorMessage: string = ''; + + selectable = true; + visible = true; + removable = true; + addOnBlur = true; + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + tmpTags: string[]; + + versionInfo: VersionInfo; + backend: string = ''; + nodeIp: string = ''; + apiToken: string = ''; + storagePath: string = '/var/lib/streampipes'; + resourceUpdateFreq: string = '30'; + dockerNodePruningFreq: string = '3600'; + eventBufferSize: string = '1000'; + + dockerRunCommand: string; + + firstFormGroup: FormGroup; + secondFormGroup: FormGroup; + + + constructor(private nodeService: NodeService, + private dialogRef: DialogRef<NodeAddDetailsComponent>, + private restApi: RestApi, + private _snackBar: MatSnackBar, + private _formBuilder: FormBuilder) { } + + ngOnInit(): void { + this.dockerCommandCreated = false; + this.advancedSettings = false; + this.tmpTags = [] + this.getVersion(); + + this.nodeMetadataFormGroup.addControl("backendHost", new FormControl(this.backend, + [Validators.required, + Validators.maxLength(40)])) + + this.nodeMetadataFormGroup.addControl("nodeHost", new FormControl(this.nodeIp, + [Validators.required, + Validators.maxLength(15)])) + this.nodeMetadataFormGroup.addControl("token", new FormControl(this.apiToken, + [Validators.maxLength(80)])) + + + this.nodeMetadataFormGroup.controls["backendHost"].valueChanges.subscribe(value => { + this.backend = value; + }); + + this.nodeMetadataFormGroup.controls["nodeHost"].valueChanges.subscribe(value => { + this.nodeIp = value; + }); + + this.nodeMetadataFormGroup.controls["token"].valueChanges.subscribe(value => { + this.apiToken = value; + }); + + this.firstFormGroup = this._formBuilder.group({ + firstCtrl: ['', Validators.required] + }); + this.secondFormGroup = this._formBuilder.group({ + secondCtrl: ['', Validators.required] + }); + + } + + hide() { + this.dialogRef.close(); + } + + add(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; + if ((value || '').trim()) { + this.tmpTags.push(value.trim()); + } + // Reset the input value + if (input) { + input.value = ''; + } + } + + remove(tag: string): void { + const index = this.tmpTags.indexOf(tag); + if (index >= 0) { + this.tmpTags.splice(index, 1); + } + } + + openSnackBar(message: string) { + this._snackBar.open(message, "close", { + duration: 3000, + }); + } + + createDocker() { + // add new node + this.buildDockerCommand(); + this.dockerCommandCreated = true; + } + + buildDockerCommand() { + this.dockerRunCommand = 'docker run -d -p 7077:7077 '; + this.dockerRunCommand += '--restart always '; + this.dockerRunCommand += '--hostname streampipes-node-controller ' + this.dockerRunCommand += '--name streampipes-node-controller '; + this.dockerRunCommand += `-e SP_URL=http://${this.backend}:8030 `; + this.dockerRunCommand += `-e SP_NODE_CONTROLLER_URL=http://${this.nodeIp}:7077 `; + this.dockerRunCommand += `-e SP_API_KEY=${this.apiToken} `; + if (this.tmpTags.length > 0) { + this.dockerRunCommand += `-e SP_NODE_LOCATION="${this.tagsToString(this.tmpTags)}" `; + } + if (this.advancedSettings) { + this.dockerRunCommand += `-e SP_DOCKER_PRUNING_FREQ_SECS=${this.dockerNodePruningFreq} `; + this.dockerRunCommand += `-e SP_NODE_RESOURCE_UPDATE_FREQ_SECS=${this.resourceUpdateFreq} `; + this.dockerRunCommand += `-e SP_NODE_EVENT_BUFFER_SIZE=${this.eventBufferSize} `; + } + this.dockerRunCommand += '-v /var/run/docker.sock:/var/run/docker.sock '; + this.dockerRunCommand += '-v /var/lib/streampipes:/var/lib/streampipes '; + this.dockerRunCommand += `apachestreampipes/node-controller:${this.versionInfo.backendVersion}`; + + } + + tagsToString(tags: string[]){ + return tags.join(';') + } + + getVersion(){ + this.restApi.getVersionInfo().subscribe((response) => { + this.versionInfo = response as VersionInfo; + }) + } + + copyDockerCmd() { + + } +} diff --git a/ui/src/app/configuration/node-configuration/node-configuration.component.html b/ui/src/app/configuration/node-configuration/node-configuration.component.html index e339f40..e94dcf2 100644 --- a/ui/src/app/configuration/node-configuration/node-configuration.component.html +++ b/ui/src/app/configuration/node-configuration/node-configuration.component.html @@ -25,6 +25,15 @@ </div> </div> <div fxFlex="100" fxLayoutAlign="start start" class="sp-blue-border page-container-padding-inner"> + <div fxLayoutAlign="center right"> + <button class="md-raised md-primary submit-button" + mat-fab color="accent" + type="submit" + matTooltip="add new node" + (click)="addNode()"> + <mat-icon>add</mat-icon> + </button> + </div> <mat-grid-list [cols]="3" [rowHeight]="300" [gutterSize]="10" fxFlex="100"> @@ -32,47 +41,56 @@ <mat-grid-tile class="gray" *ngFor="let node of nodes"> <mat-card class="node-card"> <mat-card-header class="header"> - <div class="div-icon-node-actions"> - <button mat-icon-button class="node-mat-icon-button" - [disabled]="node.active" - (click)="changeNodeState(node, true)"> - <mat-icon - [ngClass]="node.active ? 'node-activated' : 'node-inactive'" - matTooltip="{{node.active ? 'node alive and running' : 'activate node'}}" - matTooltipPosition="above"> - play_circle_filled - </mat-icon> - </button> - <button mat-icon-button class="node-mat-icon-button" - [disabled]="!node.active" - (click)="changeNodeState(node, false)"> - <mat-icon - [ngClass]="node.active ? 'node-inactive' : 'node-deactivated'" - matTooltip="{{!node.active ? 'node deactivated' : 'deactivate node'}}" - matTooltipPosition="above"> - pause_circle_filled - </mat-icon> - </button> - <button mat-icon-button class="node-mat-icon-button" - [disabled]="node.active" - (click)="evict(node)"> - <mat-icon - [ngClass]="node.active ? 'node-inactive' : 'node-evict'" - matTooltip="{{!node.active ? 'evict processors' : - 'evict processors option not available'}}" - matTooltipPosition="above"> - swap_horizontal_circle_filled - </mat-icon> - </button> - <button mat-icon-button class="node-mat-icon-button" - matTooltip="Edit node configuration" - matTooltipPosition="above" - (click)="settings(node)"> - <mat-icon - [ngClass]="'node-inactive'"> - info - </mat-icon> + <div class="div-menu"> + <button mat-icon-button [matMenuTriggerFor]="menu" aria-label="node menu"> + <mat-icon>more_vert</mat-icon> </button> + <mat-menu #menu="matMenu" style="font-size: 10pt"> + <button mat-menu-item + *ngIf="!node.active" + (click)="changeNodeState(node, true)"> + <mat-icon + [ngClass]="node.active ? 'node-activated' : 'node-inactive'" + matTooltip="{{node.active ? 'node alive and running' : 'activate node'}}" + matTooltipPosition="above"> + play_circle_filled + </mat-icon> + <span>activate</span> + </button> + <button mat-menu-item + *ngIf="node.active" + (click)="changeNodeState(node, false)"> + <mat-icon + [ngClass]="node.active ? 'node-inactive' : 'node-deactivated'" + matTooltip="{{!node.active ? 'node deactivated' : 'deactivate node'}}" + matTooltipPosition="above"> + pause_circle_filled + </mat-icon> + <span>deactivate</span> + </button> +<!-- <button mat-menu-item--> +<!-- [disabled]="node.active"--> +<!-- matTooltip="{{!node.active ? 'evict processors' :--> +<!-- 'evict processors option not available'}}"--> +<!-- (click)="evict(node)">--> +<!-- <mat-icon--> +<!-- [ngClass]="node.active ? 'node-inactive' : 'node-evict'"--> +<!-- matTooltipPosition="above">--> +<!-- swap_horizontal_circle_filled--> +<!-- </mat-icon>--> +<!-- <span>evict</span>--> +<!-- </button>--> + <button mat-menu-item + matTooltip="Edit node configuration" + matTooltipPosition="above" + (click)="settings(node)"> + <mat-icon + [ngClass]="'node-inactive'"> + sell + </mat-icon> + <span>tags</span> + </button> + </mat-menu> </div> <div mat-card-avatar class="node-header-avatar"> <button mat-icon-button class="node-mat-icon-button" disabled> @@ -87,6 +105,8 @@ {{node.nodeControllerId}} | <b>{{node.nodeResources.softwareResource.os}}</b> <div style="color: lightgrey;"> last contact: {{currentTimestamp}} + <span *ngIf="node.active" class="span-node-active">active</span> + <span *ngIf="!node.active" class="span-node-inactive">inactive</span> </div> </mat-card-subtitle> </mat-card-header> @@ -124,6 +144,16 @@ <!-- </div>--> </mat-card-content> <mat-card-actions> +<!-- <mat-divider></mat-divider>--> +<!-- <div style="margin-top: 1em" fxLayout="row"--> +<!-- fxLayoutAlign="start center" fxFlex="100">--> +<!-- <div style="margin: auto; border: 1px solid rgb(57, 181, 74); color: rgb(57, 181, 74)">--> +<!-- <button mat-button>--> +<!-- <mat-icon>add_circle_outline</mat-icon>--> +<!-- add container--> +<!-- </button>--> +<!-- </div>--> +<!-- </div>--> </mat-card-actions> </mat-card> </mat-grid-tile> diff --git a/ui/src/app/configuration/node-configuration/node-configuration.component.scss b/ui/src/app/configuration/node-configuration/node-configuration.component.scss index ad31613..4c0a4ff 100644 --- a/ui/src/app/configuration/node-configuration/node-configuration.component.scss +++ b/ui/src/app/configuration/node-configuration/node-configuration.component.scss @@ -69,6 +69,24 @@ li { padding: 5px; } +.span-node-active{ + font-size: 12px; + font-weight: bold; + color: white; + background: $sp-color-primary; + border: 1px solid $sp-color-primary; + padding: 2px; +} + +.span-node-inactive{ + font-size: 12px; + font-weight: bold; + color: #f1c40f; + background: white; + border: 1px solid #f1c40f; + padding: 2px; +} + .div-sa-tag{ display: inline-block; margin-bottom: 10px; @@ -87,14 +105,14 @@ li { padding-top: 6px; } -.div-icon-node-actions { +.div-menu { margin-left: auto; margin-right: 0; } .node-state { //float:right; - transform: scale(1.3); + transform: scale(1); } .node-activated { diff --git a/ui/src/app/configuration/node-configuration/node-configuration.component.ts b/ui/src/app/configuration/node-configuration/node-configuration.component.ts index 46cd508..0e0369e 100644 --- a/ui/src/app/configuration/node-configuration/node-configuration.component.ts +++ b/ui/src/app/configuration/node-configuration/node-configuration.component.ts @@ -18,9 +18,10 @@ import {Component, OnInit, ViewEncapsulation} from "@angular/core"; import { + ContainerRuntime, ContainerRuntimeUnion, DockerContainerRuntime, FieldDeviceAccessResource, NodeInfoDescription, - NvidiaContainerRuntime + NvidiaContainerRuntime, UnnamedStreamPipesEntity } from "../../core-model/gen/streampipes-model"; import {MatSnackBar} from "@angular/material/snack-bar"; import {zip} from "rxjs"; @@ -30,6 +31,7 @@ import {DataMarketplaceService} from "../../connect/services/data-marketplace.se import {NodeService} from "../../platform-services/apis/node.service"; import {PanelType} from "../../core-ui/dialog/base-dialog/base-dialog.model"; import {NodeConfigurationDetailsComponent} from "./node-configuration-details/node-configuration-details.component"; +import {NodeAddDetailsComponent} from "./node-add-details/node-add-details.component"; @Component({ selector: 'node-configuration', @@ -92,11 +94,12 @@ export class NodeConfigurationComponent implements OnInit{ }; nvidiaRuntime(node: NodeInfoDescription) { - let nvidiaRuntime = false; - if (node.nodeResources.softwareResource.containerRuntime instanceof NvidiaContainerRuntime) { - nvidiaRuntime = true; + const containerRuntime = node.nodeResources.softwareResource.containerRuntime; + if(containerRuntime['@class'] === "org.apache.streampipes.model.node.resources.software.NvidiaContainerRuntime"){ + return true + } else { + return false } - return nvidiaRuntime; } async changeNodeState(node: NodeInfoDescription, desiredState: boolean) { @@ -175,6 +178,14 @@ export class NodeConfigurationComponent implements OnInit{ }); } + addNode() { + this.DialogService.open(NodeAddDetailsComponent,{ + panelType: PanelType.SLIDE_IN_PANEL, + title: "Add new node", + data: {} + }); + } + getDate() { this.currentTimestamp = new Date().toLocaleTimeString(['en-US'], { hour: '2-digit', minute: "2-digit", second: "2-digit" }); diff --git a/ui/src/app/dashboard/registry/widget-config-builder.ts b/ui/src/app/dashboard/registry/widget-config-builder.ts index e5136ed..da0332e 100644 --- a/ui/src/app/dashboard/registry/widget-config-builder.ts +++ b/ui/src/app/dashboard/registry/widget-config-builder.ts @@ -45,9 +45,9 @@ export class WidgetConfigBuilder { } if (withColors) { this.requiredColorParameter(WidgetConfigBuilder.BACKGROUND_COLOR_KEY, "Background color", "The background" + - " color", "#1B1464"); + " color", "#F6F6F6"); this.requiredColorParameter(WidgetConfigBuilder.PRIMARY_TEXT_COLOR_KEY, "Primary text color", "The" + - " primary text color", "#FFFFFF"); + " primary text color", "#333"); this.requiredColorParameter(WidgetConfigBuilder.SECONDARY_TEXT_COLOR_KEY, "Secondary text color", "The" + " secondary text" + " color", "#39B54A"); diff --git a/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.html b/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.html new file mode 100644 index 0000000..38083b2 --- /dev/null +++ b/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.html @@ -0,0 +1,29 @@ +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You 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 style="margin-top: 1em; margin-bottom: 1em"> + <mat-chip-list #chipList multiple="true" class="my-chip"> + <mat-chip *ngFor="let tag of nodeTags" + [value]="tag.name" + [selected]="tag.selected" + (click)="onSelectTag(tag)"> + {{tag.name}} + </mat-chip> + </mat-chip-list> + <div class="hint-label">Filter nodes by tags</div> +</div> \ No newline at end of file diff --git a/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.scss b/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.scss new file mode 100644 index 0000000..0b92957 --- /dev/null +++ b/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.scss @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +.hint-label{ + padding-left:5px; + color:rgba(0,0,0,.54); + font-size:10px; + padding-top:10px; +} + +.my-chip .mat-chip-selected { + background-color: rgb(57, 181, 74) !important; + border: 1px solid rgb(57, 181, 74); + color: #fff !important; +} + +.mat-chip.mat-standard-chip { + background: white; + border: 1px solid rgb(57, 181, 74); + color: rgb(57, 181, 74); +} + +.mat-chip.mat-standard-chip::after { + background: white; +} \ No newline at end of file diff --git a/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.ts b/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.ts new file mode 100644 index 0000000..5f91b24 --- /dev/null +++ b/ui/src/app/editor/dialog/save-pipeline/node-tag-selector/node-tag-selector.component.ts @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {NodeInfoDescription} from "../../../../core-model/gen/streampipes-model"; + +export interface NodeTags { + name: string; + selected: boolean; +} +@Component({ + selector: 'node-tag-selector', + templateUrl: './node-tag-selector.component.html', + styleUrls: ['./node-tag-selector.component.scss'] +}) +export class NodeTagSelectorComponent implements OnInit { + + @Input() + nodes: NodeInfoDescription[]; + + @Output() + createDynamicallySelectedTags: EventEmitter<NodeInfoDescription[]> = new EventEmitter<NodeInfoDescription[]>(); + + filteredNodes: NodeInfoDescription[] = []; + nodeTags: NodeTags[] = []; + dynamicallySelectedTags: NodeTags["name"][] = []; + + ngOnInit(): void { + this.nodes.forEach(node => { + if (node.supportedElements.length > 0 || node.registeredContainers.length > 0) { + for (let tag of node.staticNodeMetadata.locationTags) { + if (!this.nodeTags.some(n => n.name === tag)) { + this.nodeTags.push({'name': tag, selected: false}) + } + } + } + }) + } + + onSelectTag(tag: NodeTags) { + tag.selected=!tag.selected + this.dynamicallySelectedTags = []; + for (let tag of this.nodeTags) { + if(tag.selected){ + this.dynamicallySelectedTags.push(tag.name); + } + } + this.filterAndEmitNodes(); + } + + filterAndEmitNodes() { + this.filteredNodes = []; + this.nodes.forEach(node => { + node.staticNodeMetadata.locationTags.forEach(tag => { + if (this.dynamicallySelectedTags.includes(tag)) { + if (!this.filteredNodes.includes(node)) { + this.filteredNodes.push(node); + } + } + }); + }); + this.createDynamicallySelectedTags.emit(this.filteredNodes); + } +} diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html index 4f16b6f..1ccb9a7 100644 --- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html +++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html @@ -55,7 +55,7 @@ <b>Pipeline Operation Policies</b> </div> - <div style="margin-top: 2em"> + <div style="margin-top: 1em"> <div fxFlex="100" fxLayout="row"> <div fxFlex="50" fxLayout="row" fxLayoutAlign="left top"> <span>Relay Policy</span> @@ -93,9 +93,13 @@ <div style="margin: 1em 0 1em 0;"> <div> - <b>Node Execution Targets</b> + <b>Deployment Targets</b> + </div> + <div style="margin: 1em 0 1em 0;" *ngIf="selectedPipelineExecutionPolicy=='custom'"> + <node-tag-selector [nodes]="edgeNodes" + (createDynamicallySelectedTags)="nodesFromSelectedTags($event)"></node-tag-selector> </div> - <div style="margin-top: 2em"> + <div style="margin-top: 1em"> <mat-accordion class="example-headers-align"> <mat-expansion-panel (opened)="panelOpenState = true" @@ -119,9 +123,10 @@ <mat-select [(ngModel)]="processors.deploymentTargetNodeId" [disabled]="disableNodeSelection.value" required> - <mat-option [value]="nodeInfo.nodeControllerId" - *ngFor="let nodeInfo of deploymentOptions[processors.appId]"> - <em>{{nodeInfo.hostname}}</em> + <mat-option + *ngFor="let node of deploymentOptions[processors.appId]" + [value]="node.nodeControllerId"> + <em>{{node.hostname}}</em> </mat-option> </mat-select> </mat-form-field> @@ -138,9 +143,10 @@ <mat-select [(ngModel)]="sinks.deploymentTargetNodeId" [disabled]="disableNodeSelection.value" required> - <mat-option [value]="nodeInfo.nodeControllerId" - *ngFor="let nodeInfo of deploymentOptions[sinks.appId]"> - <em>{{nodeInfo.hostname}}</em> + <mat-option + *ngFor="let node of deploymentOptions[sinks.appId]" + [value]="node.nodeControllerId"> + <em>{{node.hostname}}</em> </mat-option> </mat-select> </mat-form-field> diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts index 2571ead..2b7cf7a 100644 --- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts +++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts @@ -23,7 +23,13 @@ import { Message, Pipeline, NodeInfoDescription, - StaticNodeMetadata, NvidiaContainerRuntime, DockerContainerRuntime, ContainerRuntime, DataSinkInvocation + StaticNodeMetadata, + NvidiaContainerRuntime, + DockerContainerRuntime, + ContainerRuntime, + DataSinkInvocation, + SpDataStream, + SpDataStreamUnion } from "../../../core-model/gen/streampipes-model"; import {ObjectProvider} from "../../services/object-provider.service"; import {EditorService} from "../../services/editor.service"; @@ -57,6 +63,8 @@ export class SavePipelineComponent implements OnInit { tmpPipeline: Pipeline; panelOpenState: boolean; + filteredNodes = new FormControl(); + pipelineExecutionPolicies: string[] = ['default', 'locality-aware', 'custom']; @Input() @@ -111,7 +119,7 @@ export class SavePipelineComponent implements OnInit { } else { this.selectedRelayStrategyVal = "buffer"; this.selectedPipelineExecutionPolicy = "locality-aware"; - this.applyLocalityAwarePolicy(this.tmpPipeline.streams, this.tmpPipeline.sepas); + this.applyLocalityAwarePolicy(); } } @@ -146,23 +154,55 @@ export class SavePipelineComponent implements OnInit { }); }; - applyLocalityAwarePolicy(streams, pipelineElements) { - streams.forEach(s => { + applyLocalityAwarePolicy() { + this.tmpPipeline.streams.forEach(s => { //let processors: DataProcessorInvocation[]; //processors = this.pipeline.sepas.filter(p => p.connectedTo.some(entry => entry == s.dom)); - pipelineElements.forEach(p => { - p.deploymentTargetNodeId = s.deploymentTargetNodeId; - p.deploymentTargetNodeHostname = s.deploymentTargetNodeHostname; - p.deploymentTargetNodePort = s.deploymentTargetNodePort; + this.tmpPipeline.sepas.forEach(processor => { + processor.deploymentTargetNodeId = s.deploymentTargetNodeId; + processor.deploymentTargetNodeHostname = s.deploymentTargetNodeHostname; + processor.deploymentTargetNodePort = s.deploymentTargetNodePort; }); + + // this.tmpPipeline.actions.forEach(p => { + // p.deploymentTargetNodeId = s.deploymentTargetNodeId; + // p.deploymentTargetNodeHostname = s.deploymentTargetNodeHostname; + // p.deploymentTargetNodePort = s.deploymentTargetNodePort; + // }); }); } - private applyDefaultPolicy(pipelineElements) { - pipelineElements.forEach(p => { + private applyTagBasedPolicy(filteredNodes: NodeInfoDescription[]) { + if (filteredNodes.length > 0) { + + this.tmpPipeline.sepas.forEach(processor => { + this.deploymentOptions[processor.appId] = []; + + filteredNodes.forEach(filteredNode => { + + if (filteredNode.supportedElements.length != 0 && + filteredNode.supportedElements.some(appId => appId === processor.appId)) { + this.deploymentOptions[processor.appId].push(filteredNode); + } + }) + }) + + } else { + this.addAppIds(this.tmpPipeline.sepas, this.edgeNodes); + this.addAppIds(this.tmpPipeline.actions, this.edgeNodes); + } + } + + private applyDefaultPolicy() { + this.tmpPipeline.sepas.forEach(p => { p.deploymentTargetNodeId = "default"; // this.deploymentOptions[p.appId].push(this.makeDefaultNodeInfo()); }); + + // this.tmpPipeline.actions.forEach(p => { + // p.deploymentTargetNodeId = "default"; + // // this.deploymentOptions[p.appId].push(this.makeDefaultNodeInfo()); + // }); } loadAndPrepareEdgeNodes() { @@ -303,11 +343,24 @@ export class SavePipelineComponent implements OnInit { } else if (value == "locality-aware") { this.panelOpenState = false; this.disableNodeSelection.setValue(true); - this.applyLocalityAwarePolicy(this.tmpPipeline.streams, this.tmpPipeline.sepas) + this.applyLocalityAwarePolicy() } else if (value == "default") { this.panelOpenState = false; this.disableNodeSelection.setValue(true); - this.applyDefaultPolicy(this.tmpPipeline.sepas); + this.applyDefaultPolicy(); } + // else if (value == "tag-based") { + // this.panelOpenState = true; + // this.disableNodeSelection.setValue(false); + // } + } + + nodesFromSelectedTags(filteredNodes: NodeInfoDescription[]) { + this.applyTagBasedPolicy(filteredNodes) + } + + + compareFn(c1: any, c2:any): boolean { + return c1 && c2 ? c1.id === c2.id : c1 === c2; } } \ No newline at end of file diff --git a/ui/src/app/editor/editor.module.ts b/ui/src/app/editor/editor.module.ts index 71e291d..ace0a0f 100644 --- a/ui/src/app/editor/editor.module.ts +++ b/ui/src/app/editor/editor.module.ts @@ -67,6 +67,8 @@ import {PipelineElementDraggedService} from "./services/pipeline-element-dragged import {PipelineCanvasScrollingService} from "./services/pipeline-canvas-scrolling.service"; import {JsplumbEndpointService} from "./services/jsplumb-endpoint.service"; import {JsplumbFactoryService} from "./services/jsplumb-factory.service"; +import { NodeTagSelectorComponent } from './dialog/save-pipeline/node-tag-selector/node-tag-selector.component'; +import {MatChipsModule} from "@angular/material/chips"; @NgModule({ imports: [ @@ -86,6 +88,7 @@ import {JsplumbFactoryService} from "./services/jsplumb-factory.service"; ReactiveFormsModule, MatSliderModule, ReactiveFormsModule, + MatChipsModule, ], declarations: [ CompatibleElementsComponent, @@ -110,7 +113,8 @@ import {JsplumbFactoryService} from "./services/jsplumb-factory.service"; SavePipelineComponent, SafeCss, WelcomeTourComponent, - MigratePipelineProcessorsComponent + MigratePipelineProcessorsComponent, + NodeTagSelectorComponent ], providers: [ EditorService,
