Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit eef387b

Browse files
committed
Validate against duplicate stream names on the form
Validate against duplicate names on the form Validate against duplicate names on the form Validate against duplicate names on the form
1 parent 2b8ed93 commit eef387b

File tree

5 files changed

+133
-54
lines changed

5 files changed

+133
-54
lines changed

ui/src/app/streams/flo/support/utils.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,19 @@ describe('utils', () => {
139139
expect(Utils.generateStreamName(graph, http)).toEqual('unique-name');
140140
});
141141

142+
it('find duplicates: no elements', () => {
143+
expect(Utils.findDuplicates([])).toEqual([]);
144+
});
145+
146+
it('find duplicates: no duplicates', () => {
147+
expect(Utils.findDuplicates(['1', '2', '3'])).toEqual([]);
148+
});
149+
150+
it('find duplicates: multiple duplicates', () => {
151+
expect(Utils.findDuplicates(['1', '2', '1', '3', '2'])).toEqual(['2', '1']);
152+
});
153+
154+
it('find duplicates: multiple duplicates numbers', () => {
155+
expect(Utils.findDuplicates([1, 2, 1, 3, 2])).toEqual([2, 1]);
156+
});
142157
});

ui/src/app/streams/flo/support/utils.ts

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { dia } from 'jointjs';
17+
import {dia} from 'jointjs';
1818

