Skip to content
Snippets Groups Projects
Commit 0fbe6a4f authored by Clayton, Brandon Scott's avatar Clayton, Brandon Scott
Browse files

switch to signals and reactive forms

parent c2ce5728
No related branches found
No related tags found
1 merge request!443Signals and Reactive Forms: AWS Applications
Showing
with 134 additions and 372 deletions
import {Routes} from '@angular/router';
import {provideEffects} from '@ngrx/effects';
import {provideState} from '@ngrx/store';
import {TerminateHazJobsAppEffects} from './terminate-haz-jobs/state/app.effect';
import {terminateHazJobsAppFeature} from './terminate-haz-jobs/state/app.reducer';
const routes: Routes = [
{
......@@ -22,10 +17,6 @@ const routes: Routes = [
com => com.AppComponent
),
path: 'terminate-haz-jobs',
providers: [
provideState(terminateHazJobsAppFeature),
provideEffects(TerminateHazJobsAppEffects),
],
},
{
loadComponent: () =>
......
<nshmp-template-form-fields>
@if (form$ | async; as form) {
<div class="grid-container center-x form">
<form
class="grid-col-10 tablet:grid-col-8"
[ngrxFormState]="form"
(submit)="facade.callJobInfoService()"
>
<mat-label class="label">AWS nshmp-haz Job ID</mat-label>
<div class="grid-container center-x form">
<form
class="grid-col-10 tablet:grid-col-8"
[formGroup]="form"
(submit)="facade.callJobInfoService()"
>
<mat-label class="label">AWS nshmp-haz Job ID</mat-label>
<mat-form-field class="grid-col-12">
<mat-label>Job ID <span class="form-required">*</span></mat-label>
<input
matInput
type="text"
[ngrxFormControlState]="form?.controls?.id"
/>
<div matSuffix matTooltip="The id of the running job to terminate">
<mat-icon aria-label="Info icon" fontIcon="info" />
</div>
<mat-hint>The id of the running job to terminate</mat-hint>
</mat-form-field>
<!-- Buttons -->
<div class="padding-y-2 grid-col-12">
<button
mat-raised-button
color="warn"
type="submit"
[disabled]="form?.isInvalid"
>
Terminate Job
</button>
<mat-form-field class="grid-col-12">
<mat-label>Job ID <span class="form-required">*</span></mat-label>
<input matInput type="text" [formControl]="form.controls.id" />
<div matSuffix matTooltip="The id of the running job to terminate">
<mat-icon aria-label="Info icon" fontIcon="info" />
</div>
</form>
</div>
}
<mat-hint>The id of the running job to terminate</mat-hint>
</mat-form-field>
<!-- Buttons -->
<div class="padding-y-2 grid-col-12">
<button
mat-raised-button
color="warn"
type="submit"
[disabled]="form.invalid"
>
Terminate Job
</button>
</div>
</form>
</div>
</nshmp-template-form-fields>
import {AsyncPipe} from '@angular/common';
import {Component} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {
MatFormField,
MatHint,
MatLabel,
MatSuffix,
} from '@angular/material/form-field';
import {MatIcon} from '@angular/material/icon';
import {MatInput} from '@angular/material/input';
import {MatTooltip} from '@angular/material/tooltip';
import {NshmpNgrxFormsModule} from '@ghsc/nshmp-lib-ng/nshmp';
import {NshmpTemplateFormFieldsComponent} from '@ghsc/nshmp-template';
import {AppFacade} from '../../state/app.facade';
......@@ -26,7 +27,8 @@ import {AppFacade} from '../../state/app.facade';
MatHint,
AsyncPipe,
NshmpTemplateFormFieldsComponent,
NshmpNgrxFormsModule,
ReactiveFormsModule,
MatIcon,
],
selector: 'app-content',
standalone: true,
......@@ -35,7 +37,7 @@ import {AppFacade} from '../../state/app.facade';
})
export class ContentComponent {
/** Form state */
form$ = this.facade.controlForm$;
form = this.facade.formGroup;
constructor(public facade: AppFacade) {}
}
import {createActionGroup, emptyProps} from '@ngrx/store';
import {awsActions} from 'projects/nshmp-apps/src/shared/state/aws';
export const actions = createActionGroup({
events: {
...awsActions,
/** NGRX action for application initialization */
init: emptyProps(),
/** NGRX action control form state initialization */
'Inital Form Set': emptyProps(),
},
source: 'Terminate nshmp-haz Jobs on AWS Development App',
});
import {Injectable} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AwsService, IdForm} from '@ghsc/nshmp-lib-ng/aws';
import {NshmpService} from '@ghsc/nshmp-lib-ng/nshmp';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {concatLatestFrom} from '@ngrx/operators';
import {Store} from '@ngrx/store';
import {MarkAsDirtyAction, SetValueAction} from 'ngrx-forms';
import {environment} from 'projects/nshmp-apps/src/environments/environment';
import {devApps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
import {catchError, map, mergeMap} from 'rxjs/operators';
import {actions} from './app.actions';
import {AppFacade} from './app.facade';
import {terminateHazJobsAppFeature} from './app.reducer';
import {CONTROL_FORM_ID, DEFAULT_FORM_VALUES} from './app.state';
/**
* Applicaiton NGRX effects
*/
@Injectable()
export class TerminateHazJobsAppEffects {
baseUrl = environment.webServices.aws.url;
jobsService = environment.webServices.aws.services.hazardJobs;
terminateService = environment.webServices.aws.services.terminateJob;
/**
* NGRX effect to call job history service.
*/
callJobInfoService$ = this.awsService.callJobHistoryServiceEffect({
ofTypeAction: actions.callJobHistoryService,
returnAction: actions.jobHistoryServiceResponse,
selectFormGroup: this.store.select(
terminateHazJobsAppFeature.selectControlForm
),
serviceUrl: `${this.baseUrl}/${this.jobsService}`,
});
/**
* NGRX effect to call the terminate job service
*/
callTerminateJobService$ = this.awsService.callTerminateJobServiceEffect({
ofTypeAction: actions.callTerminateJobService,
returnAction: actions.terminateJobServiceResponse,
selectFormGroup: this.store.select(
terminateHazJobsAppFeature.selectControlForm
),
serviceUrl: `${this.baseUrl}/${this.terminateService}`,
});
/**
* NGRX effect to initialize application.
*/
init$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.init),
concatLatestFrom(() =>
this.store.select(terminateHazJobsAppFeature.selectControlForm)
),
mergeMap(([, form]) => {
const query = this.route.snapshot.queryParams as IdForm;
const formValues = {
...DEFAULT_FORM_VALUES,
...query,
};
return [
new SetValueAction(form.id, formValues),
actions.initalFormSet(),
];
}),
catchError((error: Error) => this.nshmpService.throwError$(error))
)
);
/**
* NGRX effect to check the initial for set from the query and call service
* if control panel is valid.
*/
initialFormSet$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.initalFormSet),
concatLatestFrom(() =>
this.store.select(terminateHazJobsAppFeature.selectControlForm)
),
map(([, form]) => {
if (form.isValid) {
return actions.callJobHistoryService();
} else {
return new MarkAsDirtyAction(form.id);
}
}),
catchError((error: Error) => this.nshmpService.throwError$(error))
)
);
/**
* NGRX effect to open the terminating job dialog to allow user
* to actually terminate job.
*/
openTerminatingDialog$ = this.awsService.openTerminateDialogEffect({
jobInfoServiceResponseAction: actions.jobHistoryServiceResponse,
onTerminateFunction: () => this.facade.callTerminateJobService(),
selectJobInfo: this.store.select(
terminateHazJobsAppFeature.selectJobHistoryResponse
),
});
/**
* NGRX effect to update the query parameters with the job id.
*/
updateUrl$ = createEffect(
() =>
this.actions$.pipe(
ofType(SetValueAction.TYPE),
concatLatestFrom(() =>
this.store.select(terminateHazJobsAppFeature.selectControlForm)
),
map(([action, form]) => {
if (
(action as SetValueAction<unknown>).controlId.includes(
CONTROL_FORM_ID
)
) {
const query = {
jobId: form.value.id,
};
this.router
.navigate([devApps().aws.terminateHazJobs.routerLink], {
queryParams: {
...query,
},
})
.catch((error: Error) => this.nshmpService.throwError$(error));
}
}),
catchError((error: Error) => this.nshmpService.throwError$(error))
),
{dispatch: false}
);
constructor(
private actions$: Actions,
private awsService: AwsService,
private facade: AppFacade,
private route: ActivatedRoute,
private router: Router,
private nshmpService: NshmpService,
private store: Store
) {}
}
import {Injectable} from '@angular/core';
import {computed, Injectable, Signal, signal} from '@angular/core';
import {FormBuilder, Validators} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {
AwsService,
IdForm,
JobHistoryRequestData,
TerminateJobRequestData,
} from '@ghsc/nshmp-lib-ng/aws';
} from '@ghsc/nshmp-lib-no-ngrx/aws';
import {NshmpService} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
import {DynamoDBItem} from '@ghsc/nshmp-utils-ts/libs/aws/run-nshmp-haz';
import {Response} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils';
import {select, Store} from '@ngrx/store';
import {EC2} from 'aws-sdk';
import {FormGroupState} from 'ngrx-forms';
import {Observable} from 'rxjs';
import {environment} from 'projects/nshmp-apps/src/environments/environment';
import {devApps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
import {actions} from './app.actions';
import {terminateHazJobsAppFeature} from './app.reducer';
import {AppState, INITIAL_STATE} from './app.state';
/**
* Entrypoint for accessing NGRX store.
......@@ -21,57 +23,121 @@ import {terminateHazJobsAppFeature} from './app.reducer';
providedIn: 'root',
})
export class AppFacade {
constructor(private store: Store) {}
readonly formGroup = this.formBuilder.group<IdForm>({
id: null,
});
/**
* Returns the control form state observable.
*/
get controlForm$(): Observable<FormGroupState<IdForm>> {
return this.store.pipe(
select(terminateHazJobsAppFeature.selectControlForm)
);
private state = signal<AppState>(INITIAL_STATE);
private baseUrl = environment.webServices.aws.url;
private jobsService = environment.webServices.aws.services.hazardJobs;
private terminateService = environment.webServices.aws.services.terminateJob;
constructor(
private formBuilder: FormBuilder,
private nshmpService: NshmpService,
private awsService: AwsService,
private route: ActivatedRoute,
private router: Router
) {
this.formGroup.controls.id.addValidators([
control => Validators.required(control),
Validators.pattern(/^[a-zA-Z0-9]*$/),
]);
this.formGroup.valueChanges.subscribe(() => this.updateUrl());
}
/**
* Returns the job info service response observable.
*/
get jobInfoResponse$(): Observable<
Response<JobHistoryRequestData, DynamoDBItem>
> {
return this.store.pipe(
select(terminateHazJobsAppFeature.selectJobHistoryResponse)
);
get jobInfoResponse(): Signal<Response<JobHistoryRequestData, DynamoDBItem>> {
return computed(() => this.state().jobHistoryResponse);
}
/**
* Returns the terminate job service response observable.
*/
get terminateJobResponse$(): Observable<
get terminateJobResponse(): Signal<
Response<TerminateJobRequestData, EC2.InstanceStateChangeList>
> {
return this.store.pipe(
select(terminateHazJobsAppFeature.selectTerminateJobResponse)
);
return computed(() => this.state().terminateJobResponse);
}
/**
* Dispatch action to call job info service.
*/
callJobInfoService(): void {
this.store.dispatch(actions.callJobHistoryService());
this.awsService
.callJobHistoryService<DynamoDBItem>(
`${this.baseUrl}/${this.jobsService}`,
this.formGroup.controls.id
)
.subscribe(jobHistoryResponse => {
this.updateState({jobHistoryResponse});
this.awsService.openTerminateDialog(jobHistoryResponse, () =>
this.callTerminateJobService()
);
});
}
/**
* Dispatch action to call terminate job service.
*/
callTerminateJobService(): void {
this.store.dispatch(actions.callTerminateJobService());
this.awsService
.callTerminateJobServiceEffect(
`${this.baseUrl}/${this.terminateService}`,
this.formGroup.controls.id
)
.subscribe(terminateJobResponse =>
this.updateState({terminateJobResponse})
);
}
/**
* Dispatch action to initialize application.
*/
init(): void {
this.store.dispatch(actions.init());
const query = this.route.snapshot.queryParams as IdForm;
const formValues: IdForm = {
...query,
};
this.formGroup.patchValue(formValues);
if (this.formGroup.valid) {
this.nshmpService.selectPlotControl();
this.callJobInfoService();
}
}
/**
* Update state.
*
* @param state Partial state to update
*/
private updateState(state: Partial<AppState>): void {
this.state.set({
...this.state(),
...state,
});
}
/**
* Update URL based on form values.
*/
private updateUrl(): void {
const query: Partial<IdForm> = {
id: this.formGroup.getRawValue().id,
};
this.router
.navigate([devApps().aws.terminateHazJobs.routerLink], {
queryParams: {
...query,
},
})
.catch((error: Error) => this.nshmpService.throwError$(error));
}
}
import {IdForm} from '@ghsc/nshmp-lib-ng/aws';
import {createFeature, createReducer, on} from '@ngrx/store';
import {
onNgrxForms,
onNgrxFormsAction,
SetValueAction,
updateGroup,
validate,
wrapReducerWithFormStateUpdate,
} from 'ngrx-forms';
import {pattern, required} from 'ngrx-forms/validation';
import {actions} from './app.actions';
import {INITIAL_STATE} from './app.state';
export const terminateHazJobsAppFeature = createFeature({
name: 'terminateHazJobs',
reducer: createReducer(
/** Initial state */
INITIAL_STATE,
/** ngrx-forms reducer */
onNgrxForms(),
/** Handle ngrx-form actions */
onNgrxFormsAction(SetValueAction, state => {
return {
...state,
};
}),
/** Handle job info service response action */
on(actions.jobHistoryServiceResponse, (state, {response}) => {
return {
...state,
jobHistoryResponse: response,
};
}),
/** Handle terminate job service response action */
on(actions.terminateJobServiceResponse, (state, {response}) => {
return {
...state,
terminateJobResponse: response,
};
})
),
});
/**
* NGRX application raw reducer.
*/
/** Control form validators */
const validators = updateGroup<IdForm>({
id: validate([required, pattern(/^[a-zA-Z0-9]*$/)]),
});
/**
* Application NGRX reducer with validators.
*/
terminateHazJobsAppFeature.reducer = wrapReducerWithFormStateUpdate(
terminateHazJobsAppFeature.reducer,
state => state.controlForm,
validators
);
import {IdForm} from '@ghsc/nshmp-lib-ng/aws';
import {DynamoDBItem} from '@ghsc/nshmp-utils-ts/libs/aws/run-nshmp-haz';
import {createFormGroupState} from 'ngrx-forms';
import {TerminateJobState} from 'projects/nshmp-apps/src/shared/state/aws';
/** Control form id for ngrx-forms */
export const CONTROL_FORM_ID = '[ngrx-forms] AWS cancel nshmp-haz jobs';
/** Default form values */
export const DEFAULT_FORM_VALUES: IdForm = {
id: null,
};
/** Initial control form state */
const initialControlFormState = createFormGroupState<IdForm>(CONTROL_FORM_ID, {
...DEFAULT_FORM_VALUES,
});
/**
* Application NGRX state.
*/
export type AppState = TerminateJobState<IdForm, DynamoDBItem>;
export type AppState = TerminateJobState<DynamoDBItem>;
/**
* The initial application NGRX state.
*/
export const INITIAL_STATE: AppState = {
controlForm: initialControlFormState,
jobHistoryResponse: null,
terminateJobResponse: null,
};
import {
JobHistoryRequestData,
TerminateJobRequestData,
} from '@ghsc/nshmp-lib-ng/aws';
import {DynamoDBItem} from '@ghsc/nshmp-utils-ts/libs/aws/run-nshmp-haz';
import {Response} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils';
import {emptyProps, props} from '@ngrx/store';
/**
* Common AWS NGRX actions.
*/
export const awsActions = {
/** NGRX action to call job history service */
'Call Job History Service': emptyProps(),
/** NGRX action to call terminate job service */
'Call Terminate Job Service': emptyProps(),
/** NGRX action for job history response */
'Job History Service Response': props<{
response: Response<JobHistoryRequestData, DynamoDBItem>;
}>(),
/** NGRX action for job history responses */
'Job History Service Responses': props<{
response: Response<JobHistoryRequestData, DynamoDBItem[]>;
}>(),
/** NGRX action for terminate job service response */
'Terminate Job Service Response': props<{
response: Response<
TerminateJobRequestData,
AWS.EC2.InstanceStateChangeList
>;
}>(),
};
import {
IdForm,
JobHistoryRequestData,
TerminateJobRequestData,
} from '@ghsc/nshmp-lib-ng/aws';
import {DynamoDBItem} from '@ghsc/nshmp-utils-ts/libs/aws/run-nshmp-haz';
import {Response} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils';
import {EC2} from 'aws-sdk';
import {FormGroupState} from 'ngrx-forms';
/**
* NGRX state for control form fields with AWS hazard job id.
* State for job history.
*/
export interface IdFormState<T extends IdForm> {
/** The control form state */
controlForm: FormGroupState<T>;
}
/**
* NGRX state for job history.
*/
export interface JobHistoryState<
T extends IdForm,
U extends DynamoDBItem | DynamoDBItem[],
> extends IdFormState<T> {
export interface JobHistoryState<T extends DynamoDBItem | DynamoDBItem[]> {
/** Job history response */
jobHistoryResponse: Response<JobHistoryRequestData, U>;
jobHistoryResponse: Response<JobHistoryRequestData, T>;
}
/**
* NGRX state for terminating job.
* State for terminating job.
*/
export interface TerminateJobState<
T extends IdForm,
U extends DynamoDBItem | DynamoDBItem[],
> extends JobHistoryState<T, U> {
export interface TerminateJobState<T extends DynamoDBItem | DynamoDBItem[]>
extends JobHistoryState<T> {
/** Terminate job response */
terminateJobResponse: Response<
TerminateJobRequestData,
......
......@@ -2,5 +2,4 @@
* Export state
*/
export * from './aws.actions';
export * from './aws.state';
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment