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

Commit 5c01cc2

Browse files
oodamienghillert
authored andcommitted
gh-659 Apps Bulk Import: Improve UX
- Add tab component - Update tests - Apps Bulk Import: separate component - Create dedicate route for URI/Properties
1 parent ea2225c commit 5c01cc2

14 files changed

+593
-313
lines changed

ui/src/app/apps/apps-bulk-import/apps-bulk-import.component.html

Lines changed: 9 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -11,77 +11,17 @@ <h1>Bulk Import Application(s)</h1>
1111
</div>
1212
</div>
1313
<hr/>
14-
<form [formGroup]="form" class="form-horizontal" role="form" (ngSubmit)="bulkImportApps()" novalidate>
15-
<fieldset id="importAppsViaUri">
16-
<div class="form-group" [class.has-error]="form.get('uri').invalid">
17-
<label for="uriInput" class="col-sm-6 control-label">Uri</label>
18-
<div class="col-sm-14">
19-
<input type="text" id="uriInput" name="uri" autofocus formControlName="uri" class="form-control"
20-
placeholder="http://url.to.properties">
21-
<span class="help-block" *ngIf="form.get('uri').invalid">Please provide a valid URI pointing to the respective properties file.</span>
22-
</div>
23-
</div>
24-
</fieldset>
25-
<h2 class="text-center">OR</h2>
26-
<div class="row" style="margin-bottom: 1em;">
27-
<div class="col-md-12 col-md-offset-6">
28-
<p>
29-
Enter the list of properties into the text area field below. Alternatively, you can also select a
30-
file in your local file system, which is used to populate the text area field.
31-
</p>
32-
</div>
33-
</div>
34-
<fieldset id="importAppsViaUpload">
35-
<div class="form-group" [class.has-error]="form.get('properties').invalid">
36-
<label for="propertiesInput" class="col-sm-6 control-label">Apps as Properties</label>
37-
<div class="col-sm-14">
38-
<textarea id="propertiesInput" name="properties" autofocus
39-
formControlName="properties"
40-
rows="5"
41-
class="form-control" placeholder="Example:
42-
task.timestamp=maven://o.s.cloud.task.app:timestamp-task:1.2.3.RELEASE
43-
task.spark-client=maven://o.s.cloud.task.app:spark-client-task:1.2.3.RELEASE"
44-
></textarea>
45-
46-
<span class="help-block" *ngIf="form.get('properties').invalid">Please provide a valid properties where the keys are formatted as
47-
<strong>type.name</strong> and the values are the URIs of the apps.</span>
48-
49-
<div class="input-file">
50-
<label class="btn-file btn btn-primary" placement="bottom"
51-
tooltip="Please provide a text file containing properties. This will be used to populate the text area above.">
52-
<input formControlName="file" id="propertiesFile" name="propertiesFile" type="file"
53-
(change)="fileChange($event)"/>
54-
Import a local file
55-
</label>
56-
</div>
57-
</div>
58-
</div>
59-
</fieldset>
6014

61-
<div class="form-group">
62-
<div class="col-sm-14 col-sm-offset-6">
63-
<hr />
64-
<label class="checkbox-inline">
65-
<input formControlName="force" id="forceInput" name="force" type="checkbox" />
66-
Force
67-
</label>
68-
<p class="checkbox-description">
69-
By checking <strong>force</strong>, the applications will be imported and installed
70-
even if it already exists but only if not being used already.
71-
</p>
15+
<div class="tab-simple">
16+
<ul class="nav nav-tabs">
17+
<li role="presentation" routerLinkActive="active"><a routerLink="uri">From HTTP</a></li>
18+
<li role="presentation" routerLinkActive="active"><a routerLink="properties">From Properties</a></li>
19+
</ul>
20+
<div class="tab-content">
21+
<div class="tab-pane in active">
22+
<router-outlet></router-outlet>
7223
</div>
7324
</div>
74-
75-
<div *ngIf="form.invalid && form.hasError('both')" class="alert alert-danger" style="width: 350px;margin: 0 auto 2rem;">
76-
Please provide only a URI or Properties not both.
77-
</div>
78-
79-
<div class="footer-actions row" style="margin-bottom: 2em;text-align: center">
80-
<button type="button" class="btn btn-default" (click)="cancel()">Cancel</button>
81-
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
82-
Import the application(s)
83-
</button>
84-
</div>
85-
</form>
25+
</div>
8626

8727
</div>

ui/src/app/apps/apps-bulk-import/apps-bulk-import.component.spec.ts

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {MockToastyService} from '../../tests/mocks/toasty';
77
import {MockAppsService} from '../../tests/mocks/apps';
88
import {AppsBulkImportComponent} from './apps-bulk-import.component';
99
import {ReactiveFormsModule, FormsModule} from '@angular/forms';
10-
import {By} from '@angular/platform-browser';
1110
import {BusyService} from '../../shared/services/busy.service';
1211

1312
/**
@@ -55,93 +54,4 @@ describe('AppsBulkImportComponent', () => {
5554
expect(component).toBeTruthy();
5655
});
5756

58-
it('should disabled the import action', () => {
59-
fixture.detectChanges();
60-
const bt = fixture.debugElement.query(By.css('.footer-actions .btn-primary')).nativeElement;
61-
const inputs = {
62-
uri: fixture.debugElement.query(By.css('#uriInput')).nativeElement,
63-
properties: fixture.debugElement.query(By.css('#propertiesInput')).nativeElement,
64-
force: fixture.debugElement.query(By.css('#forceInput')).nativeElement
65-
};
66-
[
67-
{uri: '', properties: '', force: true},
68-
{uri: 'http://foo.ly/foo-bar-foo', properties: 'a=a', force: false},
69-
{uri: 'bar', properties: 'bar', force: false},
70-
{uri: 'foo@bar.com', properties: 'bar', force: true},
71-
{uri: '', properties: 'bar', force: false},
72-
{uri: '', properties: 'foo=bar=bar', force: false},
73-
{uri: '', properties: 'foo=bar\nbar', force: false}
74-
].forEach((a) => {
75-
component.form.get('uri').setValue(a.uri);
76-
component.form.get('properties').setValue(a.properties);
77-
component.form.get('force').setValue(a.force);
78-
fixture.detectChanges();
79-
if (a.uri) {
80-
expect(inputs.uri.value).toContain(a.uri);
81-
}
82-
if (a.properties) {
83-
expect(inputs.properties.value).toContain(a.properties);
84-
}
85-
expect(inputs.force.checked).toBe(a.force);
86-
expect(bt.disabled).toBeTruthy();
87-
});
88-
});
89-
90-
it('should enable the import action and call the appService.bulkImportApps method', () => {
91-
fixture.detectChanges();
92-
const bt = fixture.debugElement.query(By.css('.footer-actions .btn-primary')).nativeElement;
93-
const inputs = {
94-
uri: fixture.debugElement.query(By.css('#uriInput')).nativeElement,
95-
properties: fixture.debugElement.query(By.css('#propertiesInput')).nativeElement,
96-
force: fixture.debugElement.query(By.css('#forceInput')).nativeElement
97-
};
98-
const spy = spyOn(appsService, 'bulkImportApps');
99-
[
100-
{uri: 'http://foo.ly/foo-bar-foo', properties: '', force: false},
101-
{uri: '', properties: 'foo=http://foo.ly/foo-bar-foo', force: true},
102-
{uri: '', properties: 'foo=http://foo.ly/foo-bar-foo\nbar=http://foo.ly/foo-bar-foo', force: true}
103-
].forEach((a) => {
104-
component.form.get('uri').setValue(a.uri);
105-
component.form.get('properties').setValue(a.properties);
106-
component.form.get('force').setValue(a.force);
107-
fixture.detectChanges();
108-
expect(bt.disabled).not.toBeTruthy();
109-
if (a.uri) {
110-
expect(inputs.uri.value).toContain(a.uri);
111-
}
112-
if (a.properties) {
113-
expect(inputs.properties.value).toContain(a.properties);
114-
}
115-
expect(inputs.force.checked).toBe(a.force);
116-
bt.click();
117-
});
118-
expect(spy).toHaveBeenCalledTimes(3);
119-
});
120-
121-
it('should display a toast after a success import', () => {
122-
component.form.get('uri').setValue('http://foo.ly/foo-bar-foo');
123-
component.bulkImportApps();
124-
fixture.detectChanges();
125-
expect(toastyService.testSuccess[0]).toContain('Apps Imported');
126-
});
127-
128-
it('should load a file in the properties input', (done) => {
129-
const event = {target: {files: [new Blob(['a=a'])]}};
130-
component.fileChange(event);
131-
setTimeout(() => {
132-
fixture.detectChanges();
133-
expect(component.form.get('properties').value).toContain('a=a');
134-
done();
135-
}, 1000);
136-
});
137-
138-
it('should go back to the apps list (footer close)', () => {
139-
fixture.detectChanges();
140-
const navigate = spyOn((<any>component).router, 'navigate');
141-
const bt: HTMLElement = fixture.debugElement.query(By.css('.footer-actions .btn-default')).nativeElement;
142-
bt.click();
143-
fixture.detectChanges();
144-
expect(navigate).toHaveBeenCalledWith(['apps']);
145-
});
146-
14757
});

ui/src/app/apps/apps-bulk-import/apps-bulk-import.component.ts

Lines changed: 2 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,102 +12,15 @@ import {takeUntil} from 'rxjs/operators';
1212

1313
/**
1414
* Applications Bulk Import
15-
* Provide a form to import applications
1615
*
1716
* @author Damien Vitrac
1817
*/
1918
@Component({
2019
selector: 'app-apps',
21-
styleUrls: ['./styles.scss'],
2220
templateUrl: './apps-bulk-import.component.html'
2321
})
24-
export class AppsBulkImportComponent implements OnDestroy {
22+
export class AppsBulkImportComponent {
2523

26-
/**
27-
* Busy Subscriptions
28-
*/
29-
private ngUnsubscribe$: Subject<any> = new Subject();
30-
31-
/**
32-
* Fom Group
33-
*/
34-
form: FormGroup;
35-
36-
/**
37-
* Constructor
38-
*
39-
* @param {AppsService} appsService
40-
* @param {ToastyService} toastyService
41-
* @param {FormBuilder} fb
42-
* @param {BusyService} busyService
43-
* @param {Router} router
44-
*/
45-
constructor(private appsService: AppsService,
46-
private toastyService: ToastyService,
47-
private fb: FormBuilder,
48-
private busyService: BusyService,
49-
private router: Router) {
50-
51-
this.form = fb.group({
52-
'uri': new FormControl('', AppsBulkImportValidator.uri),
53-
'properties': new FormControl('', AppsBulkImportValidator.properties),
54-
'file': new FormControl(''),
55-
'force': new FormControl(false)
56-
}, {validator: AppsBulkImportValidator.form}
57-
);
58-
}
59-
60-
/**
61-
* Will cleanup any {@link Subscription}s to prevent
62-
* memory leaks.
63-
*/
64-
ngOnDestroy() {
65-
this.ngUnsubscribe$.next();
66-
this.ngUnsubscribe$.complete();
67-
}
68-
69-
/**
70-
* Parse and load a file to the properties control
71-
*
72-
* @param {Blob} contents File
73-
*/
74-
fileChange(contents) {
75-
try {
76-
const reader = new FileReader();
77-
reader.onloadend = (e) => {
78-
this.form.get('properties').setValue(reader.result);
79-
this.form.get('file').setValue('');
80-
};
81-
reader.readAsText(contents.target.files[0]);
82-
} catch (e) {
83-
}
84-
}
85-
86-
/**
87-
* Bulk Import Apps.
88-
*/
89-
bulkImportApps() {
90-
const bulkImportParams: BulkImportParams = {
91-
force: this.form.get('force').value,
92-
properties: this.form.get('properties').value.toString().split('/n'),
93-
uri: this.form.get('uri').value.toString()
94-
};
95-
const busy = this.appsService.bulkImportApps(bulkImportParams)
96-
.pipe(takeUntil(this.ngUnsubscribe$))
97-
.subscribe(
98-
data => {
99-
this.toastyService.success('Apps Imported.');
100-
this.cancel();
101-
}
102-
);
103-
104-
this.busyService.addSubscription(busy);
105-
}
106-
107-
/**
108-
* Cancel to applications list
109-
*/
110-
cancel() {
111-
this.router.navigate(['apps']);
24+
constructor() {
11225
}
11326
}

ui/src/app/apps/apps-bulk-import/apps-bulk-import.validator.spec.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,12 @@ import {AppsBulkImportValidator} from './apps-bulk-import.validator';
88
*/
99
describe('AppsBulkImportValidator', () => {
1010

11-
describe('form', () => {
12-
13-
it('invalid: empty or 2 values set', () => {
14-
const form: FormGroup = new FormGroup({
15-
'properties': new FormControl(''),
16-
'uri': new FormControl('')
17-
});
18-
expect(AppsBulkImportValidator.form(form).invalid).toBeTruthy();
19-
form.get('uri').setValue('http://foo.ly/foo-bar-foo');
20-
form.get('properties').setValue('foo=bar');
21-
expect(AppsBulkImportValidator.form(form).both).toBeTruthy();
22-
});
23-
24-
it('valid', () => {
25-
[
26-
{properties: '', uri: 'http://foo.ly/foo-bar-foo'},
27-
{properties: 'foo=http://foo.ly/foo-bar-foo', uri: ''},
28-
].forEach((mock) => {
29-
const form: FormGroup = new FormGroup({
30-
'properties': new FormControl(mock.properties),
31-
'uri': new FormControl(mock.uri)
32-
});
33-
expect(AppsBulkImportValidator.form(form)).toBeNull();
34-
});
35-
});
36-
37-
});
38-
3911
describe('uri', () => {
4012
it('invalid', () => {
4113
[
4214
' ',
4315
'bb',
16+
' http://foo.ly/foo',
4417
'b b'
4518
].forEach((mock) => {
4619
const uri: FormControl = new FormControl(mock);

ui/src/app/apps/apps-bulk-import/apps-bulk-import.validator.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,6 @@ export class AppsBulkImportValidator {
1414
*/
1515
static uriRegex = /^([a-zA-Z0-9-]+:\/\/)([\\w\\.:-]+)?([a-zA-Z0-9-\/.:-]+)*$/;
1616

17-
/**
18-
* Validate the conditions: uri or properties value
19-
*
20-
* @param {FormGroup} control
21-
* @returns {any} An object error
22-
*/
23-
static form(control: FormGroup): any {
24-
const uri = control.get('uri');
25-
const properties = control.get('properties');
26-
27-
if (!uri.value && !properties.value) {
28-
return {invalid: true};
29-
}
30-
if (uri.value && properties.value) {
31-
return {both: true};
32-
}
33-
34-
return null;
35-
}
36-
3717
/**
3818
* Validate the uri conditions
3919
*
@@ -82,6 +62,6 @@ export class AppsBulkImportValidator {
8262
return {invalid: true};
8363
}
8464
return null;
85-
8665
}
66+
8767
}

0 commit comments

Comments
 (0)