feat: add control side-panel

This commit is contained in:
2025-02-11 10:56:49 +01:00
parent 24da687a26
commit 4602583597
10 changed files with 251 additions and 118 deletions

View File

@ -0,0 +1,7 @@
export interface Session {
name: string;
deployment_id: string;
_id: string;
owner_groups: string[];
access_groups: []; // This should probably be string[] as well
}

View File

@ -1,5 +1,6 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Session } from './model/session';
import { ServerSettingsService } from '../server-settings.service';
import { ScanDataResponse } from './model/scan-data';
import { Realm } from './model/realm';
@ -229,3 +230,26 @@ export class ScanDataService extends RemoteDataService {
);
}
}
@Injectable({
providedIn: 'root',
})
export class SessionDataService extends RemoteDataService {
/**
* Method for getting the available sessions
* @param offset Pagination offset (default = 0)
* @param limit Number of records to retrieve (default = 100)
* @returns response from the server with the scan data
* @throws HttpErrorResponse if the request fails
* @throws TimeoutError if the request takes too long
*/
getSessions(offset: number = 0, limit: number = 100) {
let headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json; charset=utf-8');
return this.get<Session[]>(
'sessions',
{ offset: offset.toString(), limit: limit.toString() },
headers
);
}
}

View File

@ -25,15 +25,32 @@
<mat-divider></mat-divider>
<!-- Scan Table -->
<button
mat-button
class="menu-item"
[routerLink]="['/dashboard/scan-table']"
>
<mat-icon>home</mat-icon>
<span class="menu-text">Data Browser</span>
</button>
<!-- Data Browser -->
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-icon>table_chart</mat-icon>
<span class="menu-text">Data Browser</span>
</mat-expansion-panel-header>
<button
mat-button
class="menu-item"
[routerLink]="['/dashboard/scan-table']"
> Session Data
</button>
<button
mat-button
class="menu-item"
> Scan Data
</button>
<button
mat-button
class="menu-item"
> Device Data
</button>
</mat-expansion-panel>
<!-- Experiment Control Expansion -->
<mat-expansion-panel

View File

@ -1,69 +1,76 @@
<!-- Table -->
<div class="table-container">
<mat-card>
<!-- Toolbar -->
<mat-toolbar color="primary">
Table with Scan Data
<span class="spacer"></span>
<button mat-icon-button (click)="ngOnInit()">
<mat-icon>search</mat-icon>
</button>
<button mat-icon-button (click)="handleRefresh()">
<mat-icon>refresh</mat-icon>
</button>
<button mat-icon-button (click)="openDialog()">
<mat-icon>settings</mat-icon>
</button>
</mat-toolbar>
<!-- Table -->
<table mat-table *ngIf="tableData() as data" [dataSource]="data" class="mat-elevation-z8">
@for (column of displayedColumns(); track column) {
<ng-container [matColumnDef]="column">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ column | titlecase }}</th>
<td mat-cell class="table-cell" *matCellDef="let element">
@if (column === 'timestamp') {
{{ element[column] * 1000 | date :'HH:mm:ss'}}
<br>
{{ element[column] * 1000 | date :'dd/MM/yyyy'}}
}
@else if (column === 'user_rating') {
<star-rating
[starType]="'svg'"
[hoverEnabled]="true"
(ratingChange)="handleOnRatingChanged($event, element)"
[rating]="element[column]">
</star-rating>
}
@else if (column === 'user_comments') {
<mat-form-field appearance="outline">
<textarea matInput ></textarea>
</mat-form-field>
<div class="main-container">
<mat-sidenav-container class="sidenav-container">
}
@else{
<p> {{ element[column] }}</p>
}
</ng-container>
}
<!-- Header Row -->
<tr mat-header-row *matHeaderRowDef="displayedColumns(); sticky: true"></tr>
<!-- Data Rows -->
<tr mat-row *matRowDef="let row; columns: displayedColumns()"></tr>
</table>
</mat-card>
</div>
<!-- Paginator -->
<div class="table-paginator">
<mat-paginator
#paginator
class="table-paginator"
(page) = "handlePageEvent($event)"
[length]="totalScanCount()"
[pageSize]= "limit()"
[showFirstLastButtons]="true"
[pageSizeOptions]="[5, 10, 25, 100]"
[pageIndex]="offset()/limit()"
aria-label="Select page"
sticky= true>
</mat-paginator>
<mat-sidenav-content>
<div class="table-container">
<mat-card>
<!-- Toolbar -->
<mat-toolbar color="primary">
Scan Data for {{session()?.name}}
<span class="spacer"></span>
<button mat-icon-button>
<!-- Search is not working yet and not hooked up-->
<mat-icon>search</mat-icon>
</button>
<button mat-icon-button (click)="handleRefresh()">
<mat-icon>refresh</mat-icon>
</button>
<button mat-icon-button (click)="openDialog()">
<mat-icon>settings</mat-icon>
</button>
</mat-toolbar>
<!-- Table -->
<table mat-table *ngIf="tableData() as data" [dataSource]="data" class="mat-elevation-z8">
@for (column of displayedColumns(); track column) {
<ng-container [matColumnDef]="column">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ column | titlecase }}</th>
<td mat-cell class="table-cell" *matCellDef="let element">
@if (column === 'timestamp') {
{{ element[column] * 1000 | date :'HH:mm:ss'}}
<br>
{{ element[column] * 1000 | date :'dd/MM/yyyy'}}
}
@else if (column === 'user_rating') {
<star-rating
[starType]="'svg'"
[hoverEnabled]="true"
(ratingChange)="handleOnRatingChanged($event, element)"
[rating]="element[column]">
</star-rating>
}
@else{
<p> {{ element[column] }}</p>
}
</ng-container>
}
<!-- Header Row -->
<tr mat-header-row *matHeaderRowDef="displayedColumns(); sticky: true"></tr>
<!-- Data Rows -->
<tr mat-row *matRowDef="let row; columns: displayedColumns()"></tr>
</table>
</mat-card>
</div>
<!-- Paginator -->
<div class="table-paginator">
<mat-paginator
#paginator
class="table-paginator"
(page) = "handlePageEvent($event)"
[length]="totalScanCount()"
[pageSize]= "limit()"
[showFirstLastButtons]="true"
[pageSizeOptions]="[5, 10, 25, 100]"
[pageIndex]="offset()/limit()"
aria-label="Select page"
sticky= true>
</mat-paginator>
</div>
</mat-sidenav-content>
<!-- Right side panel -->
<mat-sidenav #rightSidenav mode="side" opened="true" position="end" class="sidenav">
<!-- Embed the side-panel component here -->
<app-side-panel (sessionChanged)="onSessionChange($event)"></app-side-panel>
</mat-sidenav>
</mat-sidenav-container>
</div>

