-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #333 from e-picsa/feat/dashboard-climate-admin
feat: dashboard climate admin page
- Loading branch information
Showing
20 changed files
with
508 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<div class="page-content"> | ||
<h2>Admin</h2> | ||
<div style="display: flex; align-items: center; gap: 2em"> | ||
<p style="flex: 1">See the table below for rainfall summary data for all stations</p> | ||
<!-- Data Refresh --> | ||
<button mat-stroked-button color="primary" (click)="refreshAllStations()" [disabled]="refreshCount() > -1"> | ||
<mat-icon>refresh</mat-icon> | ||
<span> | ||
@if(refreshCount() > -1){ | ||
{{ refreshCount() }} / {{ tableData().length }} } @else { Refresh All} | ||
</span> | ||
</button> | ||
<!-- Data Download --> | ||
<button mat-stroked-button color="primary" (click)="downloadAllStationsCSV()"> | ||
<mat-icon>download</mat-icon>Download All | ||
</button> | ||
</div> | ||
<picsa-data-table | ||
style="margin-top: 1em" | ||
[data]="tableData()" | ||
[options]="tableOptions" | ||
[valueTemplates]="{ csv: csvTemplate, updated_at: updatedAtTemplate }" | ||
> | ||
<ng-template #csvTemplate let-row="row" let-value="value"> | ||
<button mat-button (click)="downloadStationCSV(row)" [disabled]="!value"><mat-icon>download</mat-icon></button> | ||
</ng-template> | ||
<ng-template #updatedAtTemplate let-value> | ||
{{ value | date: 'mediumDate' }} | ||
</ng-template> | ||
</picsa-data-table> | ||
</div> |
Empty file.
22 changes: 22 additions & 0 deletions
22
apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
||
import { ClimateAdminPageComponent } from './admin.component'; | ||
|
||
describe('ClimateAdminPageComponent', () => { | ||
let component: ClimateAdminPageComponent; | ||
let fixture: ComponentFixture<ClimateAdminPageComponent>; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [ClimateAdminPageComponent], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(ClimateAdminPageComponent); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should create', () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
}); |
152 changes: 152 additions & 0 deletions
152
apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { CommonModule, DatePipe } from '@angular/common'; | ||
import { Component, computed, effect, signal } from '@angular/core'; | ||
import { MatButtonModule } from '@angular/material/button'; | ||
import { MatIconModule } from '@angular/material/icon'; | ||
import { ActivatedRoute, Router } from '@angular/router'; | ||
import { IDataTableOptions, PicsaDataTableComponent } from '@picsa/shared/features'; | ||
import { SupabaseService } from '@picsa/shared/services/core/supabase'; | ||
import { _wait, arrayToHashmap } from '@picsa/utils'; | ||
import download from 'downloadjs'; | ||
import JSZip from 'jszip'; | ||
import { unparse } from 'papaparse'; | ||
|
||
import { DeploymentDashboardService } from '../../../deployment/deployment.service'; | ||
import { ClimateService } from '../../climate.service'; | ||
import type { IAnnualRainfallSummariesData, IClimateSummaryRainfallRow, IStationRow } from '../../types'; | ||
import { hackConvertAPIDataToLegacyFormat } from '../station-details/components/rainfall-summary/rainfall-summary.utils'; | ||
|
||
interface IStationAdminSummary { | ||
station_id: string; | ||
row: IStationRow; | ||
updated_at?: string; | ||
rainfall_summary?: IClimateSummaryRainfallRow; | ||
start_year?: number; | ||
end_year?: number; | ||
} | ||
|
||
const DISPLAY_COLUMNS: (keyof IStationAdminSummary)[] = ['station_id', 'updated_at', 'start_year', 'end_year']; | ||
|
||
/** | ||
* TODOs - See #333 | ||
*/ | ||
|
||
@Component({ | ||
selector: 'dashboard-climate-admin-page', | ||
standalone: true, | ||
imports: [CommonModule, DatePipe, MatButtonModule, MatIconModule, PicsaDataTableComponent], | ||
templateUrl: './admin.component.html', | ||
styleUrl: './admin.component.scss', | ||
}) | ||
export class ClimateAdminPageComponent { | ||
public tableData = computed(() => { | ||
const stations = this.service.stations(); | ||
const rainfallSummaries = this.rainfallSummaries(); | ||
return this.generateTableSummaryData(stations, rainfallSummaries); | ||
}); | ||
public tableOptions: IDataTableOptions = { | ||
displayColumns: DISPLAY_COLUMNS, | ||
handleRowClick: ({ station_id }: IStationAdminSummary) => | ||
this.router.navigate(['../', 'station', station_id], { relativeTo: this.route }), | ||
}; | ||
public refreshCount = signal(-1); | ||
|
||
private rainfallSummaries = signal<IClimateSummaryRainfallRow[]>([]); | ||
|
||
constructor( | ||
private service: ClimateService, | ||
private deploymentService: DeploymentDashboardService, | ||
private supabase: SupabaseService, | ||
private router: Router, | ||
private route: ActivatedRoute | ||
) { | ||
effect( | ||
() => { | ||
const country_code = this.deploymentService.activeDeployment()?.country_code; | ||
if (country_code) { | ||
this.loadRainfallSummaries(country_code); | ||
} | ||
}, | ||
{ allowSignalWrites: true } | ||
); | ||
} | ||
private get db() { | ||
return this.supabase.db.table('climate_summary_rainfall'); | ||
} | ||
|
||
public async downloadAllStationsCSV() { | ||
const zip = new JSZip(); | ||
for (const summary of this.tableData()) { | ||
const csvData = this.generateStationCSVDownload(summary); | ||
if (csvData) { | ||
zip.file(`${summary.row.station_id}.csv`, csvData); | ||
} | ||
} | ||
const blob = await zip.generateAsync({ type: 'blob' }); | ||
const country_code = this.deploymentService.activeDeployment()?.country_code; | ||
download(blob, `${country_code}_rainfall_summaries.zip`); | ||
} | ||
|
||
public downloadStationCSV(station: IStationAdminSummary) { | ||
const csv = this.generateStationCSVDownload(station); | ||
if (csv) { | ||
download(csv, station.row.station_id, 'text/csv'); | ||
} | ||
} | ||
|
||
public async refreshAllStations() { | ||
this.refreshCount.set(0); | ||
const promises = this.tableData().map(async (station, i) => { | ||
// hack - instead of queueing apply small offset between requests to reduce blocking | ||
await _wait(200 * i); | ||
await this.service.loadFromAPI.rainfallSummaries(station.row); | ||
this.refreshCount.update((v) => v + 1); | ||
}); | ||
await Promise.all(promises); | ||
await this.loadRainfallSummaries(this.deploymentService.activeDeployment()?.country_code as string); | ||
} | ||
|
||
private generateStationCSVDownload(summary: IStationAdminSummary) { | ||
const { rainfall_summary } = summary; | ||
if (rainfall_summary && rainfall_summary.data) { | ||
const data = rainfall_summary.data as any[]; | ||
const csvData = hackConvertAPIDataToLegacyFormat(data); | ||
const columns = Object.keys(csvData[0]); | ||
const csv = unparse(csvData, { columns }); | ||
return csv; | ||
} | ||
return undefined; | ||
} | ||
|
||
private generateTableSummaryData(stations: IStationRow[], rainfallSummaries: IClimateSummaryRainfallRow[]) { | ||
// NOTE - only single entry for rainfallSummary (not hashmapArray) | ||
const rainfallSummariesHashmap = arrayToHashmap(rainfallSummaries, 'station_id'); | ||
return stations.map((row) => { | ||
const { station_id, id } = row; | ||
const summary: IStationAdminSummary = { station_id, row }; | ||
const rainfallSummary = rainfallSummariesHashmap[station_id]; | ||
if (rainfallSummary) { | ||
const { data, updated_at } = rainfallSummary; | ||
summary.updated_at = updated_at; | ||
summary.rainfall_summary = rainfallSummary; | ||
const entries = data as IAnnualRainfallSummariesData[]; | ||
summary.start_year = entries[0]?.year; | ||
summary.end_year = entries[entries.length - 1]?.year; | ||
} | ||
return summary; | ||
}); | ||
} | ||
|
||
private async loadRainfallSummaries(country_code: string) { | ||
const { data, error } = await this.db.select<'*', IClimateSummaryRainfallRow>('*').eq('country_code', country_code); | ||
if (error) { | ||
throw error; | ||
} | ||
this.rainfallSummaries.set( | ||
data.map((el) => { | ||
// HACK - to keep parent ref id includes full country prefix, remove for lookup | ||
el.station_id = el.station_id.replace(`${country_code}/`, ''); | ||
return el; | ||
}) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.