Skip to content

Commit

Permalink
Merge pull request #231 from e-picsa/ft-monitoring-dashboard
Browse files Browse the repository at this point in the history
feat(dashboard): add monitoring pages and forms db
  • Loading branch information
chrismclarke authored Mar 1, 2024
2 parents 1dde812 + b5c44f1 commit c468984
Show file tree
Hide file tree
Showing 27 changed files with 584 additions and 19 deletions.
1 change: 0 additions & 1 deletion apps/picsa-apps/dashboard/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { DeploymentSelectComponent } from './modules/deployment/components';
})
export class AppComponent implements AfterViewInit {
title = 'picsa-apps-dashboard';

navLinks = DASHBOARD_NAV_LINKS;

globalLinks: INavLink[] = [
Expand Down
4 changes: 4 additions & 0 deletions apps/picsa-apps/dashboard/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const appRoutes: Route[] = [
},

// unmatched routes fallback to home
{
path: 'monitoring',
loadChildren: () => import('./modules/monitoring/monitoring-forms.module').then((m) => m.MonitoringFormsPageModule),
},
{
path: '**',
redirectTo: 'home',
Expand Down
9 changes: 5 additions & 4 deletions apps/picsa-apps/dashboard/src/app/data/navLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ export const DASHBOARD_NAV_LINKS = [
// label: 'Crop Information',
// href: '/crop-information',
// },
// {
// label: 'Monitoring Forms',
// href: '/monitoring-forms',
// },
{
label: 'Monitoring Forms',
href: '/monitoring',
matIcon: 'poll',
},
{
label: 'Translations',
href: '/translations',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { FormSubmissionsComponent } from './pages/form-submissions/form-submissions.component';
import { MonitoringPageComponent } from './pages/home/monitoring.page';
import { ViewMonitoringFormsComponent } from './pages/view/view-monitoring-forms.component';

@NgModule({
declarations: [],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: MonitoringPageComponent,
},
{
path: ':id',
component: ViewMonitoringFormsComponent,
},
{
path: ':id/submissions',
component: FormSubmissionsComponent,
},
]),
],
})
export class MonitoringFormsPageModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Database } from '@picsa/server-types';
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service';
import { SupabaseService } from '@picsa/shared/services/core/supabase';
import { IStorageEntry } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';

export type IMonitoringFormsRow = Database['public']['Tables']['monitoring_forms']['Row'];

