Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model validation UI + manifest-aware validation #236

Merged
merged 27 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
983c623
Add validatw API and UI
haneslinger Jan 3, 2023
3593616
Fix API
haneslinger Jan 11, 2023
9f65179
Move to drawer
haneslinger Jan 18, 2023
a24d585
Alter get shapes endpoint
haneslinger Jan 26, 2023
69edf42
Update validate endpoint to work with new shape endpoint
haneslinger Feb 2, 2023
3350664
Get shapes async
haneslinger Feb 21, 2023
16a64e0
Merge remote-tracking branch 'origin/develop' into validate-on-shapes
gtfierro Mar 30, 2023
1c8cbac
update docker compose
gtfierro Apr 1, 2023
53b0fb0
fix library test
gtfierro Apr 2, 2023
18741f6
Merge remote-tracking branch 'origin/Add-shape-collection-to-model' i…
gtfierro Apr 2, 2023
146d9c6
default validate() call to use manifest
gtfierro Apr 3, 2023
be04535
implement validation endpoint
gtfierro Apr 3, 2023
362e6f3
Merge branch 'develop' into gtf-214-update-with-docker-compose
gtfierro Apr 3, 2023
50c460e
running black to reformat
gtfierro Apr 3, 2023
7c19733
use environment variables from .env consistently throughout docker-co…
gtfierro Apr 4, 2023
b96af31
set -ex in Dockerfile for transparency
gtfierro Apr 5, 2023
e0d7c90
buildingmotif apiserver prioritizes DB_URI environment variable
gtfierro Apr 5, 2023
2c3a2c0
switch to list of libraries
gtfierro Apr 5, 2023
be91f4a
add test for nonexistent library ids
gtfierro Apr 5, 2023
192d748
Fix form group console errors
haneslinger Apr 5, 2023
c5e6cd9
make sure formControlName is present
gtfierro Apr 5, 2023
117aea2
add validationresponse object for structured output
gtfierro Apr 7, 2023
f57fe44
print list of reasons in validation output
gtfierro Apr 7, 2023
bc9020a
add validator
gtfierro Apr 7, 2023
74b8099
move bmotif api dockerfile + extras to the buildingmotif/api directory
gtfierro Apr 12, 2023
579af54
remove bind to local directories because it can complicate distributi…
gtfierro Apr 12, 2023
f54d8ee
use typing to remove need to explicitly call JSON.parse
gtfierro Apr 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
POSTGRES_DB=buildingmotif
POSTGRES_USER=buildingmotif
POSTGRES_PASSWORD=password
POSTGRES_PASSWORD=password
DB_URI="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
17 changes: 0 additions & 17 deletions Dockerfile

This file was deleted.

1 change: 1 addition & 0 deletions buildingmotif-app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ WORKDIR /buildingmotif-app
COPY . .

RUN npm install -g @angular/cli
RUN npm install

CMD ng serve --host 0.0.0.0
2 changes: 1 addition & 1 deletion buildingmotif-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const routes: Routes = [
{ path: 'models/new', component: ModelNewComponent},
{ path: 'templates/:id', component: TemplateDetailComponent },
{ path: 'templates/:id/evaluate', component: TemplateEvaluateComponent, resolve: {TemplateEvaluateResolver}},
{ path: 'models/:id', component: ModelDetailComponent, resolve: {ModelDetailResolver} },
{ path: 'models/:id', component: ModelDetailComponent, resolve: {ModelDetailResolver}},
{ path: 'templates', component: TemplateSearchComponent, resolve: {templateSearch:TemplateSearchResolver}},
{ path: 'models', component: ModelSearchComponent, resolve: {ModelSearchResolver}},
{ path: '', redirectTo: '/templates', pathMatch: 'full' },
Expand Down
13 changes: 11 additions & 2 deletions buildingmotif-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import {MatTableModule} from '@angular/material/table';
import { TemplateEvaluateResultComponent } from './template-evaluate/template-evaluate-result/template-evaluate-result.component';
import { TemplateEvaluateComponent} from './template-evaluate/template-evaluate.component'
import { ModelNewComponent } from './model-new/model-new.component';
import { ModelValidateComponent } from './model-validate/model-validate.component';
import { LibraryService } from './library/library.service';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatTabsModule} from '@angular/material/tabs';
import {MatCheckboxModule} from '@angular/material/checkbox';

