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 station forecasts #233

Merged
merged 34 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
bfc2ebf
feat: add dashboard nav accordion
chrismclarke Feb 9, 2024
9743c95
chore: code tidying
chrismclarke Feb 9, 2024
ece72c1
style: dashboard climate api status spinner
chrismclarke Feb 9, 2024
83b4818
feat: integrate climate station list api
chrismclarke Feb 9, 2024
0a740fc
chore: station-data rename
chrismclarke Feb 9, 2024
a2d4cb9
chore: service rename
chrismclarke Feb 9, 2024
fdc7ee9
chore: code tidying
chrismclarke Feb 9, 2024
b355f51
chore: rename station-details page
chrismclarke Feb 9, 2024
c4a7187
refactor: station details page
chrismclarke Feb 9, 2024
e229641
chore: rename station home
chrismclarke Feb 9, 2024
48913ff
chore: code tidying
chrismclarke Feb 9, 2024
3f23914
feat: forecast page placeholder
chrismclarke Feb 9, 2024
ce7d69a
feat: data table custom value templates and header formatter
chrismclarke Feb 9, 2024
dc3ab35
feat: add climate forecasts db endpoint
chrismclarke Feb 9, 2024
d58c96d
chore: update type definitions
chrismclarke Feb 9, 2024
162b219
chore: code tidying
chrismclarke Feb 9, 2024
57bd1bd
chore: update type definitions
chrismclarke Feb 9, 2024
b2d1907
feat: api status component
chrismclarke Feb 14, 2024
a88cab6
refactor: climate api service client
chrismclarke Feb 14, 2024
074c1fe
style: api status component
chrismclarke Feb 14, 2024
7db2fc7
chore: code tidying
chrismclarke Feb 14, 2024
4765248
chore: code tidying
chrismclarke Feb 14, 2024
5346926
feat: wip forecast component
chrismclarke Feb 14, 2024
93c114e
chore: code tidying
chrismclarke Feb 14, 2024
e1da45b
feat: wip climate api mapping system
chrismclarke Feb 14, 2024
2663ab3
feat: wip rainfall summary
chrismclarke Feb 14, 2024
a091ad6
chore: code tidying
chrismclarke Feb 14, 2024
59f1bf5
Merge branch 'main' into feat/dashboard-station-forecasts
chrismclarke Feb 15, 2024
c4b4899
feat: forecast storage file mapping
chrismclarke Feb 15, 2024
7aeb112
feat: add country storage buckets
chrismclarke Feb 15, 2024
d150bcc
refactor: shared icon spin animation
chrismclarke Feb 15, 2024
015d54f
feat: storage putFile and blob response handling
chrismclarke Feb 15, 2024
80fa1a0
feat: data-table context row and column
chrismclarke Feb 15, 2024
06acb59
feat: forecast dashboard download and open
chrismclarke Feb 15, 2024
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
29 changes: 27 additions & 2 deletions apps/picsa-apps/dashboard/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,36 @@
<mat-sidenav-container style="flex: 1">
<mat-sidenav #sidenav mode="side" opened [fixedInViewport]="true" fixedTopGap="64">
<mat-nav-list>
@for (link of navLinks; track link.href) {
@for (link of navLinks; track link.href) { @if(link.children){
<!-- Nested nav items -->
<mat-expansion-panel class="mat-elevation-z0" [expanded]="rla.isActive">
<mat-expansion-panel-header
style="padding: 0 16px"
[routerLink]="link.href"
routerLinkActive
#rla="routerLinkActive"
[routerLinkActiveOptions]="{ exact: false }"
>
<mat-panel-title>
{{ link.label }}
</mat-panel-title>
</mat-expansion-panel-header>
@for(child of link.children || []; track $index){
<a
mat-list-item
[routerLink]="link.href + child.href"
routerLinkActive="mdc-list-item--activated active-link"
>
{{ child.label }}</a
>
}
</mat-expansion-panel>
} @else {
<!-- Single nav item -->
<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>
Expand Down
20 changes: 13 additions & 7 deletions apps/picsa-apps/dashboard/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DashboardMaterialModule } from './material.module';
interface INavLink {
label: string;
href: string;
isActive?: boolean;
children?: INavLink[];
}

@Component({
Expand All @@ -22,17 +22,23 @@ export class AppComponent implements AfterViewInit {
title = 'picsa-apps-dashboard';

navLinks: INavLink[] = [
// {
// label: 'Home',
// href: '',
// },
{
label: 'Resources',
href: '/resources',
},
{
label: 'Climate Data',
href: '/climate-data',
label: 'Climate',
href: '/climate',
children: [
{
label: 'Station Data',
href: '/station',
},
{
label: 'Forecasts',
href: '/forecast',
},
],
},
// {
// label: 'Crop Information',
Expand Down
4 changes: 2 additions & 2 deletions apps/picsa-apps/dashboard/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export const appRoutes: Route[] = [
loadChildren: () => import('./modules/resources/resources.module').then((m) => m.ResourcesPageModule),
},
{
path: 'climate-data',
loadChildren: () => import('./modules/climate-data/climate-data.module').then((m) => m.ClimateDataModule),
path: 'climate',
loadChildren: () => import('./modules/climate/climate.module').then((m) => m.ClimateModule),
},
{
path: 'translations',
Expand Down
4 changes: 3 additions & 1 deletion apps/picsa-apps/dashboard/src/app/material.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
Expand All @@ -15,6 +16,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
const matModules = [
MatButtonModule,
MatChipsModule,
MatExpansionModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
Expand All @@ -24,7 +26,7 @@ const matModules = [
MatStepperModule,
MatTableModule,
MatTabsModule,
MatToolbarModule
MatToolbarModule,
];

@NgModule({
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

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

import type { ClimateApiService } from './climate-api.service';
import { IClimateProductInsert, IClimateProductRow, IForecastInsert, IForecastRow, IStationRow } from './types';

export type IApiMapping = ReturnType<typeof ApiMapping>;
export type IApiMappingName = keyof IApiMapping;

// TODO - certain amount of boilerplate could be reduced
// TODO - depends on climate api updates
// TODO - most of these should be run on server as server functions

/**
* Mapping functions that handle processing of data loaded from API server endpoints,
* and populating entries to supabase DB
*/
export const ApiMapping = (api: ClimateApiService, db: SupabaseService['db'], storage: SupabaseStorageService) => {
return {
rainfallSummaries: async (country_code: string, station_id: number) => {
// TODO - add model type definitions for server rainfall summary response body
const { data, error } = await api
.getObservableClient(`rainfallSummary_${country_code}_${station_id}`)
.POST('/v1/annual_rainfall_summaries/', {
body: {
country: `${country_code}` as any,
station_id: `${station_id}`,
summaries: ['annual_rain', 'start_rains', 'end_rains', 'end_season', 'seasonal_rain', 'seasonal_length'],
},
});
if (error) throw error;
console.log('summary data', data);
// TODO - gen types and handle mapping
const mappedData = data as any;
const { data: dbData, error: dbError } = await db
.table('climate_products')
.upsert<IClimateProductInsert>({
data: mappedData,
station_id,
type: 'rainfallSummary',
})
.select<'*', IClimateProductRow>('*');
if (dbError) throw dbError;
return dbData || [];
},
//
station: async () => {
const { data, error } = await api.getObservableClient('station').GET('/v1/station/');
if (error) throw error;
// TODO - fix climate api bindigns to avoid data.data
console.log('station data', data);
const dbData = data.map(
(d): IStationRow => ({
...d,
})
);
const { error: dbError } = await db.table('climate_stations').upsert<IStationRow>(dbData);
if (dbError) throw dbError;
return dbData;
},
//
forecasts: async (country_code: 'zm' | 'mw') => {
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 => ({ ...d, country_code }));
const { error: dbError, data: dbData } = await db
.table('climate_forecasts')
.upsert<IForecastInsert>(forecasts)
.select<'*', IForecastRow>('*');
if (dbError) throw dbError;
return dbData || [];
},
forecast_file: async (row: IForecastRow) => {
const { country_code, filename } = row;
const { data, error } = await api
.getObservableClient(`forecasts/${filename}`)
.GET(`/v1/forecasts/{country_code}/{file_name}`, {
params: { path: { country_code: country_code as any, 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.getFile({ 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');
},
};
};
Loading
Loading