Skip to content

Commit

Permalink
Merge pull request #354 from e-picsa/feat/forecasts-server
Browse files Browse the repository at this point in the history
Feat: forecasts server
  • Loading branch information
chrismclarke authored Jan 28, 2025
2 parents 062102f + 8147ac0 commit 22b2abc
Show file tree
Hide file tree
Showing 37 changed files with 688 additions and 421 deletions.
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint-staged
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type { SupabaseService } from '@picsa/shared/services/core/supabase';
import { SupabaseStorageService } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';

import { IDeploymentRow } from '../deployment/types';
import { ClimateService } from './climate.service';
import type { ClimateApiService } from './climate-api.service';
import {
IAPICountryCode,
IClimateSummaryRainfallInsert,
IClimateSummaryRainfallRow,
IForecastInsert,
IForecastRow,
IForecastUpdate,
IStationInsert,
IStationRow,
} from './types';
Expand All @@ -28,9 +27,8 @@ export type IApiMappingName = keyof IApiMapping;
export const ApiMapping = (
api: ClimateApiService,
service: ClimateService,
db: SupabaseService['db'],
storage: SupabaseStorageService,
deployment: IDeploymentRow
supabaseService: SupabaseService,
storage: SupabaseStorageService
) => {
return {
rainfallSummaries: async (station: IStationRow) => {
Expand Down Expand Up @@ -61,7 +59,7 @@ export const ApiMapping = (
station_id: id as string,
country_code: country_code as any,
};
const { data: dbData, error: dbError } = await db
const { data: dbData, error: dbError } = await supabaseService.db
.table('climate_summary_rainfall')
.upsert<IClimateSummaryRainfallInsert>(entry)
.select<'*', IClimateSummaryRainfallRow>('*');
Expand All @@ -83,7 +81,7 @@ export const ApiMapping = (
station_id: `${d.station_id.toLowerCase().replace(/[^a-z]/gi, '_')}`,
})
);
const { error: dbError, data: dbData } = await db
const { error: dbError, data: dbData } = await supabaseService.db
.table('climate_stations')
.upsert<IStationInsert>(update)
.select();
Expand All @@ -92,51 +90,39 @@ export const ApiMapping = (
service.stations.set(dbData);
}
},
//
forecasts: async (country_code: IAPICountryCode) => {
// const { data, error } = await api
// .getObservableClient(`forecasts/${country_code}`)
// .GET(`/v1/forecasts/{country_code}`, { params: { path: { country_code } } });
// if (error) throw error;
// const forecasts = data.map((d): IForecastInsert => {
// const { date, filename, format, type } = d;
// // TODO - handle format
// return { date_modified: date, filename, country_code, type, id: filename.split('/').pop() as string };
// });
// const { error: dbError, data: dbData } = await db
// .table('climate_forecasts')
// .upsert<IForecastInsert>(forecasts)
// .select<'*', IForecastRow>('*');
// if (dbError) throw dbError;
// return dbData || [];
return [];
},
/**
*
* @param row
* @returns
*/
forecast_file: async (row: IForecastRow) => {
// const { country_code, filename } = row;
// const { data, error } = await api.getObservableClient(`forecasts/${filename}`).GET(`/v1/forecasts/{file_name}`, {
// params: { path: { file_name: filename } },
// parseAs: 'blob',
// });
// if (error) throw error;
// // setup metadata
// const fileBlob = data as Blob;
// const bucketId = country_code as string;
// const folderPath = 'climate/forecasts';
// // upload to storage
// await storage.putFile({ bucketId, fileBlob, filename, folderPath });
// // TODO - handle error if filename already exists
// const storageEntry = await storage.getFileAlt({ bucketId, filename, folderPath });
// if (storageEntry) {
// const { error: dbError } = await db
// .table('climate_forecasts')
// .upsert<IForecastInsert>({ ...row, storage_file: storageEntry.id })
// .select('*');
// if (dbError) {
// throw dbError;
// }
// return;
// }
// throw new Error('Storage file not found');
const { country_code, id } = row;
// api does not use per-country buckets, so recreate folder structure from id
const filepath = id.replace(`${country_code}/`, '');
const { data, error } = await api
.getObservableClient(`forecasts/${id}`)
.GET(`/v1/documents/{country}/{filepath}`, {
params: { path: { country: country_code as any, filepath } },
parseAs: 'blob',
});

if (error) throw error;
// setup metadata
const fileBlob = data as any as Blob;
const bucketId = country_code as string;
const folderPath = 'climate/forecasts';
// upload to storage
const { fullPath } = await storage.putFile({ bucketId, fileBlob, filename: filepath, folderPath });

// TODO - handle error if filename already exists
const { error: dbError } = await supabaseService.db
.table('climate_forecasts')
.update<IForecastUpdate>({ storage_file: fullPath })
.eq('id', row.id);
if (dbError) {
throw dbError;
}
return fullPath;
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BehaviorSubject, Subject } from 'rxjs';
import { paths } from './types/api';

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

/** Type-safe http client with added support for callbacks */
export type IApiClient = ReturnType<typeof createClient<paths>> & { $:Subject<Response | undefined>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,7 @@ export class ClimateService extends PicsaAsyncService {
public apiCountryCode: IAPICountryCode;

/** Trigger API request that includes mapping response to local database */
public loadFromAPI = ApiMapping(
this.api,
this,
this.supabaseService.db,
this.supabaseService.storage,
this.deploymentSevice.activeDeployment() as IDeploymentRow
);
public loadFromAPI = ApiMapping(this.api, this, this.supabaseService, this.supabaseService.storage);

// Create a signal to represent current stationId as defined by route params
private activeStationId = toSignal(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
} @if(options().labels?.ready; as readyLabel){
<div class="status-label">{{readyLabel}}</div>
} @if(options().events?.refresh; as refreshEvent){
<button class="status-refresh-button" mat-icon-button [disabled]="status() === 'pending'" (click)="refreshEvent()">
<mat-icon iconPositionEnd [class.spin]="status() === 'pending'">autorenew</mat-icon>
</button>
<picsa-refresh-spinner [spin]="status() === 'pending'" [disabled]="status()==='pending'" (click)="refreshEvent()" />
}
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, effect, input, OnDestroy, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { RefreshSpinnerComponent } from '@picsa/components';
import { Subject, Subscription, takeUntil } from 'rxjs';

import { ClimateService } from '../../climate.service';
Expand Down Expand Up @@ -30,7 +31,7 @@ const DEFAULT_OPTIONS: IApiStatusOptions = {
*/
@Component({
selector: 'dashboard-climate-api-status',
imports: [MatButtonModule, MatIconModule],
imports: [MatButtonModule, MatIconModule, RefreshSpinnerComponent],
templateUrl: './api-status.html',
styleUrls: ['./api-status.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Forecasts</h2>
<dashboard-climate-api-status [options]="apiStatusOptions" clientId="forecasts/mw"></dashboard-climate-api-status>
<h2 class="mr-2">Forecasts</h2>
<dashboard-climate-forecast-month-select (dateChanged)="apiStartDate.set($event)" />
<picsa-refresh-spinner [spin]="refreshPending()" [disabled]="refreshPending()" (click)="handleRefreshClick()" class="ml-auto"></picsa-refresh-spinner>
</div>
<picsa-data-table [data]="forecastData" [options]="tableOptions" [valueTemplates]="{ date_modified,storage_file }">
<ng-template #date_modified let-value> {{ value | date }} </ng-template>
<picsa-data-table [data]="forecastData()" [options]="tableOptions" [valueTemplates]="{ updated_at,storage_file }">
<ng-template #updated_at let-value> {{ value | date }} </ng-template>
<ng-template #storage_file let-value let-row="row">
<button
mat-icon-button
(click)="handleStorageClick(row)"
[disabled]="activeDownloads[row.filename] === 'pending'"
>
@if(value || activeDownloads[row.filename]==='complete'){
@let status = activeDownloads()[row.id];
<button mat-icon-button [disabled]="status === 'pending'">
@if(value || status==='complete'){
<mat-icon>open_in_new</mat-icon>
} @else if(activeDownloads[row.filename]==='pending'){
} @else if(status==='pending'){
<mat-icon class="spin">autorenew</mat-icon>
} @else {
<mat-icon>download</mat-icon>
Expand Down
Loading

0 comments on commit 22b2abc

Please sign in to comment.