diff --git a/module-frontend/package-lock.json b/module-frontend/package-lock.json index 458568b..76df3a4 100644 --- a/module-frontend/package-lock.json +++ b/module-frontend/package-lock.json @@ -18,6 +18,8 @@ "@angular/platform-browser": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0", "@angular/router": "^16.1.0", + "@ngrx/effects": "^16.1.0", + "@ngrx/store": "^16.1.0", "keycloak-angular": "^14.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -27,6 +29,7 @@ "@angular-devkit/build-angular": "^16.1.4", "@angular/cli": "~16.1.4", "@angular/compiler-cli": "^16.1.0", + "@angular/localize": "^16.1.5", "@types/jasmine": "~4.3.0", "jasmine-core": "~4.6.0", "karma": "~6.4.0", @@ -414,6 +417,29 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/localize": { + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.5.tgz", + "integrity": "sha512-8ApTdmv4sH0VbW9kVNanze5DEmb3OPIGzbD19jzvUSb6mTVMfUcQrsf4h+H8+cT+epBhor8RgVeVbUJaUbaLNQ==", + "dev": true, + "dependencies": { + "@babel/core": "7.22.5", + "fast-glob": "3.2.12", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "16.1.5", + "@angular/compiler-cli": "16.1.5" + } + }, "node_modules/@angular/material": { "version": "16.1.5", "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz", @@ -3650,6 +3676,31 @@ "tslib": "^2.1.0" } }, + "node_modules/@ngrx/effects": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-16.1.0.tgz", + "integrity": "sha512-sdLzxjdQcYht3SYiuhchF4uOxxZIy3h9TEoyVp6PZJmiLJqhMAmV9/+s3tPD7yKd4+4o4n+HeiLDOTlMxn3Bxg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^16.0.0", + "@ngrx/store": "16.1.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-16.1.0.tgz", + "integrity": "sha512-cGwI5wy+irIudQubfrbaKrDbJxCvu+lZYlBmsOlpzevQLCa+ZjNLrN05J025P3KvUmfzX4StEpAc8Ord089Kig==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^16.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, "node_modules/@ngtools/webpack": { "version": "16.1.4", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz", @@ -12593,6 +12644,17 @@ "tslib": "^2.3.0" } }, + "@angular/localize": { + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.5.tgz", + "integrity": "sha512-8ApTdmv4sH0VbW9kVNanze5DEmb3OPIGzbD19jzvUSb6mTVMfUcQrsf4h+H8+cT+epBhor8RgVeVbUJaUbaLNQ==", + "dev": true, + "requires": { + "@babel/core": "7.22.5", + "fast-glob": "3.2.12", + "yargs": "^17.2.1" + } + }, "@angular/material": { "version": "16.1.5", "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz", @@ -14998,6 +15060,22 @@ "tslib": "^2.1.0" } }, + "@ngrx/effects": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-16.1.0.tgz", + "integrity": "sha512-sdLzxjdQcYht3SYiuhchF4uOxxZIy3h9TEoyVp6PZJmiLJqhMAmV9/+s3tPD7yKd4+4o4n+HeiLDOTlMxn3Bxg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-16.1.0.tgz", + "integrity": "sha512-cGwI5wy+irIudQubfrbaKrDbJxCvu+lZYlBmsOlpzevQLCa+ZjNLrN05J025P3KvUmfzX4StEpAc8Ord089Kig==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "16.1.4", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz", diff --git a/module-frontend/package.json b/module-frontend/package.json index 4df3b3b..a4acdea 100644 --- a/module-frontend/package.json +++ b/module-frontend/package.json @@ -20,6 +20,8 @@ "@angular/platform-browser": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0", "@angular/router": "^16.1.0", + "@ngrx/effects": "^16.1.0", + "@ngrx/store": "^16.1.0", "keycloak-angular": "^14.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -29,6 +31,7 @@ "@angular-devkit/build-angular": "^16.1.4", "@angular/cli": "~16.1.4", "@angular/compiler-cli": "^16.1.0", + "@angular/localize": "^16.1.5", "@types/jasmine": "~4.3.0", "jasmine-core": "~4.6.0", "karma": "~6.4.0", diff --git a/module-frontend/src/app/app-routing.module.ts b/module-frontend/src/app/app-routing.module.ts index 9f29392..175171f 100644 --- a/module-frontend/src/app/app-routing.module.ts +++ b/module-frontend/src/app/app-routing.module.ts @@ -1,23 +1,16 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {DashboardComponent} from "./dashboard/dashboard.component"; -import {ElectionsComponent} from "./elections/elections.component"; -import {AppAuthGuard} from "./auth/auth.guard"; import {PermissionDeniedComponent} from "./permission-denied/permission-denied.component"; const routes: Routes = [ { path: '', component: DashboardComponent, - canActivate: [AppAuthGuard], children: [ { path: 'elections', - component: ElectionsComponent, - data: { - roles: ['USER'] - }, - canActivate: [AppAuthGuard] + loadChildren: () => import("./elections/elections.module").then(mod => mod.ElectionsModule) }, { path: 'permission-denied', diff --git a/module-frontend/src/app/app.component.html b/module-frontend/src/app/app.component.html index a419b82..4683f1c 100644 --- a/module-frontend/src/app/app.component.html +++ b/module-frontend/src/app/app.component.html @@ -1,4 +1,3 @@ - diff --git a/module-frontend/src/app/app.module.ts b/module-frontend/src/app/app.module.ts index de56c32..c382010 100644 --- a/module-frontend/src/app/app.module.ts +++ b/module-frontend/src/app/app.module.ts @@ -11,45 +11,45 @@ import {MatGridListModule} from "@angular/material/grid-list"; import {MatIconModule} from "@angular/material/icon"; import {MatListModule} from "@angular/material/list"; import {KeycloakAngularModule, KeycloakService} from "keycloak-angular"; -import {ElectionsComponent} from './elections/elections.component'; import {PermissionDeniedComponent} from './permission-denied/permission-denied.component'; -import {MessagesComponent} from './messages/messages.component'; import {HttpClientModule} from "@angular/common/http"; import {environment} from "../environments/environment"; import {NavigationComponent} from './navigation/navigation.component'; import {MatSidenavModule} from '@angular/material/sidenav'; import {MatMenuModule} from "@angular/material/menu"; import {MatTableModule} from "@angular/material/table"; +import {StoreModule} from '@ngrx/store'; +import {ElectionsModule} from "./elections/elections.module"; +import {EffectsModule} from '@ngrx/effects'; +import {MessagesModule} from "./messages/messages.module"; function initializeKeycloak(keycloak: KeycloakService) { return () => - keycloak.init({ - config: { - url: environment.keycloakURL, - realm: environment.realm, - clientId: environment.clientId, - }, - initOptions: { - onLoad: 'check-sso', - silentCheckSsoRedirectUri: - window.location.origin + '/assets/silent-check-sso.html', - flow: "standard" - }, - shouldAddToken: (request) => { - const { url } = request; - return url.startsWith(environment.backendURL); - }, - loadUserProfileAtStartUp: true - }); + keycloak.init({ + config: { + url: environment.keycloakURL, + realm: environment.realm, + clientId: environment.clientId, + }, + initOptions: { + onLoad: 'check-sso', + silentCheckSsoRedirectUri: + window.location.origin + '/assets/silent-check-sso.html', + flow: "standard" + }, + shouldAddToken: (request) => { + const {url} = request; + return url.startsWith(environment.backendURL); + }, + loadUserProfileAtStartUp: true + }); } @NgModule({ declarations: [ AppComponent, DashboardComponent, - ElectionsComponent, PermissionDeniedComponent, - MessagesComponent, NavigationComponent ], imports: [ @@ -66,7 +66,11 @@ function initializeKeycloak(keycloak: KeycloakService) { HttpClientModule, MatSidenavModule, MatMenuModule, - MatTableModule + MatTableModule, + ElectionsModule, + MessagesModule, + StoreModule.forRoot({}, {}), + EffectsModule.forRoot([]) ], providers: [ { diff --git a/module-frontend/src/app/dashboard/dashboard.component.ts b/module-frontend/src/app/dashboard/dashboard.component.ts index f7bd656..58bb724 100644 --- a/module-frontend/src/app/dashboard/dashboard.component.ts +++ b/module-frontend/src/app/dashboard/dashboard.component.ts @@ -1,7 +1,7 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import {MatTableDataSource} from "@angular/material/table"; import {ElectionService} from "../elections/election.service"; -import {Election} from "../elections/election"; +import {Election} from "../elections/model/election"; import {Subscription} from "rxjs"; @Component({ diff --git a/module-frontend/src/app/elections/election.service.ts b/module-frontend/src/app/elections/election.service.ts index 2623a1c..4d27b4f 100644 --- a/module-frontend/src/app/elections/election.service.ts +++ b/module-frontend/src/app/elections/election.service.ts @@ -1,9 +1,12 @@ import {Injectable} from '@angular/core'; import {HttpClient, HttpHeaders} from "@angular/common/http"; import {environment} from "../../environments/environment"; -import {Election} from "./election"; -import {catchError, Observable, of, tap} from "rxjs"; -import {MessageService} from "../messages/message.service"; +import {Election} from "./model/election"; +import {catchError, Observable, of} from "rxjs"; +import {Store} from "@ngrx/store"; +import {electionByName} from "./store"; +import {MessagesService} from "../messages/messages.service"; +import {MessageType} from "../messages/model/message-type"; @Injectable({ providedIn: 'root' @@ -17,16 +20,20 @@ export class ElectionService { }; constructor(private http: HttpClient, - private messageService: MessageService) { } + private store: Store, + private messageService: MessagesService) { } getElections(): Observable { - return this.http.get(this.electionsURL) + return this.http.get(this.electionsURL, this.httpOptions) .pipe( - tap(_ => this.log('fetched elections')), catchError(this.handleError('getElections', [])) ); } + selectElection(name: string) { + return this.store.select(electionByName(name)); + } + /** * Handle Http operation that failed. * Let the app continue. @@ -39,16 +46,22 @@ export class ElectionService { // TODO: send the error to remote logging infrastructure console.error(error); // log to console instead - // TODO: better job of transforming error for user consumption - this.log(`${operation} failed: ${error.message}`); + if (error.status == 401) { + this.log(MessageType.UNAUTHENTICATED); + } + if (error.status == 403) { + this.log(MessageType.UNAUTHORIZED); + } + if (error.status == 500) { + this.log(MessageType.INTERNAL_SERVER_ERROR); + } // Let the app keep running by returning an empty result. return of(result as T); }; } - /** Log a HeroService message with the MessageService */ - private log(message: string) { - this.messageService.add(`ElectionService: ${message}`); + private log(type: MessageType) { + this.messageService.logMessage('ElectionService', type); } } diff --git a/module-frontend/src/app/elections/elections-routing.module.ts b/module-frontend/src/app/elections/elections-routing.module.ts new file mode 100644 index 0000000..c7444ac --- /dev/null +++ b/module-frontend/src/app/elections/elections-routing.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from "@angular/router"; +import {ElectionsComponent} from "./elections.component"; + +const routes: Routes = [ + { + path: '', + component: ElectionsComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ElectionsRoutingModule { +} diff --git a/module-frontend/src/app/elections/elections.component.ts b/module-frontend/src/app/elections/elections.component.ts index bed3497..c4a4fcf 100644 --- a/module-frontend/src/app/elections/elections.component.ts +++ b/module-frontend/src/app/elections/elections.component.ts @@ -1,8 +1,10 @@ import {Component, OnInit} from '@angular/core'; import {KeycloakService} from "keycloak-angular"; -import {Election} from "./election"; -import {ElectionService} from "./election.service"; +import {Election} from "./model/election"; import {Observable} from "rxjs"; +import {Store} from "@ngrx/store"; +import {allElections} from "./store"; +import {loadAllElectionsAction} from "./store/elections.actions"; @Component({ selector: 'app-elections', @@ -10,12 +12,13 @@ import {Observable} from "rxjs"; styleUrls: ['./elections.component.scss'] }) export class ElectionsComponent implements OnInit{ - $elections: Observable = this.electionService.getElections(); + $elections: Observable = this.store.select(allElections()); constructor(private keycloakService: KeycloakService, - private electionService: ElectionService) { + private store: Store) { } ngOnInit() { + this.store.dispatch(loadAllElectionsAction()); } } diff --git a/module-frontend/src/app/elections/elections.module.ts b/module-frontend/src/app/elections/elections.module.ts new file mode 100644 index 0000000..ec26fc8 --- /dev/null +++ b/module-frontend/src/app/elections/elections.module.ts @@ -0,0 +1,26 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ElectionsComponent} from "./elections.component"; +import {StoreModule} from "@ngrx/store"; +import {ElectionsEffects} from "./store/elections.effects"; +import {EffectsModule} from "@ngrx/effects"; +import {electionsReducers, featureStateName} from "./store"; +import {ElectionsRoutingModule} from "./elections-routing.module"; + + +@NgModule({ + declarations: [ + ElectionsComponent + ], + imports: [ + CommonModule, + ElectionsRoutingModule, + StoreModule.forFeature(featureStateName, electionsReducers), + EffectsModule.forFeature([ElectionsEffects]), + ], + exports: [ + ElectionsComponent + ] +}) +export class ElectionsModule { +} diff --git a/module-frontend/src/app/elections/constituency.ts b/module-frontend/src/app/elections/model/constituency.ts similarity index 100% rename from module-frontend/src/app/elections/constituency.ts rename to module-frontend/src/app/elections/model/constituency.ts diff --git a/module-frontend/src/app/elections/election.ts b/module-frontend/src/app/elections/model/election.ts similarity index 100% rename from module-frontend/src/app/elections/election.ts rename to module-frontend/src/app/elections/model/election.ts diff --git a/module-frontend/src/app/elections/voting-threshold.ts b/module-frontend/src/app/elections/model/voting-threshold.ts similarity index 100% rename from module-frontend/src/app/elections/voting-threshold.ts rename to module-frontend/src/app/elections/model/voting-threshold.ts diff --git a/module-frontend/src/app/elections/store/elections.actions.ts b/module-frontend/src/app/elections/store/elections.actions.ts new file mode 100644 index 0000000..f6c42de --- /dev/null +++ b/module-frontend/src/app/elections/store/elections.actions.ts @@ -0,0 +1,43 @@ +import {createAction, props} from "@ngrx/store"; +import {Election} from "../model/election"; + +export enum ActionTypes { + LoadAllElections = '[Elections] Load All Elections', + LoadAllElectionsFinished = '[Elections] Load All Elections Finished', + + LoadSingleElection = '[Elections] Load Single Election', + LoadSingleElectionFinished = '[Elections] Load Single Election Finished', +} + +export const loadAllElectionsAction = createAction( + ActionTypes.LoadAllElections +) + +export type LoadAllElectionsAction = ReturnType; + +export const loadAllElectionsFinishedAction = createAction( + ActionTypes.LoadAllElectionsFinished, + props<{payload: Election[]}>() +) + +export type LoadAllElectionsFinishedAction = ReturnType; + +export const loadSingleElectionAction = createAction( + ActionTypes.LoadSingleElection, + props<{payload: string}>() +) + +export type LoadSingleElectionAction = ReturnType; + +export const loadSingleElectionFinishedAction = createAction( + ActionTypes.LoadSingleElectionFinished, + props<{payload: Election|undefined}>() +) + +export type LoadSingleElectionFinishedAction = ReturnType; + +export type ElectionsActions = + | LoadAllElectionsAction + | LoadAllElectionsFinishedAction + | LoadSingleElectionAction + | LoadSingleElectionFinishedAction; diff --git a/module-frontend/src/app/elections/store/elections.effects.ts b/module-frontend/src/app/elections/store/elections.effects.ts new file mode 100644 index 0000000..f93303f --- /dev/null +++ b/module-frontend/src/app/elections/store/elections.effects.ts @@ -0,0 +1,38 @@ +import {Injectable} from "@angular/core"; +import {ElectionService} from "../election.service"; +import { + ActionTypes, + loadAllElectionsFinishedAction, + LoadSingleElectionAction, + loadSingleElectionFinishedAction +} from "./elections.actions"; +import {map, switchMap} from "rxjs"; +import {Actions, createEffect, ofType} from "@ngrx/effects"; + +@Injectable() +export class ElectionsEffects { + constructor(private actions$: Actions, + private electionService: ElectionService) { + } + + loadAllElections$ = createEffect(() => this.actions$.pipe( + ofType(ActionTypes.LoadAllElections), + switchMap(() => this.electionService.getElections() + .pipe( + map(elections => loadAllElectionsFinishedAction({payload: elections})) + ) + ) + )); + + loadSingleElection$ = createEffect(() => { + return this.actions$.pipe( + ofType(ActionTypes.LoadSingleElection), + switchMap((action: LoadSingleElectionAction) => this.electionService.selectElection(action.payload) + .pipe( + map(election => + loadSingleElectionFinishedAction({payload: election})) + ) + ), + ) + }); +} diff --git a/module-frontend/src/app/elections/store/elections.reducer.ts b/module-frontend/src/app/elections/store/elections.reducer.ts new file mode 100644 index 0000000..e5aa9db --- /dev/null +++ b/module-frontend/src/app/elections/store/elections.reducer.ts @@ -0,0 +1,30 @@ +import {Election} from "../model/election"; +import { + loadAllElectionsFinishedAction, + loadSingleElectionFinishedAction +} from "./elections.actions"; +import {createReducer, on} from "@ngrx/store"; + +export interface ReducerElectionsState { + items: Election[]; + selectedItem: Election|undefined; +} + +export const initialState: ReducerElectionsState = { + items: [], + selectedItem: undefined +}; + +export const electionsReducer = createReducer( + initialState, + on(loadAllElectionsFinishedAction, (state, + action) => ({ + ...state, + items: [...action.payload] + })), + on(loadSingleElectionFinishedAction, (state, + action) => ({ + ...state, + selectedItem: action.payload + })) +); diff --git a/module-frontend/src/app/elections/store/index.ts b/module-frontend/src/app/elections/store/index.ts new file mode 100644 index 0000000..7a7facc --- /dev/null +++ b/module-frontend/src/app/elections/store/index.ts @@ -0,0 +1,28 @@ +import {electionsReducer, ReducerElectionsState} from "./elections.reducer"; +import {ActionReducerMap, createFeatureSelector, createSelector} from "@ngrx/store"; + +export const featureStateName = 'electionsFeature'; + +export interface ElectionsState { + elections: ReducerElectionsState; +} + +export const electionsReducers: ActionReducerMap = { + elections: electionsReducer, +}; + +// extract the main property 'electionsFeature' from the state object +export const getElectionsFeatureState = createFeatureSelector( + featureStateName +); + +export const allElections = () => createSelector( + getElectionsFeatureState, + (state: ElectionsState) => state.elections.items +); + +export const electionByName = (name: string) => createSelector( + getElectionsFeatureState, + (state: ElectionsState) => + state.elections.items.find(election => election.name === name) +); diff --git a/module-frontend/src/app/messages/message.service.ts b/module-frontend/src/app/messages/message.service.ts deleted file mode 100644 index d4d05b4..0000000 --- a/module-frontend/src/app/messages/message.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Injectable} from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class MessageService { - messages: string[] = []; - - add(message: string) { - this.messages.push(message); - } - - clear() { - this.messages = []; - } -} diff --git a/module-frontend/src/app/messages/messages.component.html b/module-frontend/src/app/messages/messages.component.html deleted file mode 100644 index 1df7dfd..0000000 --- a/module-frontend/src/app/messages/messages.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
- -

