src/app/streams/stream-create/stream-create.component.ts
encapsulation | ViewEncapsulation.None |
selector | app-stream-create |
styleUrls | .../shared/flo/flo.scss |
templateUrl | ./stream-create.component.html |
Properties |
|
Methods |
HostListeners |
Accessors |
constructor(metamodelService: MetamodelService, renderService: RenderService, editorService: EditorService, bsModalService: BsModalService, renderer: Renderer2, notificationService: NotificationService, contentAssistService: ContentAssistService, loggerService: LoggerService, parserService: ParserService)
|
||||||||||||||||||||||||||||||||||||||||
Parameters :
|
window:resize |
Arguments : '$event'
|
window:resize(event: )
|
arrangeAll |
arrangeAll()
|
Returns :
void
|
contentAssist | ||||||||
contentAssist(doc: CodeMirror.EditorFromTextArea)
|
||||||||
Parameters :
Returns :
any
|
createStreamDefs |
createStreamDefs()
|
Returns :
void
|
findLast |
findLast(string: string, predicate: (s: string) => void, start?: number)
|
Returns :
number
|
interestingPrefixStart | ||||||||||||
interestingPrefixStart(prefix: string, completions: Array
|
||||||||||||
The suggestions provided by rest api are very long and include the whole command typed from the start of the line. This function determines the start of the 'interesting' part at the end of the prefix, so that we can use it to chop-off the suggestion there.
Parameters :
Returns :
any
|
isDelimiter | ||||||||
isDelimiter(c: string)
|
||||||||
Parameters :
Returns :
any
|
lint | ||||||||||||||||
lint(dsl: string, updateLintingCallback: CodeMirror.UpdateLintingCallback, editor: CodeMirror.Editor)
|
||||||||||||||||
Parameters :
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Will cleanup any {@link Subscription}s to prevent memory leaks.
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
resizeFloGraph | ||||||||
resizeFloGraph(height?: number)
|
||||||||
Parameters :
Returns :
void
|
setEditorContext | ||||||||
setEditorContext(editorContext: Flo.EditorContext)
|
||||||||
Parameters :
Returns :
void
|
contentValidated |
contentValidated:
|
Default value : false
|
dsl |
dsl:
|
Type : string
|
editorContext |
editorContext:
|
Type : Flo.EditorContext
|
Public editorService |
editorService:
|
Type : EditorService
|
flo |
flo:
|
Decorators : ViewChild
|
hintOptions |
hintOptions:
|
Type : any
|
initSubject |
initSubject:
|
Type : Subject<void>
|
lintOptions |
lintOptions:
|
Type : CodeMirror.LintOptions
|
Public metamodelService |
metamodelService:
|
Type : MetamodelService
|
Private ngUnsubscribe$ |
ngUnsubscribe$:
|
Type : Subject<any>
|
paletteSize |
paletteSize:
|
Default value : 310
|
parseErrors |
parseErrors:
|
Type : Parser.Error[]
|
Public renderService |
renderService:
|
Type : RenderService
|
validationMarkers |
validationMarkers:
|
Type : Map<string | []>
|
gridOn | ||||||||
getgridOn()
|
||||||||
Returns :
boolean
|
||||||||
setgridOn(on: boolean)
|
||||||||
Parameters :
Returns :
void
|
isCreateStreamsDisabled |
getisCreateStreamsDisabled()
|
Returns :
boolean
|
import {
Component, OnDestroy, OnInit, Renderer2,
ViewChild, ViewEncapsulation, HostListener
} from '@angular/core';
import { Flo } from 'spring-flo';
import { ParserService } from '../../shared/services/parser.service';
import { Parser } from '../../shared/services/parser';
import { MetamodelService } from '../components/flo/metamodel.service';
import { RenderService } from '../components/flo/render.service';
import { EditorService } from '../components/flo/editor.service';
import { BsModalService } from 'ngx-bootstrap';
import { StreamCreateDialogComponent } from './create-dialog/create-dialog.component';
import { ContentAssistService } from '../components/flo/content-assist.service';
import * as CodeMirror from 'codemirror';
import { Subject, Subscription } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { LoggerService } from '../../shared/services/logger.service';
import { EditorComponent } from 'spring-flo';
import { NotificationService } from '../../shared/services/notification.service';
/**
* @author Gunnar Hillert
*/
@Component({
selector: 'app-stream-create',
templateUrl: './stream-create.component.html',
styleUrls: ['../../shared/flo/flo.scss'],
encapsulation: ViewEncapsulation.None
})
export class StreamCreateComponent implements OnInit, OnDestroy {
private ngUnsubscribe$: Subject<any> = new Subject();
dsl: string;
editorContext: Flo.EditorContext;
paletteSize = 310;
hintOptions: any;
lintOptions: CodeMirror.LintOptions;
validationMarkers: Map<string, Flo.Marker[]>;
parseErrors: Parser.Error[] = [];
contentValidated = false;
initSubject: Subject<void>;
@ViewChild(EditorComponent, { static: true }) flo;
constructor(public metamodelService: MetamodelService,
public renderService: RenderService,
public editorService: EditorService,
private bsModalService: BsModalService,
private renderer: Renderer2,
private notificationService: NotificationService,
private contentAssistService: ContentAssistService,
private loggerService: LoggerService,
private parserService: ParserService) {
this.validationMarkers = new Map();
this.hintOptions = {
async: true,
hint: (doc: CodeMirror.EditorFromTextArea) => this.contentAssist(doc)
};
this.lintOptions = {
async: true,
hasGutters: true,
getAnnotations: (content: string,
updateLintingCallback: CodeMirror.UpdateLintingCallback,
options: CodeMirror.LintStateOptions,
editor: CodeMirror.Editor) => this.lint(content, updateLintingCallback, editor)
};
this.initSubject = new Subject();
this.initSubject
.pipe(takeUntil(this.ngUnsubscribe$))
.subscribe();
}
ngOnInit() {
this.resizeFloGraph();
}
/**
* Will cleanup any {@link Subscription}s to prevent
* memory leaks.
*/
ngOnDestroy() {
// Invalidate cached metamodel, thus it's reloaded next time page is opened
this.metamodelService.clearCachedData();
this.ngUnsubscribe$.next();
this.ngUnsubscribe$.complete();
}
@HostListener('window:resize', ['$event'])
onResize(event) {
this.resizeFloGraph();
}
resizeFloGraph(height?: number) {
const viewEditor = this.flo.element.nativeElement.children[2];
if (height) {
height = height - 330;
} else {
height = document.documentElement.clientHeight - 330;
}
this.renderer.setStyle(viewEditor, 'height', `${Math.max(height, 300)}px`);
}
setEditorContext(editorContext: Flo.EditorContext) {
this.editorContext = editorContext;
if (this.editorContext) {
const subscription = this.editorContext.paletteReady
.pipe(takeUntil(this.ngUnsubscribe$))
.pipe(map((value) => {
this.resizeFloGraph();
return value;
}))
.subscribe(ready => {
if (ready) {
subscription.unsubscribe();
this.initSubject.next();
this.initSubject.complete();
}
});
}
}
get gridOn(): boolean {
return this.editorContext.gridSize !== 1;
}
set gridOn(on: boolean) {
this.editorContext.gridSize = on ? 40 : 1;
}
arrangeAll() {
this.editorContext.performLayout().then(() => this.editorContext.fitToPage());
}
createStreamDefs() {
if (this.isCreateStreamsDisabled) {
this.notificationService.error('Some field(s) are missing or invalid.');
} else {
const bsModalRef = this.bsModalService
.show(StreamCreateDialogComponent, { class: 'modal-lg' });
bsModalRef.content.open({ dsl: this.dsl }).subscribe(() => {
this.editorContext.clearGraph();
});
}
}
contentAssist(doc: CodeMirror.EditorFromTextArea) {
const cursor = (<any>doc).getCursor();
const startOfLine = { line: cursor.line, ch: 0 };
const prefix = (<any>doc).getRange(startOfLine, cursor);
return new Promise((resolve) => {
this.contentAssistService.getProposals(prefix)
.pipe(takeUntil(this.ngUnsubscribe$))
.subscribe(completions => {
const chopAt = this.interestingPrefixStart(prefix, completions);
const finalProposals = completions.map((longCompletion: any) => {
const text = typeof longCompletion === 'string' ? longCompletion : longCompletion.text;
return text.substring(chopAt);
});
this.loggerService.log(JSON.stringify(finalProposals));
resolve({
list: finalProposals,
from: { line: startOfLine.line, ch: chopAt },
to: cursor
});
}, err => {
this.loggerService.error(err);
resolve();
});
});
}
lint(dsl: string, updateLintingCallback: CodeMirror.UpdateLintingCallback, editor: CodeMirror.Editor): void {
const result = this.parserService.parseDsl(dsl, 'stream');
const annotations: CodeMirror.Annotation[] = [];
Array.from(this.validationMarkers.values())
.filter(markers => Array.isArray(markers))
.forEach(markers => markers
.filter(m => m.range && m.hasOwnProperty('severity'))
.forEach(m => annotations.push({
message: m.message,
from: m.range.start,
to: m.range.end,
severity: Flo.Severity[m.severity].toLowerCase()
}))
);
if (result.lines) {
const newParseErrors = [];
result.lines.filter(l => Array.isArray(l.errors)).forEach(l => newParseErrors.push(...l.errors));
this.parseErrors = newParseErrors;
newParseErrors.forEach(e => annotations.push({
from: e.range.start,
to: e.range.end,
message: e.message,
severity: 'error'
}));
}
updateLintingCallback(editor, annotations);
}
/**
* The suggestions provided by rest api are very long and include the whole command typed
* from the start of the line. This function determines the start of the 'interesting' part
* at the end of the prefix, so that we can use it to chop-off the suggestion there.
*/
interestingPrefixStart(prefix: string, completions: Array<any>) {
const cursor = prefix.length;
if (completions.every(completion => this.isDelimiter(completion[cursor]))) {
return cursor;
}
return this.findLast(prefix, (s: string) => this.isDelimiter(s));
}
isDelimiter(c: string) {
return c && (/\s|\|/).test(c);
}
findLast(string: string, predicate: (s: string) => boolean, start?: number): number {
let pos = start || string.length - 1;
while (pos >= 0 && !predicate(string[pos])) {
pos--;
}
return pos;
}
get isCreateStreamsDisabled(): boolean {
if (this.dsl && this.dsl.trim() && this.contentValidated && this.parseErrors.length === 0) {
return Array.from(this.validationMarkers.values())
.find(markers => markers
.find(m => m.severity === Flo.Severity.Error) !== undefined) !== undefined;
}
return true;
}
}
<app-page>
<app-page-head>
<app-page-head-back [defaultUrl]="'/streams/definitions'"></app-page-head-back>
<app-page-head-title><strong>Create a stream</strong></app-page-head-title>
</app-page-head>
<p>Create a stream using text based input or the visual editor.</p>
<div id="flo-container" class="stream-editor" dataflowLayoutType type="full">
<flo-editor (floApi)="setEditorContext($event)" [metamodel]="metamodelService" [renderer]="renderService"
[editor]="editorService" [paletteSize]="paletteSize" [(dsl)]="dsl" [paperPadding]="55"
(contentValidated)="contentValidated=$event" (validationMarkers)="validationMarkers = $event">
<div class="flow-definition-container">
<dsl-editor [(dsl)]="dsl" line-numbers="true" line-wrapping="true"
(focus)="editorContext.graphToTextSync=false" placeholder="Enter stream definition..."
[hintOptions]="hintOptions" [lintOptions]="lintOptions"></dsl-editor>
</div>
<div class="flow-actions">
<button (click)="editorContext.clearGraph()" class="btn btn-default" type="button">Clear</button>
<button (click)="arrangeAll()" class="btn btn-default" type="button">Layout</button>
<button class="btn" (click)="editorContext.autolink = !editorContext.autolink"
[ngClass]="{'btn-default-alt': !editorContext.autolink, 'btn-default': editorContext.autolink}">Auto
Link
</button>
<button class="btn" (click)="gridOn = !gridOn"
[ngClass]="{'btn-default-alt': !gridOn, 'btn-default': gridOn}">Grid
</button>
</div>
</flo-editor>
</div>
<app-page-actions>
<button id="back-button" type="button" class="btn btn-default" routerLink="/streams">
Cancel
</button>
<span style="margin-left: 8px;"
tooltip="Disabled whilst stream definition(s) invalid !" [isDisabled]="!isCreateStreamsDisabled" container="body" triggers="click:mouseleave" placement="right">
<button (click)="createStreamDefs()" class="btn btn-primary" type="button" [disabled]="isCreateStreamsDisabled">
Create Stream
</button>
</span>
</app-page-actions>
</app-page>