mirror of
https://github.com/bec-project/bec_atlas.git
synced 2025-07-13 22:51:49 +02:00
feat: add control side-panel
This commit is contained in:
7
frontend/bec_atlas/src/app/core/model/session.ts
Normal file
7
frontend/bec_atlas/src/app/core/model/session.ts
Normal 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
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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%;
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user