1919
/**
2020
* Utilities for Flo based Stream Definition graph editor.
@@ -23,37 +23,53 @@ import { dia } from 'jointjs';
2323
*/
2424
export class Utils {
2525

26-
static canBeHeadOfStream(graph: dia.Graph, element: dia.Element): boolean {
27-
if (element.attr('metadata')) {
28-
if (!element.attr('.input-port') || element.attr('.input-port/display') === 'none') {
29-
return true;
30-
} else {
31-
const incoming = graph.getConnectedLinks(element, { inbound: true });
32-
const tapLink = incoming.find(l => l.attr('props/isTapLink'));
33-
if (tapLink) {
34-
return true;
35-
}
36-
}
37-
}
38-
return false;
26+
static canBeHeadOfStream(graph: dia.Graph, element: dia.Element): boolean {
27+
if (element.attr('metadata')) {
28+
if (!element.attr('.input-port') || element.attr('.input-port/display') === 'none') {
29+
return true;
30+
} else {
31+
const incoming = graph.getConnectedLinks(element, {inbound: true});
32+
const tapLink = incoming.find(l => l.attr('props/isTapLink'));
33+
if (tapLink) {
34+
return true;
35+
}
36+
}
3937
}
38+
return false;
39+
}
4040

41-
static generateStreamName(graph: dia.Graph, element: dia.Element) {
42-
const streamNames: Array<string> = graph.getElements()
43-
.filter(e => element !== e && e.attr('stream-name') && this.canBeHeadOfStream(graph, e))
44-
.map(e => e.attr('stream-name'));
41+
static generateStreamName(graph: dia.Graph, element: dia.Element) {
42+
const streamNames: Array<string> = graph.getElements()
43+
.filter(e => element !== e && e.attr('stream-name') && this.canBeHeadOfStream(graph, e))
44+
.map(e => e.attr('stream-name'));
4545

46-
// Check if current element stream name is unique
47-
if (element && element.attr('stream-name') && streamNames.indexOf(element.attr('stream-name')) === -1) {
48-
return element.attr('stream-name');
49-
}
46+
// Check if current element stream name is unique
47+
if (element && element.attr('stream-name') && streamNames.indexOf(element.attr('stream-name')) === -1) {
48+
return element.attr('stream-name');
49+
}
5050

51-
const name = 'STREAM_';
52-
let index = 1;
53-
while (streamNames.indexOf(name + index) >= 0) {
54-
index++;
55-
}
56-
return name + index;
51+
const name = 'STREAM_';
52+
let index = 1;
53+
while (streamNames.indexOf(name + index) >= 0) {
54+
index++;
55+
}
56+
return name + index;
57+
}
58+
59+
static findDuplicates<T>(elements: T[]): T[] {
60+
const duplicates: T[] = [];
61+
while (elements.length !== 0) {
62+
const e = elements.pop();
63+
let idx = elements.indexOf(e);
64+
if (idx >= 0) {
65+
duplicates.push(e);
66+
}
67+
while (idx >= 0) {
68+
elements.splice(idx, 1);
69+
idx = elements.indexOf(e);
70+
}
5771
}
72+
return duplicates;
73+
}
5874

5975
}

ui/src/app/streams/stream-create/stream-create-dialog.component.html

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ <h4 class="modal-title pull-left">Create Stream</h4>
77
<div class="modal-body" [formGroup]="form">
88
<div *ngIf="errors && errors.length > 0" class="dialog-validation">
99
<div *ngFor="let error of errors">
10-
<label class="glyphicon glyphicon-exclamation-sign dialog-error-sign"></label>
11-
<label>{{error}}</label>
10+
<label class="glyphicon glyphicon-exclamation-sign dialog-error-sign">{{error}}</label>
1211
</div>
1312
</div>
1413
<div *ngIf="!(errors && errors.length) && warnings && warnings.length > 0" class="dialog-validation">
@@ -18,30 +17,36 @@ <h4 class="modal-title pull-left">Create Stream</h4>
1817
</div>
1918
</div>
2019
<p>This action will create stream(s):</p>
21-
<table class="table table-striped table-hover table-fixed table-compact">
22-
<!--<col width="300px"/>-->
23-
<!--<col width="auto"/>-->
24-
<tbody>
25-
<tr *ngFor="let def of streamDefsToCreate()" [ngClass]="{'has-warning': !getControl(def.index.toString()).valid}">
26-
<td>
27-
<input [disabled]="isStreamCreationInProgress()" class="form-control" [id]="def.index.toString()" [name]="def.index.toString()" [formControlName]="def.index.toString()"
28-
type="text" placeholder="<Stream Name>" [ngModel]="def.name" (ngModelChange)="changeStreamName(def.index, $event)"/>
29-
<p *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.required" class="help-block validation-block">Stream name is required!</p>
30-
<p *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.uniqueResource" class="help-block validation-block">Stream name is already taken!</p>
31-
<p *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.pattern" class="help-block validation-block">Invalid stream name!</p>
32-
<!--<p *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.uniqueFieldValues" class="help-block validation-block">Duplicate stream name on the form!</p>-->
33-
</td>
34-
<td>
35-
<label class="control-label">{{def.def}}</label>
36-
</td>
37-
</tr>
38-
</tbody>
39-
</table>
40-
<label class="dialog-control"><input [disabled]="isStreamCreationInProgress()" type="checkbox" [(ngModel)]="deploy" [ngModelOptions]="{standalone: true}"/>Deploy stream(s)</label>
20+
<tbody>
21+
<tr *ngFor="let def of streamDefsToCreate()" [ngClass]="{'has-warning': invalidStreamRow(def)}">
22+
<td class="stream-name-form-control-cell">
23+
<input [disabled]="isStreamCreationInProgress()" class="form-control" [id]="def.index.toString()"
24+
[name]="def.index.toString()" [formControlName]="def.index.toString()"
25+
type="text" placeholder="<Stream Name>" [ngModel]="def.name"
26+
(ngModelChange)="changeStreamName(def.index, $event)"/>
27+
<p>
28+
<span *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.required"
29+
class="help-block validation-block">Stream name is required!</span>
30+
<span *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.uniqueResource"
31+
class="help-block validation-block">Stream name is already taken!</span>
32+
<span *ngIf="getControl(def.index.toString()).errors && getControl(def.index.toString()).errors.pattern"
33+
class="help-block validation-block">Invalid stream name!</span>
34+
<span *ngIf="hasDuplicateName(def)"
35+
class="help-block validation-block">Duplicate stream name on the form</span>
36+
</p>
37+
</td>
38+
<td class="stream-definition-label-cell">
39+
<label class="control-label">{{def.def}}</label>
40+
</td>
41+
</tr>
42+
</tbody>
43+
<label class="dialog-control"><input [disabled]="isStreamCreationInProgress()" type="checkbox" [(ngModel)]="deploy"
44+
[ngModelOptions]="{standalone: true}"/>Deploy stream(s)</label>
4145
<div *ngIf="progressData">
4246
<hr/>
4347
<div>Creating Streams...</div>
44-
<progressbar animate="true" [value]="progressData.percent" type="success"><b>{{progressData.percent}}%</b></progressbar>
48+
<progressbar animate="true" [value]="progressData.percent" type="success"><b>{{progressData.percent}}%</b>
49+
</progressbar>
4550
</div>
4651
</div>
4752
<div class="modal-footer">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.modal-body {
2+
tbody {
3+
width: 100%;
4+
display: inline-table;
5+
}
6+
tr {
7+
width: 100%;
8+
display: inline-table;
9+
}
10+
.stream-definition-label-cell {
11+
vertical-align: top;
12+
padding: 4px 8px;
13+
}
14+
.stream-name-form-control-cell {
15+
width: 50%;
16+
}
17+
.dialog-error-sign {
18+
color: red;
19+
}
20+
.dialog-validation {
21+
margin-bottom: 4px;
22+
font-size: smaller;
23+
line-height: 1em;
24+
}
25+
}

ui/src/app/streams/stream-create/stream-create-dialog.component.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BsModalRef } from 'ngx-bootstrap';
33
import { FormGroup, FormControl, AbstractControl, Validators } from '@angular/forms';
44
import { ParserService } from '../../shared/services/parser.service';
55
import { convertParseResponseToJsonGraph } from '../flo/text-to-graph';
6+
import { Utils } from '../flo/support/utils';
67
import { StreamsService } from '../streams.service';
78
import { ToastyService } from 'ng2-toasty';
89
import { Properties } from 'spring-flo';
@@ -31,12 +32,13 @@ const PROGRESS_BAR_WAIT_TIME = 500; // to account for animation delay
3132
@Component({
3233
selector: 'app-stream-create-dialog-content',
3334
templateUrl: 'stream-create-dialog.component.html',
35+
styleUrls: [ 'stream-create-dialog.component.scss' ],
3436
encapsulation: ViewEncapsulation.None
3537
})
3638
export class StreamCreateDialogComponent implements OnInit {
3739

3840
form: FormGroup;
39-
streamDefs: Array<any>;
41+
streamDefs: Array<any> = [];
4042
errors: Array<string>;
4143
warnings: Array<string>;
4244
dependencies: Map<number, Array<number>>;
@@ -53,7 +55,7 @@ export class StreamCreateDialogComponent implements OnInit {
5355
) {}
5456

5557
ngOnInit() {
56-
this.form = new FormGroup({});
58+
this.form = new FormGroup({}, this.uniqueStreamNames());
5759
}
5860

5961

@@ -74,7 +76,7 @@ export class StreamCreateDialogComponent implements OnInit {
7476
// TODO: Adopt to parser types once they are available
7577
const graphAndErrors = convertParseResponseToJsonGraph(text, this.parserService.parseDsl(text));
7678
if (graphAndErrors.graph) {
77-
this.streamDefs = graphAndErrors.graph.streamdefs;
79+
this.streamDefs.push(...graphAndErrors.graph.streamdefs);
7880
this.streamDefs.forEach((streamDef, i) => {
7981
streamDef.created = false;
8082
streamDef.index = i;
@@ -110,6 +112,22 @@ export class StreamCreateDialogComponent implements OnInit {
110112
}
111113
}
112114

115+
uniqueStreamNames() {
116+
const streamDefs = this.streamDefs;
117+
return (control: AbstractControl): { [key: string]: any } => {
118+
const duplicates = Utils.findDuplicates(streamDefs.filter(s => s.name).map(s => s.name));
119+
return duplicates.length === 0 ? null : {'uniqueStreamNames': duplicates};
120+
};
121+
}
122+
123+
invalidStreamRow(def: any): boolean {
124+
return this.getControl(def.index.toString()).invalid || this.hasDuplicateName(def);
125+
}
126+
127+
hasDuplicateName(def: any): boolean {
128+
return this.form.errors && this.form.errors.uniqueStreamNames && this.form.errors.uniqueStreamNames.indexOf(def.name) >= 0;
129+
}
130+
113131
getControl(id: string): AbstractControl {
114132
return this.form.controls[id];
115133
}

0 commit comments

Comments
 (0)