export interface IMonitoringStorageEntry extends IStorageEntry {
/** Url generated when upload to public bucket (will always be populated, even if bucket not public) */
publicUrl: string;
}
@Injectable({ providedIn: 'root' })
export class MonitoringFormsDashboardService extends PicsaAsyncService {
public forms: IMonitoringFormsRow[] = [];
public TABLE_NAME = 'monitoring_forms';
public SUBMISSIONS_TABLE_NAME = 'monitoring_tool_submissions';

public get table() {
return this.supabaseService.db.table(this.TABLE_NAME);
}

constructor(private supabaseService: SupabaseService) {
super();
}

public override async init() {
await this.supabaseService.ready();
await this.listMonitoringForms();
}

public async listMonitoringForms() {
const { data, error } = await this.supabaseService.db.table(this.TABLE_NAME).select<'*', IMonitoringFormsRow>('*');
if (error) {
throw error;
}
this.forms = data || [];
}

// Fetch a form record by ID
public async getFormById(id: string): Promise<IMonitoringFormsRow> {
const { data, error } = await this.supabaseService.db.table(this.TABLE_NAME).select('*').eq('id', id).single();
if (error) {
throw error;
}
return data;
}

public async getSubmissions(formId: string) {
const { data, error } = await this.supabaseService.db
.table(this.SUBMISSIONS_TABLE_NAME)
.select('*')
.eq('formId', formId);
if (error) {
throw error;
}
return { data, error };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">
@if(form){
{{ form.title }}
}
</h2>
</div>
@if(submissions){
<h3>Submissions</h3>
<picsa-data-table [data]="submissions" [valueTemplates]="{}" [options]="{ displayColumns: displayedColumns }">
</picsa-data-table>
} @else if(dataLoadError){
<div>{{ dataLoadError }}</div>
} @else {
<div>Loading...</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.form-content{
display: flex;
flex-direction: column;
gap: 1.4rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormSubmissionsComponent } from './form-submissions.component'

describe('FormSubmissionsComponent', () => {
let component: FormSubmissionsComponent;
let fixture: ComponentFixture<FormSubmissionsComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormSubmissionsComponent],
}).compileComponents();

fixture = TestBed.createComponent(FormSubmissionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
// eslint-disable-next-line @nx/enforce-module-boundaries
import type { Database } from '@picsa/server-types';
import { IDataTableOptions, PicsaDataTableComponent } from '@picsa/shared/features/data-table';
import { NgxJsonViewerModule } from 'ngx-json-viewer';

import { DashboardMaterialModule } from '../../../../material.module';
import { IMonitoringFormsRow, MonitoringFormsDashboardService } from '../../monitoring.service';

export type IMonitoringSubmissionsRow = Database['public']['Tables']['monitoring_tool_submissions']['Row'];

@Component({
selector: 'dashboard-form-submissions',
standalone: true,
imports: [
CommonModule,
DashboardMaterialModule,
FormsModule,
ReactiveFormsModule,
NgxJsonViewerModule,
PicsaDataTableComponent,
],
templateUrl: './form-submissions.component.html',
styleUrls: ['./form-submissions.component.scss'],
})
export class FormSubmissionsComponent {
public form: IMonitoringFormsRow | null = null;
public submissions: any[];
dataLoadError: string;
displayedColumns: string[];

constructor(private service: MonitoringFormsDashboardService, private route: ActivatedRoute, private router: Router) {
this.service.ready();
this.route.params.subscribe(async (params) => {
const id = params['id'];
const form = await service.getFormById(id);
this.form = form;
if (form && form.summary_fields) {
this.displayedColumns = form.summary_fields.map((entry) => entry?.['field']);
}
await this.fetchSubmissions(id);
});
}
public tableOptions: IDataTableOptions = {
paginatorSizes: [25, 50],
};

async fetchSubmissions(formId: string) {
const { data, error } = await this.service.getSubmissions(formId);
if (error) {
this.dataLoadError = 'Failed to fetch submitions';
} else {
this.submissions = data.map((submission) => {
if (submission.json) {
return submission.json;
}
return;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Monitoring Forms</h2>
</div>
@if(service.forms){
<picsa-data-table
[data]="service.forms"
[valueTemplates]="{summary_fields:summaryFieldTemplate, enketo_form:enketoFormTemplete, enketo_model:enketoModelTemplate,created_at:dateTemplate }"
[options]="{displayColumns:displayedColumns,handleRowClick:onRowClick}"
>
<ng-template #summaryFieldTemplate let-values>
@for (item of values; track $index) { {{ item['label'] }}<br />}
</ng-template>
<ng-template #enketoFormTemplete let-values>
<div class="long-text-rows">{{values}}</div>
</ng-template>
<ng-template #enketoModelTemplate let-values>
<div class="long-text-rows">{{values}}</div>
</ng-template>
<ng-template #dateTemplate let-value> {{ value | date: 'mediumDate' }} </ng-template>
</picsa-data-table>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
table.resources-table {
max-height: 50vh;
display: block;
overflow: auto;
}
tr.resource-row {
cursor: pointer;
&:hover {
background: whitesmoke;
}
}
th {
font-weight: bold;
}

td {
cursor: pointer;
}

.disallow-click {
pointer-events: none;
cursor: not-allowed;
}
.allow-click {
pointer-events: all;
cursor: pointer;
}
.long-text-rows {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 13rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { MonitoringPageComponent } from './monitoring.page';

describe('MonitoringPageComponent', () => {
let component: MonitoringPageComponent;
let fixture: ComponentFixture<MonitoringPageComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MonitoringPageComponent],
}).compileComponents();

fixture = TestBed.createComponent(MonitoringPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { Router } from '@angular/router';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Database } from '@picsa/server-types';
import { IDataTableOptions, PicsaDataTableComponent } from '@picsa/shared/features/data-table';

import { DashboardMaterialModule } from '../../../../material.module';
import { MonitoringFormsDashboardService } from '../../monitoring.service';
export type IMonitoringFormsRow = Database['public']['Tables']['monitoring_forms']['Row'];

@Component({
selector: 'dashboard-monitoring-page',
standalone: true,
imports: [CommonModule, DashboardMaterialModule, RouterModule, PicsaDataTableComponent],
templateUrl: './monitoring.page.html',
styleUrls: ['./monitoring.page.scss'],
})
export class MonitoringPageComponent implements OnInit {
displayedColumns: (keyof IMonitoringFormsRow)[] = [
'title',
'description',
'summary_fields',
'enketo_form',
'enketo_model',
'created_at',
];

constructor(public service: MonitoringFormsDashboardService, private router: Router, private route: ActivatedRoute) {}

public tableOptions: IDataTableOptions = {
paginatorSizes: [25, 50],
};
ngOnInit(): void {
this.service.ready();
}
//this was returning undefined for "this.router" before I made it an arrow funtion, any idea why?
onRowClick = (row: IMonitoringFormsRow) => {
this.router.navigate([row.id], { relativeTo: this.route });
};
}
Loading

0 comments on commit c468984

Please sign in to comment.