Skip to content

Commit

Permalink
quota log and detail view
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelmattig committed Nov 27, 2024
1 parent e533c0f commit 16a9a75
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 56 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 11 additions & 3 deletions projects/common/src/lib/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Injectable} from '@angular/core';
import {ComputationQuota, Configuration, DefaultConfig, SessionApi, UserApi, UserSession} from '@geoengine/openapi-client';
import {ComputationQuota, Configuration, DefaultConfig, OperatorQuota, SessionApi, UserApi, UserSession} from '@geoengine/openapi-client';
import {Observable, ReplaySubject, filter, first, firstValueFrom, map} from 'rxjs';
import {UUID} from '../datasets/dataset.model';

Expand Down Expand Up @@ -93,15 +93,23 @@ export class UserService {
this.session$.next(undefined);
}

async computationsQuota(workflow: UUID, limit: number): Promise<ComputationQuota[]> {
async computationsQuota(offset: number, limit: number): Promise<ComputationQuota[]> {
const userApi = await firstValueFrom(this.userApi);

return userApi.computationsQuotaHandler({
workflow,
offset,
limit,
});
}

async computationQuota(computation: UUID): Promise<OperatorQuota[]> {
const userApi = await firstValueFrom(this.userApi);

return userApi.computationQuotaHandler({
computation,
});
}

async createSessionWithToken(sessionToken: string): Promise<UserSession> {
return new SessionApi(apiConfigurationWithAccessKey(sessionToken)).sessionHandler().then((session) => {
this.session$.next(session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,45 +101,7 @@ <h1 class="mat-h1 header-container">
<mat-card-title> Usage log </mat-card-title>
</mat-card-header>
<mat-card-content #analyzecard class="dashboard-card-content score-container">
@if (usage(); as usage) {
<table class="mat-elevation-z8 mat-table">
<tr class="mat-row">
<td class="mat-cell">Workflow</td>
<td class="mat-cell">{{ usage.workflowId }}</td>
</tr>
<tr class="mat-row">
<td class="mat-cell">Computation</td>
<td class="mat-cell">{{ usage.computationId }}</td>
</tr>
</table>

<table class="mat-elevation-z8 mat-table">
<tr class="mat-row">
<td class="mat-cell">Operator</td>
@for (operator of usage.operators; track $index) {
<td class="mat-cell">{{ operator.operatorName }}</td>
}
</tr>
<!-- <tr class="mat-row">
<td class="mat-cell">Path</td>
@for (operator of usage.operators; track $index) {
<td class="mat-cell">{{ operator.operatorPath }}</td>
}
</tr> -->
<tr class="mat-row">
<td class="mat-cell">Count</td>
@for (operator of usage.operators; track $index) {
<td class="mat-cell">{{ operator.count }}</td>
}
</tr>
</table>
} @else {
@if (usageLoading()) {
<mat-spinner color="accent"></mat-spinner>
} @else {
<p class="center">Usage log will be visible after an analysis was performed.</p>
}
}
<geoengine-quota-log></geoengine-quota-log>
</mat-card-content>
</mat-card>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ import {
import {utc} from 'moment';
import {DataSelectionService} from '../data-selection.service';
import {ComputationQuota, Workflow} from '@geoengine/openapi-client';
import {LegendComponent} from '../legend/legend.component';
import {toSignal} from '@angular/core/rxjs-interop';
import {Router} from '@angular/router';
import proj4 from 'proj4';
import {MatProgressSpinner} from '@angular/material/progress-spinner';
import {QuotaLogComponent} from '../quota-log/quota-log.component';

interface SelectedProperty {
featureId: number;
Expand All @@ -67,7 +67,7 @@ interface SelectedProperty {
styleUrl: './dashboard.component.scss',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CoreModule, AsyncPipe, MatGridListModule, MatMenuModule, MatIconModule, MatButtonModule, MatCardModule, LegendComponent],
imports: [CoreModule, AsyncPipe, MatGridListModule, MatMenuModule, MatIconModule, MatButtonModule, MatCardModule, QuotaLogComponent],
})
export class DashboardComponent implements AfterViewInit, AfterContentInit {
readonly userService = inject(UserService);
Expand Down Expand Up @@ -122,8 +122,7 @@ export class DashboardComponent implements AfterViewInit, AfterContentInit {
}

async ngAfterContentInit(): Promise<void> {
this.loadClassification();
await this.loadProperties();
this.loadData();

this.projectService.getSelectedFeatureStream().subscribe(async (featureSelection) => {
const features = await this.dataSelectionService.getPolygonLayerFeatures();
Expand Down Expand Up @@ -153,6 +152,14 @@ export class DashboardComponent implements AfterViewInit, AfterContentInit {
});
}

private async loadData(): Promise<void> {
// wait for project to be loaded before redirecting
await firstValueFrom(this.projectService.getProjectOnce());

this.loadClassification();
await this.loadProperties();
}

async loadClassification(): Promise<void> {
const workflowId = await firstValueFrom(this.projectService.registerWorkflow(CLASSIFICATION_WORKFLOW));

Expand Down Expand Up @@ -272,14 +279,14 @@ export class DashboardComponent implements AfterViewInit, AfterContentInit {
this.score.set(score);
this.scoreLoading.set(false);

this.usageLoading.set(true);
const usage = await this.commonUserService.computationsQuota(workflowId, 1);
// this.usageLoading.set(true);
// const usage = await this.commonUserService.computationsQuota(workflowId, 1);

if (usage.length > 0) {
this.usage.set(usage[0]);
}
// if (usage.length > 0) {
// this.usage.set(usage[0]);
// }

this.usageLoading.set(false);
// this.usageLoading.set(false);
}

logout(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {BehaviorSubject, firstValueFrom, Subscription} from 'rxjs';
import {BehaviorSubject, Subscription} from 'rxjs';

import {AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
Expand Down Expand Up @@ -112,8 +112,6 @@ export class LoginComponent implements OnInit, AfterViewInit, OnDestroy {
this.invalidCredentials$.next(false);
this.formStatus$.next(FormStatus.LoggedIn);

// wait for project to be loaded before redirecting
await firstValueFrom(this.projectService.getProjectOnce());
this.redirectToMainView();
},
() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<div [class.hidden]="details()">
<table mat-table [dataSource]="source">
<ng-container matColumnDef="timestamp">
<th mat-header-cell *matHeaderCellDef>Timestamp</th>
<td mat-cell *matCellDef="let element">{{ element.timestamp.toISOString() }}</td>
</ng-container>

<ng-container matColumnDef="computationId">
<th mat-header-cell *matHeaderCellDef>Computation</th>
<td mat-cell *matCellDef="let element">{{ element.computationId }}</td>
</ng-container>

<ng-container matColumnDef="workflowId">
<th mat-header-cell *matHeaderCellDef>Workflow</th>
<td mat-cell *matCellDef="let element">{{ element.workflowId }}</td>
</ng-container>

<ng-container matColumnDef="count">
<th mat-header-cell *matHeaderCellDef>Count</th>
<td mat-cell *matCellDef="let element">{{ element.count }}</td>
</ng-container>

<ng-container matColumnDef="details">
<th mat-header-cell *matHeaderCellDef>Details</th>
<td mat-cell *matCellDef="let element">
<button class="button" mat-icon-button (click)="showDetails(element)">
<mat-icon>zoom_in</mat-icon>
</button>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>

<div class="flex-container">
<button mat-button (click)="setUpSource()"><mat-icon>refresh</mat-icon>Refresh</button>

<mat-paginator [pageSize]="5" [pageSizeOptions]="[5, 10, 20]"></mat-paginator>
</div>
</div>

@if (details(); as details) {
<div>
<button class="button" mat-button (click)="hideDetails()"><mat-icon>arrow_back</mat-icon> Back</button>
</div>
<table mat-table [dataSource]="details">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Operator</th>
<td mat-cell *matCellDef="let element">{{ element.operatorName }}</td>
</ng-container>

<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef>Path</th>
<td mat-cell *matCellDef="let element">{{ element.operatorPath }}</td>
</ng-container>

<ng-container matColumnDef="count">
<th mat-header-cell *matHeaderCellDef>Count</th>
<td mat-cell *matCellDef="let element">{{ element.count }}</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedDetailsColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedDetailsColumns"></tr>
</table>
}

<mat-progress-bar class="loading-indicator" mode="query" *ngIf="source.loading()"></mat-progress-bar>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
:host {
width: 100%;
}

mat-spinner {
margin: 0.5rem auto;
}

table {
width: 100%;
}

.flex-container {
display: flex;
justify-content: space-between;
gap: 1rem;
}

.hidden {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {DataSource} from '@angular/cdk/collections';
import {AfterViewInit, ChangeDetectionStrategy, Component, signal, ViewChild} from '@angular/core';
import {Observable, Subject, tap} from 'rxjs';
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
import {UserService} from '@geoengine/common';
import {MatTableModule} from '@angular/material/table';
import {ComputationQuota, OperatorQuota} from '@geoengine/openapi-client';
import {MatButtonModule} from '@angular/material/button';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatIconModule} from '@angular/material/icon';

@Component({
selector: 'geoengine-quota-log',
templateUrl: './quota-log.component.html',
styleUrl: './quota-log.component.scss',
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatProgressBarModule, MatButtonModule, MatIconModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuotaLogComponent implements AfterViewInit {
@ViewChild(MatPaginator) paginator!: MatPaginator;

readonly loadingSpinnerDiameterPx: number = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize);

source!: QuotaLogDataSource;

displayedColumns: string[] = ['timestamp', 'computationId', 'workflowId', 'count', 'details'];
displayedDetailsColumns: string[] = ['name', 'path', 'count'];

readonly detailsVisible = signal(false);
readonly details = signal<OperatorQuota[] | undefined>(undefined);

constructor(private readonly userService: UserService) {
this.setUpSource();
}

ngAfterViewInit(): void {
this.paginator.page.pipe(tap(() => this.loadQuotaLogsPage())).subscribe();
}

async showDetails(element: ComputationQuota): Promise<void> {
this.detailsVisible.set(true);
const quota = await this.userService.computationQuota(element.computationId);
this.details.set(quota);
}

hideDetails(): void {
this.details.set(undefined);
this.detailsVisible.set(false);
}

protected loadQuotaLogsPage(): void {
this.source.loadQuotaLogs(this.paginator.pageIndex, this.paginator.pageSize);
}

protected setUpSource(): void {
this.source = new QuotaLogDataSource(this.userService, this.paginator);
this.source.loadQuotaLogs(0, 5);
}
}

/**
* A custom data source that allows fetching datasets for a virtual scroll source.
*/
class QuotaLogDataSource extends DataSource<ComputationQuota> {
readonly loading = signal(false);

protected quotas$ = new Subject<Array<ComputationQuota>>();

constructor(
private userService: UserService,
private paginator: MatPaginator,
) {
super();
}

connect(): Observable<Array<ComputationQuota>> {
return this.quotas$.asObservable();
}

/**
* Clean up resources
*/
disconnect(): void {
this.quotas$.complete();
}

loadQuotaLogs(pageIndex: number, pageSize: number): void {
this.loading.set(true);

this.userService.computationsQuota(pageIndex * pageSize, pageSize).then((logs) => {
this.loading.set(false);
if (this.paginator && logs.length === pageSize) {
// we do not know the number of items in total, so instead for each full page set the length to show the "next" button
this.paginator.length = (pageIndex + 1) * pageSize + 1;
}

this.quotas$.next(logs);
});
}
}

0 comments on commit 16a9a75

Please sign in to comment.