View File

@ -5,8 +5,10 @@
padding-left: 16px;
padding-bottom: 16px;
}
.table-container{
width: auto;
padding-right: 16px;
}
.mat-mdc-row:hover {
background-color: var(--mat-sys-secondary-container);
@ -21,19 +23,9 @@
max-height: 200px
}
textarea {
resize: none;
min-height: 32px; /* Initial height */
max-height: 200px; /* Maximum height */
overflow-y: auto; /* Enable scroll when max height is reached */
width: 100%;
padding:0px;
border: none;
}
mat-form-field {
font-size: 12px;
line-height: 1.2;
padding-top: 16px;
max-height: var(max-height);
}
.main-container {
// display: flex;
// flex-direction: column;
height: 100%;
width:100%;
}

View File

@ -6,6 +6,7 @@ import {
resource,
Signal,
inject,
WritableSignal,
} from '@angular/core';
import { ScanDataService } from '../core/remote-data.service';
import { ScanDataResponse } from '../core/model/scan-data';
@ -31,15 +32,9 @@ import { MatDialog } from '@angular/material/dialog';
import { ColumnSelectionDialogComponent } from './column-selection-dialog/column-selection-dialog.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
export interface ResourceStatus {
status: any;
}
export interface ResourceLoaderParams {
request: any;
abortSignal: AbortSignal;
previous: ResourceStatus;
}
import { SidePanelComponent } from './side-panel/side-panel.component';
import { MatSidenavModule } from '@angular/material/sidenav';
import { Session } from '../core/model/session';
@Component({
selector: 'app-scan-table',
@ -59,20 +54,21 @@ export interface ResourceLoaderParams {
MatCheckboxModule,
MatFormFieldModule,
MatInputModule,
SidePanelComponent,
MatSidenavModule,
],
templateUrl: './scan-table.component.html',
styleUrl: './scan-table.component.scss',
})
export class ScanTableComponent {
// --------------------------------
// -------------Signals-------------
// --------------------------------
tableData: Signal<ScanDataResponse[]>;
totalScanCount: Signal<number>;
limit = signal<number>(10);
offset = signal<number>(0);
sessionId = signal<string>('');
dialog = inject(MatDialog);
pageEvent: PageEvent = new PageEvent();
isEditingUserComments: boolean = false;
sorting: number = -1;
session: WritableSignal<Session | null> = signal(null);
displayedColumns = signal<string[]>([
'scan_number',
'status',
@ -82,8 +78,15 @@ export class ScanTableComponent {
'dataset_number',
'timestamp',
'user_rating',
'user_comments',
]);
// -----------------------------------
// -------------Variables-------------
// -----------------------------------
dialog = inject(MatDialog);
pageEvent: PageEvent = new PageEvent();
isEditingUserComments: boolean = false;
sorting: number = -1;
allColumns: string[] = [
'scan_id',
'scan_number',
@ -116,17 +119,28 @@ export class ScanTableComponent {
'info',
];
// ----------------------------------------
// -------------Compute Signals-------------
// ----------------------------------------
// Available columns are all columns that are not ignored
availableColumns = computed(() =>
this.allColumns.filter((element) => !this.ignoredEntries.includes(element))
);
// Reload criteria is the criteria used to reload the scan data
reloadCriteria = computed(() => ({
sessionId: this.sessionId(),
session: this.session(),
offset: this.offset(),
limit: this.limit(),
column: this.displayedColumns(),
}));
// -----------------------------------
// -------------Resources-------------
// -----------------------------------
// Load scan data resource
loadScanDataResource = resource({
request: () => this.reloadCriteria(),
loader: ({ request, abortSignal }): Promise<ScanDataResponse[]> => {
@ -142,10 +156,11 @@ export class ScanTableComponent {
: element
);
columns.push('scan_id'); // always include scan_id
let sessionId = request.session ? request.session._id : '';
console.log('Columns', columns);
return firstValueFrom(
this.scanData.getScanData(
request.sessionId,
sessionId,
request.offset,
request.limit,
columns,
@ -156,13 +171,18 @@ export class ScanTableComponent {
},
});
// Load scan count resource
loadScanCountResource = resource({
request: () => this.reloadCriteria(),
loader: ({ request, abortSignal }): Promise<ScanCountResponse> => {
return firstValueFrom(this.scanData.getScanCount(request.sessionId));
let sessionId = request.session ? request.session._id : '';
return firstValueFrom(this.scanData.getScanCount(sessionId));
},
});
// -----------------------------------
// -------------Functions-------------
// -----------------------------------
handleScanData(data: ScanDataResponse[] | []) {
for (const entry of data) {
if (entry?.user_data !== undefined) {
@ -201,11 +221,9 @@ export class ScanTableComponent {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
ngOnInit(): void {
this.sessionId.set('6793628df62026a414d9338e');
// this.updateUI();
}
// ----------------------------------------
// -------------Event Handlers-------------
// ----------------------------------------
handlePageEvent(event: PageEvent) {
this.pageEvent = event;
this.offset.set(event.pageIndex * event.pageSize);
@ -228,14 +246,14 @@ export class ScanTableComponent {
dialogRef.afterClosed().subscribe((result: string[] | null) => {
if (result !== null) {
this.displayedColumns.set(result);
// this.handleRefresh();
}
});
}
handleColumnSelection(event: any) {}
toggleAllEdit() {}
onSessionChange(session: Session | null): void {
console.log('Session changed', session);
this.session.set(session);
}
async handleOnRatingChanged(event: any, element: ScanDataResponse) {
console.log('Event', event, 'Element', element);

View File

@ -0,0 +1,11 @@
<h2>Control Panel</h2>
<mat-form-field appearance="outline" class="side-panel-item">
<mat-label>Select Session</mat-label>
<mat-select [value]="selectedSession" (selectionChange)="onSessionChange($event.value)">
<mat-option>--</mat-option>
@for (session of sessions; track session) {
<mat-option [value]="session">{{session.name}}</mat-option>
}
</mat-select>
</mat-form-field>
<h2>Add Filters</h2>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SidePanelComponent } from './side-panel.component';
describe('SidePanelComponent', () => {
let component: SidePanelComponent;
let fixture: ComponentFixture<SidePanelComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SidePanelComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SidePanelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,34 @@
import { Component, output, Signal, signal } from '@angular/core';
import { MatSelect } from '@angular/material/select';
import { MatFormField } from '@angular/material/select';
import { MatLabel } from '@angular/material/select';
import { MatOption } from '@angular/material/select';
import { Session } from '../../core/model/session';
import { SessionDataService } from '../../core/remote-data.service';
@Component({
selector: 'app-side-panel',
imports: [MatSelect, MatFormField, MatLabel, MatOption],
templateUrl: './side-panel.component.html',
styleUrl: './side-panel.component.scss',
})
export class SidePanelComponent {
selectedSession: Session | null = null;
sessions: Session[] = [];
readonly sessionChanged = output<Session | null>();
constructor(private sessionDataService: SessionDataService) {}
ngOnInit(): void {
this.sessionDataService.getSessions().subscribe((sessions) => {
this.sessions = sessions;
});
}
onSessionChange(session: Session | null): void {
this.selectedSession = session;
this.sessionChanged.emit(session);
}
}