@NgModule({
declarations: [
Expand All @@ -45,6 +50,7 @@ import { ModelNewComponent } from './model-new/model-new.component';
TemplateEvaluateFormComponent,
TemplateEvaluateResultComponent,
ModelNewComponent,
ModelValidateComponent,
],
imports: [
BrowserModule,
Expand All @@ -66,9 +72,12 @@ import { ModelNewComponent } from './model-new/model-new.component';
MatSnackBarModule,
CodemirrorModule,
MatCardModule,
MatTableModule
MatTableModule,
MatSidenavModule,
MatTabsModule,
MatCheckboxModule,
],
providers: [TemplateDetailService],
providers: [TemplateDetailService, LibraryService],
bootstrap: [AppComponent]
})
export class AppModule { }
16 changes: 16 additions & 0 deletions buildingmotif-app/src/app/library/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export interface Template {
dependency_ids: number[];
}

export interface Shape {
library_name: string;
library_id: number;
uri: string;
label: string;
description: string;
}

@Injectable()
export class LibraryService {

Expand All @@ -35,6 +43,14 @@ export class LibraryService {
);
}

getAllShapes() {
return this.http.get<Shape[]>("http://localhost:5000/libraries/shapes")
.pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}

getLibrarysTemplates(library_id: number) {
return this.http.get<Library>(`http://localhost:5000/libraries/${library_id}?expand_templates=True`)
.pipe(
Expand Down
20 changes: 18 additions & 2 deletions buildingmotif-app/src/app/model-detail/model-detail.component.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
.container {
height: 100%;
padding: 1rem 3rem;
}

.main-content-and-button {
height: 100%;
display: flex;
flex-direction: column;
}

.main-content {
padding: 1rem 3rem;
flex-grow: 1;
}

.title {
Expand All @@ -13,8 +20,17 @@
.buttons {
display: flex;
gap: 1rem;
padding-top: 1rem;
}

button {
width: 5rem;
}

.side-nav-button {
display: flex;
}

.sidenav-content {
width: 50%;
}
46 changes: 32 additions & 14 deletions buildingmotif-app/src/app/model-detail/model-detail.component.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
<div class="container">
<div class="title">{{model.name}}</div>

<ngx-codemirror #codemirror
class="graph"
[options]="codeMirrorOptions"
[formControl]="graphFormControl">
</ngx-codemirror>

<div class="buttons">
<button mat-raised-button (click)="onSave()" color="primary">Save</button>
<button mat-raised-button (click)="undoChangesToGraph()" color="primary">Undo</button>
</div>
</div>
<mat-drawer-container class="container">
<div class="main-content-and-button">
<!-- Main Content -->
<div class="main-content">
<div class="title">{{model.name}}</div>

<ngx-codemirror #codemirror class="graph" [options]="codeMirrorOptions" [formControl]="graphFormControl">
</ngx-codemirror>

<div class="buttons">
<button mat-raised-button (click)="onSave()" color="primary">Save</button>
<button mat-raised-button (click)="undoChangesToGraph()" color="primary">Undo</button>
</div>
</div>

<button class="side-nav-button" mat-flat-button (click)="drawer.toggle()" color="basic">
<mat-icon *ngIf="!drawer.opened"> arrow_backward </mat-icon>
<mat-icon *ngIf="drawer.opened"> arrow_forward </mat-icon>
</button>
</div>

<!-- Side Nav -->
<mat-drawer #drawer class="sidenav-content" mode="side" position="end">
<mat-tab-group>
<mat-tab label="First"> Content 1 </mat-tab>
<mat-tab label="Second"> Content 2 </mat-tab>
<mat-tab label="Validate">
<app-model-validate class="validate" [modelId]=model.id></app-model-validate>
</mat-tab>
</mat-tab-group>
</mat-drawer>
</mat-drawer-container>
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export class ModelDetailComponent{
matchBrackets: true,
lint: true
};
showFiller: boolean = true;
sideNaveOpen: boolean = false;

constructor(
private route: ActivatedRoute,
private ModelDetailService: ModelDetailService,
private _snackBar: MatSnackBar,
) {
[this.model, this.graph] = route.snapshot.data["ModelDetailResolver"]
this.graphFormControl.setValue(this.graph)
[this.model, this.graph] = route.snapshot.data["ModelDetailResolver"];
this.graphFormControl.setValue(this.graph);
}

onSave(): void{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.container{
display: flex;
flex-direction: column;
padding: 2rem;
gap: 1rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="container">
<mat-progress-bar *ngIf="showGettingLibrariesSpinner" mode="query"></mat-progress-bar>
<mat-list *ngFor="let library of libraries; let i = index" [formGroup]="selectedLibrariesForm">
<mat-checkbox [formControlName]="i" [formGroupName]="i">
<b>{{library.name}}</b>
</mat-checkbox>
</mat-list>

<button
(click)="validate()"
[disabled]="selectedLibrariesForm.invalid"
mat-raised-button
color="primary">
Validate
</button>

<mat-progress-bar *ngIf="showValidatingSpinner" mode="indeterminate"></mat-progress-bar>
<span><b *ngIf="validationResponse">Valid?</b> {{ validationResponse?.valid }}</span>
<ngx-codemirror #codemirror
*ngIf = "validationResponse"
[(ngModel)]="validationResponse.message"
[options]="codeMirrorOptions">
</ngx-codemirror>
<mat-list *ngFor="let reason of validationResponse?.reasons">
<span> {{ reason }} </span>
</mat-list>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Component, Input, OnInit} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormControl, FormGroup, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms';
import { ModelValidateService, ValidationResponse } from './model-validate.service';
import { LibraryService, Library } from '../library/library.service';

// verify that at least one checkbox is checked in the FormGroup
function NoneSelectedValidator(): ValidatorFn {
return (control: AbstractControl) => {
const formGroup = control as FormGroup;
const checkedKeys = Object.keys(formGroup.controls).filter((key) => formGroup.controls[key].value);

if (checkedKeys.length === 0) { return { noneSelected: {value: true}}; }

return null;
};
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
}

@Component({
selector: 'app-model-validate',
templateUrl: './model-validate.component.html',
providers: [ModelValidateService, LibraryService],
styleUrls: ['./model-validate.component.css'],
})
export class ModelValidateComponent implements OnInit{
@Input() modelId: number | undefined;
libraries?: Library[] = undefined;
selectedLibrariesForm: FormGroup = new FormGroup({});
validationResponse?: ValidationResponse = undefined;
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
showGettingLibrariesSpinner = false;
showValidatingSpinner = false;

codeMirrorOptions: any = {
theme: 'material',
mode: 'text',
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
lineNumbers: true,
lineWrapping: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
autoCloseBrackets: true,
matchBrackets: true,
lint: true,
readOnly: true,
};

constructor(private modelValidateService: ModelValidateService, private libraryService: LibraryService) {}

ngOnInit(): void {
this.showGettingLibrariesSpinner = true;
this.libraryService.getAllLibraries().subscribe(
res => {
this.populateSelectedLibrariesControls(res);
this.libraries = res;
},
err => {},
() => {this.showGettingLibrariesSpinner = false},
);
}

populateSelectedLibrariesControls(libraries: Library[]): void {
const selectedLibrariesControls: { [library_index: number]: FormControl } = libraries.reduce((acc, _, i) => {
return { ...acc, [i]: new FormControl(false) }
}, {});
this.selectedLibrariesForm = new FormGroup(selectedLibrariesControls, {validators: NoneSelectedValidator()})
}

validate(): void {
if (this.libraries == undefined) return;

const selectedLibraries = this.libraries.filter((_, i) => this.selectedLibrariesForm.value[i])
const args = selectedLibraries.map(l => l.id);

if (!!this.modelId){
this.showValidatingSpinner = true;

this.modelValidateService.validateModel(this.modelId, args).subscribe(
res => {this.validationResponse = res},
err => {},
() => {this.showValidatingSpinner = false},
);
}
}
}
47 changes: 47 additions & 0 deletions buildingmotif-app/src/app/model-validate/model-validate.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpErrorResponse } from '@angular/common/http';
import { Model } from '../types'
import { throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class ModelValidateService {

constructor(private http: HttpClient) { }

validateModel(modelId: number, args: number[]) {
const headers = {'Content-Type': "application/json"}

return this.http.post<ValidationResponse>(`http://localhost:5000/models/${modelId}/validate`,
{"library_ids": args},
{headers, responseType: 'json'}
)
.pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}

private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(
`Backend returned code ${error.status}, body was: `, error.error);
}
// Return an observable with a user-facing error message.
return throwError(() => new Error(`${error.status}: ${error.error}`));
}
}

export interface ValidationResponse {
valid: boolean;
message: string;
reasons: string[];
}
20 changes: 20 additions & 0 deletions buildingmotif/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM python:3.8

# Copy project
ADD buildingmotif /opt/buildingmotif
ADD libraries /opt/libraries
COPY configs.py /opt/
ADD migrations /opt/migrations
COPY alembic.ini /opt/
COPY pyproject.toml /opt/
COPY poetry.lock /opt/
ADD docs /opt/docs
ADD buildingmotif/api/start.sh /opt/start.sh
WORKDIR /opt/

# Install dpeendencies
RUN pip install poetry==1.4.0 && poetry config virtualenvs.create false
RUN ls /opt && poetry install --no-dev
RUN chmod +x /opt/start.sh

CMD /opt/start.sh
Loading