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

Feat(dashboard); climate summaries #214

Merged
merged 16 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 2 additions & 12 deletions apps/picsa-apps/dashboard/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,15 @@
<mat-sidenav #sidenav mode="side" opened [fixedInViewport]="true" fixedTopGap="64">
<mat-nav-list>
@for (link of navLinks; track link.href) {
<a
mat-list-item
[routerLink]="link.href"
routerLinkActive="mdc-list-item--activated active-link"
[routerLinkActiveOptions]="{ exact: true }"
>
<a mat-list-item [routerLink]="link.href" routerLinkActive="mdc-list-item--activated active-link">
{{ link.label }}
</a>
}
<mat-divider style="margin-top: auto"></mat-divider>
<div mat-subheader>Global Admin</div>
<mat-divider></mat-divider>
@for (link of globalLinks; track link.href) {
<a
mat-list-item
[routerLink]="link.href"
routerLinkActive="mdc-list-item--activated active-link"
[routerLinkActiveOptions]="{ exact: true }"
>
<a mat-list-item [routerLink]="link.href" routerLinkActive="mdc-list-item--activated active-link">
{{ link.label }}
</a>
}
Expand Down
8 changes: 4 additions & 4 deletions apps/picsa-apps/dashboard/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export class AppComponent implements AfterViewInit {
title = 'picsa-apps-dashboard';

navLinks: INavLink[] = [
{
label: 'Home',
href: '/',
},
// {
// label: 'Home',
// href: '',
// },
{
label: 'Resources',
href: '/resources',
Expand Down
5 changes: 5 additions & 0 deletions apps/picsa-apps/dashboard/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ export const appRoutes: Route[] = [
path: 'translations',
loadChildren: () => import('./modules/translations/translations.module').then((m) => m.TranslationsPageModule),
},
{
path: '',
redirectTo: 'resources',
pathMatch: 'full',
},
];
Original file line number Diff line number Diff line change
@@ -1,18 +1,99 @@
import { Injectable } from '@angular/core';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';
import createClient from 'openapi-fetch';

import { paths } from './types/api';

const API_ENDPOINT = 'https://api.epicsa.idems.international';

/** Service to interact with external PICSA Climate API */
/** Custom client which tracks responses by callback id */
type ICallbackClient = (id:string)=>ReturnType<typeof createClient<paths>>

/** Type-safe http client with added support for callbacks */
type IClient = ReturnType<typeof createClient<paths>> & {useMeta:ICallbackClient}



interface IMetaEntry{
status:'pending' | 'success' | 'error' | 'unknown',
rawResponse?:Response,
}


/**
* Service to interact with external PICSA Climate API
* All methods are exposed through a type-safe `client` property, or can additionally use
* a custom client that includes status notification updates via the `useMeta` method
* @example
* Use custom callback that will show user notifications on error and record to service
* ```ts
* const {response, data, error} = await api.useMeta('myRequestId').POST(...)
* ```
* Use default client without additional callbacks
* ```ts
* const {response, data, error} = await api.client.POST(...)
* ```
* */
@Injectable({ providedIn: 'root' })
export class ClimateDataApiService {

/** Request additional meta by id */
public meta:Record<string ,IMetaEntry>={}

/** Http client with type-definitions for API endpoints */
public client:ReturnType<typeof createClient<paths>>

constructor() {
this.client = createClient<paths>({ baseUrl: API_ENDPOINT,mode:'cors' });
public client:IClient

constructor(private notificationService:PicsaNotificationService) {
const client = createClient<paths>({ baseUrl: API_ENDPOINT,mode:'cors' });
this.client = {...client,useMeta:()=>{
return client
}}
}


/**
* Provide an id which which will be updated alongside requests.
* The cache will also include interceptors to provide user notification on error
**/
public useMeta(id:string){
const customFetch = this.createCustomFetchClient(id)
const customClient = createClient<paths>({ baseUrl: API_ENDPOINT,mode:'cors',fetch:customFetch });
return customClient
}

/** Create a custom implementation of fetch client to handle status updates and notifications */
private createCustomFetchClient(id:string){
return async (...args:Parameters<typeof window['fetch']>)=>{
this.meta[id]={status:'pending'}
const response = await window.fetch(...args);
this.meta[id].status = this.getCallbackStatus(response.status)
this.meta[id].rawResponse = response
if(this.meta[id].status ==='error' ){
await this.showCustomFetchErrorMessage(id,response)
}
return response
}
}

/** Show error message when using custom fetch with callbacks */
private async showCustomFetchErrorMessage(id:string,response:Response){
// clone body so that open-api can still consume when constructing full fetch response
const clone = response.clone()
try {
const json = await clone.json()
const errorText = json.detail || 'failed, see console logs for details'
this.notificationService.showUserNotification({matIcon:'error',message:`[${id}] ${errorText}`})
} catch (error) {
console.error(error)
console.error('Fetch Error',error)
this.notificationService.showUserNotification({matIcon:'error',message:`[${id}] 'failed, see console logs for details'`})
}
}

private getCallbackStatus(statusCode:number):IMetaEntry['status']{
if(200 <= statusCode && statusCode <=299) return 'success'
if(400 <= statusCode && statusCode <=499) return 'error'
if(500 <= statusCode && statusCode <=599) return 'error'
return 'unknown'
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Database } from '@picsa/server-types';
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';
import { SupabaseService } from '@picsa/shared/services/core/supabase';
import { IStorageEntry } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';
import { ngRouterMergedSnapshot$ } from '@picsa/utils';

import { ClimateDataApiService } from './climate-data-api.service';

Expand All @@ -20,21 +23,46 @@ export type IResourceEntry = Database['public']['Tables']['resources']['Row'];
export class ClimateDataDashboardService extends PicsaAsyncService {
public apiStatus: number;
public stations: IStationRow[] = [];
public activeStation: IStationRow;

constructor(private supabaseService: SupabaseService, private api: ClimateDataApiService) {
constructor(
private supabaseService: SupabaseService,
private api: ClimateDataApiService,
private notificationService: PicsaNotificationService,
private router: Router
) {
super();
this.ready();
}

public override async init() {
await this.supabaseService.ready();
await this.checkStatus();
await this.listStations();
this.subscribeToRouteChanges();
}

private setActiveStation(id: number) {
const station = this.stations.find((station) => station.station_id === id);
if (station) {
this.activeStation = station;
} else {
this.activeStation = undefined as any;
this.notificationService.showUserNotification({ matIcon: 'error', message: `Station data not found` });
}
}

private subscribeToRouteChanges() {
// Use merged router as service cannot access route params directly like component
ngRouterMergedSnapshot$(this.router).subscribe(({ params }) => {
if (params.stationId) {
this.setActiveStation(parseInt(params.stationId));
}
});
}

private async checkStatus() {
const { client } = this.api;
const { response } = await client.GET('/v1/status/');
this.apiStatus = response.status;
await this.api.useMeta('serverStatus').GET('/v1/status/');
}

private async listStations() {
Expand All @@ -44,6 +72,12 @@ export class ClimateDataDashboardService extends PicsaAsyncService {
if (error) {
throw error;
}
if (data.length === 0) {
this.notificationService.showUserNotification({
matIcon: 'warning',
message: 'climate_stations_rows must be imported into database for this feature to work',
});
}
this.stations = data || [];
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Climate Data</h2>
@if(service.apiStatus; as status){
@if(api.meta.serverStatus; as meta){
<div class="server-status">
Server Status <span class="status-code" [attr.data-status]="status">{{ status }}</span>
Server Status
<span class="status-code" [attr.data-status]="meta.rawResponse?.status">{{ meta.rawResponse?.status }}</span>
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router';
import { IMapMarker, PicsaMapComponent } from '@picsa/shared/features/map/map';

import { ClimateDataDashboardService, IStationRow } from '../../climate-data.service';
import { ClimateDataApiService } from '../../climate-data-api.service';

@Component({
selector: 'dashboard-climate-data-home',
Expand All @@ -18,7 +19,7 @@ export class ClimateDataHomeComponent implements OnInit {

public mapMarkers: IMapMarker[];

constructor(public service: ClimateDataDashboardService) {}
constructor(public service: ClimateDataDashboardService, public api: ClimateDataApiService) {}

async ngOnInit() {
await this.service.ready();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div style="display: flex; align-items: center">
<h3 style="flex: 1">Rainfall Summary</h3>
<button mat-stroked-button (click)="refreshData()" [disabled]="res.status==='pending'">
<mat-icon [class.spin]="res.status==='pending'">autorenew</mat-icon>
Refresh Data
</button>
</div>
<mat-tab-group preserveContent>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>view_list</mat-icon>
Table
</ng-template>
<picsa-data-table [data]="summary.data" [options]="tableOptions"></picsa-data-table>
</mat-tab>
<!-- <mat-tab>
<ng-template mat-tab-label>
<mat-icon>show_chart</mat-icon>
Chart
</ng-template>
</mat-tab> -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>description</mat-icon>
Definition
</ng-template>
<pre>{{summary.metadata | json}}</pre>
</mat-tab>
</mat-tab-group>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:host {
display: block;
}

mat-icon.spin {
animation: spin 2s linear infinite;
}

@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Loading
Loading