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

Feature: MP parser frontend scaffolding + API calls #84

Merged
merged 10 commits into from
Jan 9, 2025
7 changes: 5 additions & 2 deletions backend/parseport/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def get(self, request):
dict(
aethel=aethel_status(),
spindle=status_check('spindle'),
mp=status_check('minimalist_parser'),
vulcan=status_check('vulcan'),
mp=True,
vulcan=True,
# When the minimalist_parser and vulcan services are up and running, uncomment the following lines.
# mp=status_check('minimalist_parser'),
# vulcan=status_check('vulcan'),
)
)
2 changes: 1 addition & 1 deletion frontend/src/app/aethel/aethel.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ <h2 class="title is-2" i18n>Æthel</h2>
start.
</p>

@if (status$ | async) {
@if (statusOk$ | async) {
<form class="form" [formGroup]="form" (ngSubmit)="submitWord()">
<div class="field">
<label for="aethel-input" class="label">Search:</label>
Expand Down
11 changes: 4 additions & 7 deletions frontend/src/app/aethel/aethel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { AethelInput, AethelListResult } from "../shared/types";
import { AethelApiService } from "../shared/services/aethel-api.service";
import { Subject, distinctUntilChanged, map } from "rxjs";
import { distinctUntilChanged, map } from "rxjs";
import {
faChevronDown,
faChevronRight,
Expand Down Expand Up @@ -44,7 +44,9 @@ export class AethelComponent implements OnInit {
chevronDown: faChevronDown,
};

public status$ = new Subject<boolean>();
public statusOk$ = this.statusService.getStatus$().pipe(
map((status) => status.aethel),
);

constructor(
private apiService: AethelApiService,
Expand All @@ -55,11 +57,6 @@ export class AethelComponent implements OnInit {
) {}

ngOnInit(): void {
this.statusService
.get()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((status) => this.status$.next(status.aethel));

this.apiService.output$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((response) => {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { SampleComponent } from "./sample/sample.component";
import { SampleDataComponent } from "./aethel/sample-details/sample-data.component";
import { AboutComponent } from "./about/about.component";
import { SharedModule } from "./shared/shared.module";
import { MinimalistParserModule } from "./minimalist-parser/minimalist-parser.module";

@NgModule({
declarations: [
Expand All @@ -46,6 +47,11 @@ import { SharedModule } from "./shared/shared.module";
FontAwesomeModule,
TableModule,
SharedModule,
MinimalistParserModule
],
exports: [
FontAwesomeModule,
SharedModule,
],
bootstrap: [AppComponent],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,43 @@ <h1 class="title" i18n>Minimalist Parser</h1>

<p i18n>Please enter a sentence below to begin.</p>

@if (statusOk$ | async) {
<section class="section">
<div class="field">
<label for="minimalist-parser-input" class="label" i18n>
Enter a sentence
</label>
<div class="control">
<input id="minimalist-parser-input" type="text" class="input" placeholder="Enter a sentence" i18n-placeholder>
</div>
<form class="form" [formGroup]="form" (ngSubmit)="parse()">
<label for="minimalist-parser-input" class="label" i18n>
Enter a sentence
</label>
<div class="control-wrapper">
<div class="control">
<input
id="minimalist-parser-input"
type="text"
class="input"
[class.is-danger]="form.touched && form.invalid"
[formControl]="form.controls.mpInput"
placeholder="Enter a sentence"
i18n-placeholder
/>
</div>
<button
type="submit"
class="button is-primary"
[class.is-loading]="loading$ | async"
>
<span i18n>Submit</span>
</button>
</div>
@if (form.touched && form.invalid) {
<p class="is-size-7 has-text-danger" i18n>
Please enter a sentence first.
</p>
}
</form>
</div>
</section>


} @else {
<p class="notification mt-6" i18n>
The Minimalist Parser is temporarily unavailable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the 'temporarily'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important to give your users hope and prospects of a better future ;)

</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.control-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}

.control {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
import { Component } from "@angular/core";
import { Component, DestroyRef, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { map } from "rxjs";
import { MpApiService } from "src/app/shared/services/mp-api.service";
import { StatusService } from "src/app/shared/services/status.service";

@Component({
selector: "pp-minimalist-parser-input",
templateUrl: "./minimalist-parser-input.component.html",
styleUrl: "./minimalist-parser-input.component.scss",
})
export class MinimalistParserInputComponent {}
export class MinimalistParserInputComponent implements OnInit {
public form = new FormGroup({
mpInput: new FormControl<string>("", {
validators: [Validators.required],
}),
});

public loading$ = this.apiService.loading$;

public statusOk$ = this.statusService
.getStatus$()
.pipe(map((status) => status.mp && status.vulcan));

constructor(
private destroyRef: DestroyRef,
private apiService: MpApiService,
private statusService: StatusService,
) {}

ngOnInit(): void {
this.apiService.output$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((response) => {
if (!response) {
return;
}
// Do something with the response.
});
}

public parse(): void {
this.form.controls.mpInput.markAsTouched();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, considering the exportResult function from spindle.component.ts:

public parse(): void {
        this.form.controls.mpInput.markAsTouched();
        this.form.controls.mpInput.updateValueAndValidity();
        const input = this.form.controls.mpInput.value;
        if (this.form.invalid || !input) {
            return;
        }
        this.apiService.input$.next(input);
    }

These functions are very similar: could we think of a good way to make them generalizable / put them in a service? Is that even worthwhile, do you think? I'm happy to let this rest since it's not really priority but I was wondering all the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely there is room to DRY this code a bit more. I've created a new issue for it: #93 .

this.form.controls.mpInput.updateValueAndValidity();
const input = this.form.controls.mpInput.value;
if (this.form.invalid || !input) {
return;
}
this.apiService.input$.next(input);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { NgModule } from "@angular/core";
import { MinimalistParserComponent } from "./minimalist-parser.component";
import { CommonModule } from "@angular/common";
import { MinimalistParserAboutComponent } from "./minimalist-parser-about/minimalist-parser-about.component";
import { MinimalistParserInputComponent } from "./minimalist-parser-input/minimalist-parser-input.component";
import { MinimalistParserReferencesComponent } from "./minimalist-parser-references/minimalist-parser-references.component";
import { MinimalistParserBrowserComponent } from "./minimalist-parser-browser/minimalist-parser-browser.component";
import { SharedModule } from "../shared/shared.module";

@NgModule({
declarations: [
Expand All @@ -14,6 +14,6 @@ import { MinimalistParserBrowserComponent } from "./minimalist-parser-browser/mi
MinimalistParserReferencesComponent,
MinimalistParserBrowserComponent,
],
imports: [CommonModule],
imports: [SharedModule],
})
export class MinimalistParserModule {}
16 changes: 16 additions & 0 deletions frontend/src/app/shared/services/mp-api.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from "@angular/core/testing";

import { MpApiService } from "./mp-api.service";

describe("MpApiService", () => {
let service: MpApiService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MpApiService);
});

it("should be created", () => {
expect(service).toBeTruthy();
});
});
69 changes: 69 additions & 0 deletions frontend/src/app/shared/services/mp-api.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable } from "@angular/core";
import { ParsePortDataService } from "./ParsePortDataService";
import {
catchError,
distinctUntilChanged,
map,
merge,
of,
share,
Subject,
switchMap,
throttleTime,
} from "rxjs";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { environment } from "src/environments/environment";
import { ErrorHandlerService } from "./error-handler.service";

type MPInput = string;
type MPOutput = string;
type MPLoading = boolean;

@Injectable({
providedIn: "root",
})
export class MpApiService
implements ParsePortDataService<MPInput, MPOutput, MPLoading>
{
public input$ = new Subject<string>();

public throttledInput$ = this.input$.pipe(
distinctUntilChanged(),
throttleTime(300),
);

public output$ = this.throttledInput$.pipe(
switchMap((input) =>
this.http
.post<MPOutput | null>(
`${environment.apiUrl}mp/parse`,
{ input },
{
headers: new HttpHeaders({
"Content-Type": "application/json",
}),
},
)
.pipe(
catchError((error) => {
this.errorHandler.handleHttpError(
error,
$localize`An error occurred while handling your input.`,
);
return of(null);
}),
),
),
share(),
);

public loading$ = merge(
this.throttledInput$.pipe(map(() => true)),
this.output$.pipe(map(() => false)),
);

constructor(
private http: HttpClient,
private errorHandler: ErrorHandlerService,
) {}
}
8 changes: 5 additions & 3 deletions frontend/src/app/shared/services/status.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import { environment } from "src/environments/environment";
interface Status {
aethel: boolean;
spindle: boolean;
mp: boolean;
vulcan: boolean;
}

@Injectable({
providedIn: "root"
providedIn: "root",
})
export class StatusService{
export class StatusService {
constructor(private http: HttpClient) {}

get(): Observable<Status> {
public getStatus$(): Observable<Status> {
return this.http.get<Status>(`${environment.apiUrl}status/`);
}
}
8 changes: 7 additions & 1 deletion frontend/src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { CommonModule } from "@angular/common";
import { ExportTextComponent } from "./components/spindle-export/export-text/export-text.component";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { SpindleExportComponent } from "./components/spindle-export/spindle-export.component";
import { MpApiService } from "./services/mp-api.service";
import { ReactiveFormsModule } from "@angular/forms";

@NgModule({
declarations: [
Expand All @@ -25,21 +27,25 @@ import { SpindleExportComponent } from "./components/spindle-export/spindle-expo
ExportTextComponent,
SpindleExportComponent,
],
imports: [CommonModule, FontAwesomeModule],
imports: [CommonModule, FontAwesomeModule, ReactiveFormsModule],
providers: [
AethelApiService,
AlertService,
ConfigService,
ErrorHandlerService,
SpindleApiService,
StatusService,
MpApiService,
],
exports: [
AlertComponent,
AlertContainerDirective,
ProofPipe,
SpindleExportComponent,
ExportButtonComponent,
CommonModule,
ReactiveFormsModule,
FontAwesomeModule,
],
})
export class SharedModule {}
2 changes: 1 addition & 1 deletion frontend/src/app/spindle/spindle.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class SpindleComponent implements OnInit {

spindleReady$ = timer(0, 5000).pipe(
takeUntil(this.stopStatus$),
switchMap(() => this.statusService.get()),
switchMap(() => this.statusService.getStatus$()),
map((status) => status.spindle),
share(),
);
Expand Down
Loading