Messages

- -
{{message}}
- -
diff --git a/module-frontend/src/app/messages/messages.component.scss b/module-frontend/src/app/messages/messages.component.scss deleted file mode 100644 index d1c36dc..0000000 --- a/module-frontend/src/app/messages/messages.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -h2 { - color: red; - font-family: Arial, Helvetica, sans-serif; - font-weight: lighter; -} -body { - margin: 2em; -} -body, input[text], button { - color: crimson; - font-family: Cambria, Georgia, sans-serif; -} - -button.clear { - font-family: Arial, sans-serif; - background-color: #eee; - border: none; - padding: 5px 10px; - border-radius: 4px; - cursor: pointer; -} -button:hover { - background-color: #cfd8dc; -} -button:disabled { - background-color: #eee; - color: #aaa; - cursor: auto; -} -button.clear { - color: #333; - margin-bottom: 12px; -} diff --git a/module-frontend/src/app/messages/messages.component.spec.ts b/module-frontend/src/app/messages/messages.component.spec.ts deleted file mode 100644 index 9df302d..0000000 --- a/module-frontend/src/app/messages/messages.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {ComponentFixture, TestBed} from '@angular/core/testing'; - -import {MessagesComponent} from './messages.component'; - -describe('MessagesComponent', () => { - let component: MessagesComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [MessagesComponent] - }); - fixture = TestBed.createComponent(MessagesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/module-frontend/src/app/messages/messages.component.ts b/module-frontend/src/app/messages/messages.component.ts deleted file mode 100644 index f844f1f..0000000 --- a/module-frontend/src/app/messages/messages.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {Component} from '@angular/core'; -import {MessageService} from "./message.service"; - -@Component({ - selector: 'app-messages', - templateUrl: './messages.component.html', - styleUrls: ['./messages.component.scss'] -}) -export class MessagesComponent { - constructor(public messageService: MessageService) {} -} diff --git a/module-frontend/src/app/messages/messages.module.ts b/module-frontend/src/app/messages/messages.module.ts new file mode 100644 index 0000000..13d9472 --- /dev/null +++ b/module-frontend/src/app/messages/messages.module.ts @@ -0,0 +1,20 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {StoreModule} from "@ngrx/store"; +import {featureStateName, messagesReducers} from "./store"; +import {EffectsModule} from "@ngrx/effects"; +import {MessagesEffects} from "./store/messages.effects"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; + + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + MatSnackBarModule, + StoreModule.forFeature(featureStateName, messagesReducers), + EffectsModule.forFeature([MessagesEffects]) + ] +}) +export class MessagesModule { +} diff --git a/module-frontend/src/app/messages/message.service.spec.ts b/module-frontend/src/app/messages/messages.service.spec.ts similarity index 53% rename from module-frontend/src/app/messages/message.service.spec.ts rename to module-frontend/src/app/messages/messages.service.spec.ts index ce1f5d1..bf46410 100644 --- a/module-frontend/src/app/messages/message.service.spec.ts +++ b/module-frontend/src/app/messages/messages.service.spec.ts @@ -1,13 +1,13 @@ import {TestBed} from '@angular/core/testing'; -import {MessageService} from './message.service'; +import {MessagesService} from './messages.service'; -describe('MessageService', () => { - let service: MessageService; +describe('MessagesService', () => { + let service: MessagesService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(MessageService); + service = TestBed.inject(MessagesService); }); it('should be created', () => { diff --git a/module-frontend/src/app/messages/messages.service.ts b/module-frontend/src/app/messages/messages.service.ts new file mode 100644 index 0000000..8ad636f --- /dev/null +++ b/module-frontend/src/app/messages/messages.service.ts @@ -0,0 +1,34 @@ +import {Injectable} from '@angular/core'; +import {Store} from "@ngrx/store"; +import {addMessageAction} from "./store/messages.actions"; +import {MessageType} from "./model/message-type"; + +@Injectable({ + providedIn: 'root' +}) +export class MessagesService { + private static UNAUTHENTICATED = $localize `You are not logged in which prevents data from loading`; + private static UNAUTHORIZED = $localize `You don't have sufficient authorization to view the data`; + private static INTERNAL_SERVER_ERROR = $localize `An internal server error occurred. Sorry for the inconvenience.`; + private static UNKNOWN_ERROR = $localize `An unknown error occurred. Sorry for the inconvenience.`; + + constructor(private store: Store) { } + + logMessage(component: string, type: MessageType) { + let text = component + ": "; + switch (type) { + case MessageType.UNAUTHENTICATED: + text += MessagesService.UNAUTHENTICATED; + break; + case MessageType.UNAUTHORIZED: + text += MessagesService.UNAUTHORIZED; + break; + case MessageType.INTERNAL_SERVER_ERROR: + text += MessagesService.INTERNAL_SERVER_ERROR; + break; + default: + text += MessagesService.UNKNOWN_ERROR; + } + this.store.dispatch(addMessageAction({message: {text, type}})) + } +} diff --git a/module-frontend/src/app/messages/model/message-type.ts b/module-frontend/src/app/messages/model/message-type.ts new file mode 100644 index 0000000..5dd44d6 --- /dev/null +++ b/module-frontend/src/app/messages/model/message-type.ts @@ -0,0 +1,5 @@ +export enum MessageType { + UNAUTHENTICATED, + UNAUTHORIZED, + INTERNAL_SERVER_ERROR +} diff --git a/module-frontend/src/app/messages/model/message.ts b/module-frontend/src/app/messages/model/message.ts new file mode 100644 index 0000000..443155c --- /dev/null +++ b/module-frontend/src/app/messages/model/message.ts @@ -0,0 +1,6 @@ +import {MessageType} from "./message-type"; + +export interface Message { + text: string; + type: MessageType; +} diff --git a/module-frontend/src/app/messages/store/index.ts b/module-frontend/src/app/messages/store/index.ts new file mode 100644 index 0000000..528dec6 --- /dev/null +++ b/module-frontend/src/app/messages/store/index.ts @@ -0,0 +1,16 @@ +import {messagesReducer, ReducerMessagesState} from "./messages.reducer"; +import {ActionReducerMap, createFeatureSelector} from "@ngrx/store"; + +export const featureStateName = 'messagesFeature'; + +export interface MessagesState { + messages: ReducerMessagesState; +} + +export const messagesReducers: ActionReducerMap = { + messages: messagesReducer, +}; + +export const getMessagesFeatureState = createFeatureSelector( + featureStateName +); diff --git a/module-frontend/src/app/messages/store/messages.actions.ts b/module-frontend/src/app/messages/store/messages.actions.ts new file mode 100644 index 0000000..807d824 --- /dev/null +++ b/module-frontend/src/app/messages/store/messages.actions.ts @@ -0,0 +1,24 @@ +import {createAction, props} from "@ngrx/store"; +import {Message} from "../model/message"; + +export enum ActionTypes { + AddMessage = '[Messages] Add Message', + AddMessageFinished = '[Messages] Add Message Finished', +} + +export const addMessageAction = createAction( + ActionTypes.AddMessage, + props<{message: Message}>() +); + +export type AddMessageAction = ReturnType; + +export const addMessageFinishedAction = createAction( + ActionTypes.AddMessageFinished +); + +export type AddMessageFinishedAction = ReturnType; + +export type MessagesActions = + | AddMessageAction + | AddMessageFinishedAction; diff --git a/module-frontend/src/app/messages/store/messages.effects.ts b/module-frontend/src/app/messages/store/messages.effects.ts new file mode 100644 index 0000000..ac71b79 --- /dev/null +++ b/module-frontend/src/app/messages/store/messages.effects.ts @@ -0,0 +1,21 @@ +import {MatSnackBar} from "@angular/material/snack-bar"; +import {Actions, createEffect, ofType} from "@ngrx/effects"; +import {ActionTypes, AddMessageAction, addMessageFinishedAction} from "./messages.actions"; +import {map} from "rxjs"; +import {Injectable} from "@angular/core"; + +@Injectable() +export class MessagesEffects { + constructor(private actions$: Actions, + private snackBar: MatSnackBar) { + } + + showSnackbar$ = createEffect(() => + this.actions$.pipe( + ofType(ActionTypes.AddMessage), + map((action: AddMessageAction) => { + this.snackBar.open(action.message.text, "OK"); + return addMessageFinishedAction(); + }) + )); +} diff --git a/module-frontend/src/app/messages/store/messages.reducer.ts b/module-frontend/src/app/messages/store/messages.reducer.ts new file mode 100644 index 0000000..9ef2a1e --- /dev/null +++ b/module-frontend/src/app/messages/store/messages.reducer.ts @@ -0,0 +1,24 @@ +import {Message} from "../model/message"; +import {createReducer, on} from "@ngrx/store"; +import {addMessageAction, addMessageFinishedAction} from "./messages.actions"; + +export interface ReducerMessagesState { + displayedMessage: Message|undefined; +} + +const initialState: ReducerMessagesState = { + displayedMessage: undefined +} + +export const messagesReducer = createReducer( + initialState, + on(addMessageAction, (state, + action) => ({ + ...state, + displayedMessage: action.message + })), + on(addMessageFinishedAction, (state) => ({ + ...state, + displayedMessage: undefined + })) +); diff --git a/module-frontend/src/app/navigation/navigation.component.html b/module-frontend/src/app/navigation/navigation.component.html index d6fc591..5716b54 100644 --- a/module-frontend/src/app/navigation/navigation.component.html +++ b/module-frontend/src/app/navigation/navigation.component.html @@ -20,8 +20,13 @@ Wahlrecht - Logged in as {{loggedUserName}} - + + Logged in as {{loggedUserName}} + + + + + diff --git a/module-frontend/src/app/navigation/navigation.component.ts b/module-frontend/src/app/navigation/navigation.component.ts index 63c82a5..4e4f5b1 100644 --- a/module-frontend/src/app/navigation/navigation.component.ts +++ b/module-frontend/src/app/navigation/navigation.component.ts @@ -1,5 +1,7 @@ import {Component} from '@angular/core'; import {KeycloakService} from "keycloak-angular"; +import {from} from "rxjs"; +import {ActivatedRoute} from "@angular/router"; @Component({ selector: 'app-navigation', @@ -8,13 +10,24 @@ import {KeycloakService} from "keycloak-angular"; }) export class NavigationComponent { loggedUserName = ''; + isLoggedIn$ = from(this.keycloakService.isLoggedIn()); + url: string; - constructor(private keycloakService: KeycloakService) { } + constructor(private keycloakService: KeycloakService, + private route: ActivatedRoute) { + this.url = route.snapshot.url.join(''); + } ngOnInit(): void { this.loggedUserName = this.keycloakService.getUsername(); } + login(): void { + this.keycloakService.login({ + redirectUri: window.location.origin + this.url, + }); + } + logout(): void { this.keycloakService.logout(window.location.origin); } diff --git a/module-frontend/src/app/store/app.reducer.ts b/module-frontend/src/app/store/app.reducer.ts new file mode 100644 index 0000000..78e301c --- /dev/null +++ b/module-frontend/src/app/store/app.reducer.ts @@ -0,0 +1,7 @@ +import {ElectionsState} from "../elections/store"; +import {MessagesState} from "../messages/store"; + +export interface AppState { + electionsFeature: ElectionsState; + messagesFeature: MessagesState; +} diff --git a/module-frontend/src/app/store/index.ts b/module-frontend/src/app/store/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/module-frontend/src/main.ts b/module-frontend/src/main.ts index e59cec9..c20ec05 100644 --- a/module-frontend/src/main.ts +++ b/module-frontend/src/main.ts @@ -1,3 +1,5 @@ +/// + import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app/app.module'; diff --git a/module-frontend/tsconfig.app.json b/module-frontend/tsconfig.app.json index 374cc9d..ec26f70 100644 --- a/module-frontend/tsconfig.app.json +++ b/module-frontend/tsconfig.app.json @@ -3,7 +3,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": [ + "@angular/localize" + ] }, "files": [ "src/main.ts" diff --git a/module-frontend/tsconfig.spec.json b/module-frontend/tsconfig.spec.json index be7e9da..c63b698 100644 --- a/module-frontend/tsconfig.spec.json +++ b/module-frontend/tsconfig.spec.json @@ -4,7 +4,8 @@ "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ - "jasmine" + "jasmine", + "@angular/localize" ] }, "include": [