+
-
-
- cloud_queue
-
- {{project.name | truncate : 40 : '...'}}
-
- {{project.owner}}
-
-
-
- {{project.description}}
-
-
-
-
-
-
-
-
-
-
-
+
Projects
+
+
+
+
+ cloud_queue
+
+ {{project.name | truncate : 40 : '...'}}
+
+ {{project.owner}}
+
+
+
+ {{project.description}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/project-dashboard/project-dashboard.component.ts b/src/app/project-dashboard/project-dashboard.component.ts
index f9763da..6b09967 100644
--- a/src/app/project-dashboard/project-dashboard.component.ts
+++ b/src/app/project-dashboard/project-dashboard.component.ts
@@ -2,9 +2,8 @@ import { Component, OnInit, HostListener } from '@angular/core';
import { ProjectManager } from '../data-management/project-manager';
import JsonProjectMeta from '../model/json-project-meta';
import { MatDialog } from '@angular/material/dialog';
-import { CreateProjectDialogComponent } from './create-project-dialog/create-project-dialog.component';
+import { ProjectMetaDataDialogComponent } from './project-meta-data-dialog/project-meta-data-dialog.component';
import Project from '../model/project';
-import { EditProjectDialogComponent } from './edit-project-dialog/edit-project-dialog.component';
import { Router } from '@angular/router';
@Component({
@@ -20,14 +19,6 @@ export class ProjectDashboardComponent implements OnInit {
ngOnInit() {
ProjectManager.refreshProjectMetas()
- this.onResize();
- }
-
- @HostListener('window:resize', ['$event'])
- onResize(event?) {
- const screenHeight = window.innerHeight;
- let box2h = 64//document.getElementById("command-bar").offsetHeight
- document.getElementById("main-container").style.height = (screenHeight - box2h)+'px';
}
onDelete(project: JsonProjectMeta, idx: number) {
@@ -38,34 +29,25 @@ export class ProjectDashboardComponent implements OnInit {
this.router.navigate(['project', project.id]);
}
- openCreateProjectDialog() {
- console.log('Create Project Event');
- const dialogRef = this.dialog.open(CreateProjectDialogComponent);
+ openProjectDialog(metaData?: JsonProjectMeta) {
+ console.log(metaData);
+ const dialogRef = this.dialog.open(ProjectMetaDataDialogComponent,
+ {data: {projectMeta: metaData}});
- dialogRef.afterClosed().subscribe(result => {
- if (!result) {
+ dialogRef.afterClosed().subscribe((result: { isUpdate: boolean, data: Project }) => {
+ if (!result || !result.data) {
return;
}
- const project = result as Project;
- console.log(`Dialog result:`, project);
- // Save project
- ProjectManager.save(project);
- });
- }
-
- openEditDialog(metaData: JsonProjectMeta) {
- console.log('Create Project Event');
- const dialogRef = this.dialog.open(EditProjectDialogComponent,
- {data: {projectMeta: metaData}});
-
- dialogRef.afterClosed().subscribe(result => {
- if (!result) {
- return;
- }
+ console.log(`Dialog result:`, result);
+ // Update/Save project
+ if (result.isUpdate) {
ProjectManager.load(metaData.id).then(project => {
- project.metaData = result;
+ project.metaData = result.data.metaData;
ProjectManager.save(project);
});
+ } else {
+ ProjectManager.save(result.data);
+ }
});
}
diff --git a/src/app/project-dashboard/create-project-dialog/cloud-provider-item.ts b/src/app/project-dashboard/project-meta-data-dialog/cloud-provider-item.ts
similarity index 100%
rename from src/app/project-dashboard/create-project-dialog/cloud-provider-item.ts
rename to src/app/project-dashboard/project-meta-data-dialog/cloud-provider-item.ts
diff --git a/src/app/project-dashboard/create-project-dialog/create-project-dialog.component.css b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.css
similarity index 100%
rename from src/app/project-dashboard/create-project-dialog/create-project-dialog.component.css
rename to src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.css
diff --git a/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.html b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.html
new file mode 100644
index 0000000..d2f98e4
--- /dev/null
+++ b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.html
@@ -0,0 +1,81 @@
+
Create a new Project
+
+
+
+ Select all regions, which are neccessary for your cloud application.
+
+
+
+
+
+
+
+
diff --git a/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.spec.ts b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.spec.ts
new file mode 100644
index 0000000..9e59be6
--- /dev/null
+++ b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.spec.ts
@@ -0,0 +1,110 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProjectMetaDataDialogComponent } from './project-meta-data-dialog.component';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { InjectionToken } from '@angular/core';
+import JsonProjectMeta from '../../model/json-project-meta';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatExpansionModule } from '@angular/material/expansion';
+import { MatInputModule } from '@angular/material/input';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ProjectManager } from '../../data-management/project-manager';
+import { By } from '@angular/platform-browser';
+
+const testStrings: string[] = [
+ 'string1',
+ 'string1 ',
+ ' string1',
+ 'string2',
+ 'string3'
+];
+
+describe('ProjectMetaDataDialogComponent', () => {
+ let component: ProjectMetaDataDialogComponent;
+ let fixture: ComponentFixture
;
+ const noData = new InjectionToken<{ projectMeta: JsonProjectMeta }>(undefined);
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ProjectMetaDataDialogComponent],
+ imports: [
+ HttpClientTestingModule,
+ BrowserAnimationsModule,
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatExpansionModule,
+ MatInputModule
+ ],
+ providers: [
+ {provide: MatDialogRef, useValue: {}},
+ {provide: MAT_DIALOG_DATA, useValue: noData}
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ProjectMetaDataDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ ProjectManager.projectMetas = [
+ {
+ id: undefined,
+ name: testStrings[0],
+ description: undefined,
+ owner: undefined,
+ cloudProviders: undefined
+ },
+ {
+ id: undefined,
+ name: testStrings[3],
+ description: undefined,
+ owner: undefined,
+ cloudProviders: undefined
+ }
+ ];
+ });
+
+ afterEach(() => {
+ ProjectManager.projectMetas = [];
+ component.projectForm.reset()
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('#duplicateNameValidator() should recognize duplicate', () => {
+ expect(component.duplicateNameValidator()({value: testStrings[0]}).duplicate).toBeTrue();
+ expect(component.duplicateNameValidator()({value: testStrings[1]}).duplicate).toBeTrue();
+ expect(component.duplicateNameValidator()({value: testStrings[2]}).duplicate).toBeTrue();
+ expect(component.duplicateNameValidator()({value: testStrings[3]}).duplicate).toBeTrue();
+ });
+
+ it('#duplicateNameValidator() should not recognize duplicate', () => {
+ expect(component.duplicateNameValidator()({value: testStrings[4]})).toBeNull();
+ });
+
+ function addLetters(letters: string) {
+ component.projectForm.controls.name.setValue(component.name.value + letters);
+ }
+
+ it('#getNameErrorMsg() should return correct error message', () => {
+ component.projectForm.controls.name.markAsTouched()
+ expect(component.getNameErrorMsg()).toMatch('Please enter a name');
+ addLetters('s');
+ expect(component.getNameErrorMsg()).toMatch('');
+ addLetters('tring1');
+ expect(component.getNameErrorMsg()).toMatch('Name already exists');
+ addLetters('0');
+ expect(component.getNameErrorMsg()).toMatch('');
+ // Results in 257 letters
+ addLetters('x'.repeat(249));
+ expect(component.getNameErrorMsg()).toMatch('Name too long');
+ addLetters('x');
+ expect(component.getNameErrorMsg()).toMatch('Name too long');
+ });
+});
diff --git a/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.ts b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.ts
new file mode 100644
index 0000000..8a74a80
--- /dev/null
+++ b/src/app/project-dashboard/project-meta-data-dialog/project-meta-data-dialog.component.ts
@@ -0,0 +1,183 @@
+import { Component, Inject, OnInit } from '@angular/core';
+import { MatDialogRef } from '@angular/material';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import CloudProviderItem from './cloud-provider-item';
+import { HttpClient } from '@angular/common/http';
+import { ClamsProject, CloudProviderFactory, JsonCloudProvider } from '@openclams/clams-ml';
+import { environment } from '../../../environments/environment';
+import Project from '../../model/project';
+import { ProjectManager } from '../../data-management/project-manager';
+import JsonProjectMeta from '../../model/json-project-meta';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+@Component({
+ selector: 'app-project-meta-data-dialog',
+ templateUrl: './project-meta-data-dialog.component.html',
+ styleUrls: ['./project-meta-data-dialog.component.css']
+})
+export class ProjectMetaDataDialogComponent implements OnInit {
+
+ public projectForm: FormGroup;
+ public providerList: CloudProviderItem[];
+ readonly isUpdate;
+
+ constructor(private http: HttpClient,
+ private formBuilder: FormBuilder,
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { projectMeta: JsonProjectMeta }) {
+ this.isUpdate = !!data.projectMeta;
+ }
+
+ ngOnInit() {
+ this.providerList = [];
+ this.projectForm = this.formBuilder.group({
+ name: [this.isUpdate ? this.data.projectMeta.name : '', [
+ Validators.required,
+ Validators.maxLength(256),
+ this.duplicateNameValidator(this.isUpdate ? this.data.projectMeta.name : undefined)
+ ]],
+ description: [this.isUpdate ? this.data.projectMeta.description : '', [
+ Validators.maxLength(2048)
+ ]],
+ owner: [this.isUpdate ? this.data.projectMeta.owner : '', [
+ Validators.maxLength(128)
+ ]],
+ });
+
+ if (!this.isUpdate) {
+ this.http.get(environment.serviceServer).subscribe(cloudProviders =>
+ this.providerList = cloudProviders.map(provider => {
+ const item = new CloudProviderItem();
+ item.provider = provider;
+ item.options = provider.regions.map(r => {
+ return {
+ completed: true,
+ region: r
+ };
+ });
+ return item;
+ })
+ );
+ }
+ }
+
+ updateAllComplete(provider: CloudProviderItem) {
+ provider.allCompleted = provider.options != null
+ && provider.options.every(t => t.completed);
+ }
+
+ someComplete(provider: CloudProviderItem): boolean {
+ if (provider.options == null || provider.options.length === 0) {
+ return false;
+ }
+ return provider.options.filter(t => t.completed).length > 0
+ && !provider.allCompleted;
+ }
+
+ setAll(provider: CloudProviderItem, completed: boolean) {
+ provider.allCompleted = completed;
+ if (provider.options == null || provider.options.length === 0) {
+ return;
+ }
+ provider.options.forEach(t => t.completed = completed);
+ }
+
+ onCancelClick() {
+ this.dialogRef.close(undefined);
+ }
+
+ onSubmit() {
+ this.projectForm.markAllAsTouched();
+ if (this.projectForm.invalid) {
+ return;
+ }
+ const project = new Project();
+
+ project.metaData.name = this.name.value;
+ project.metaData.description = this.description.value;
+ project.metaData.owner = this.owner.value;
+
+ if (this.isUpdate) {
+ project.metaData.id = this.data.projectMeta.id;
+ project.metaData.cloudProviders = this.data.projectMeta.cloudProviders;
+ } else {
+ project.metaData.id = this.name.value + '_' + Date.now().toString(16);
+ project.metaData.cloudProviders = this.getCloudProviders();
+ // Create empty project model
+ project.model = new ClamsProject();
+ // Include Cloud Providers to model
+ project.model.cloudProviders = project.metaData.cloudProviders.map(jsonCloudProvider =>
+ CloudProviderFactory.fromJSON(jsonCloudProvider));
+ }
+ this.dialogRef.close({
+ isUpdate: this.isUpdate,
+ data: project
+ });
+ }
+
+ /**
+ * Check if the current input name already exists in the project manager and return error if it is the case.
+ * In case of update, ignore the name of this project.
+ *
+ * @param oldName: Name that is excluded from the duplicate check.
+ *
+ * @return Null if the strings do not match
+ * An object { duplicate: true } if the strings do match
+ */
+ duplicateNameValidator(oldName?: string) {
+ return (control) => {
+ for (const project of ProjectManager.projectMetas) {
+ if (oldName && !oldName.localeCompare(control.value.trim())) {
+ continue;
+ }
+ const name = project.name.toLowerCase();
+ if (!name.localeCompare(control.value.trim())) {
+ return {duplicate: true};
+ }
+ }
+ return null;
+ };
+ }
+
+ /**
+ * Return the appropriate error message for the name input form field.
+ *
+ * @return One of three strings:
+ * If no input is given: 'Please enter a name'
+ * If input is too long: 'Name too long'
+ * If name exists in metas: 'Name already exists'
+ */
+ getNameErrorMsg() {
+ return this.name.hasError('required') ? 'Please enter a name' :
+ this.name.hasError('maxlength') ? 'Name too long' :
+ this.name.invalid ? 'Name already exists' : '';
+ }
+
+ public getCloudProviders(): JsonCloudProvider[] {
+ return this.providerList.map(item => {
+ if (this.someComplete(item) || item.allCompleted) {
+ const provider = item.provider;
+ provider.regions = item.options.map(option => {
+ if (option.completed) {
+ return option.region;
+ }
+ return null;
+ }).filter(p => p);
+ return provider;
+ }
+ return null;
+ }).filter(p => p);
+ }
+
+ get description() {
+ return this.projectForm.get('description');
+ }
+
+ get name() {
+ return this.projectForm.get('name');
+ }
+
+ get owner() {
+ return this.projectForm.get('owner');
+ }
+}
diff --git a/src/styles.css b/src/styles.css
index a0a07e3..9c5939d 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,4 +1,4 @@
/* You can add global styles to this file, and also import other style files */
@import '@angular/material/prebuilt-themes/indigo-pink.css';
-html, body { height: 100%; }
+html, body { flex: 1; display: flex; flex-direction: column; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }