Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cdca935
init
Junjiequan Jun 9, 2026
3d151e4
feat(proposal-datasets): add real-time dataset table updates Add a "L…
Junjiequan Jun 15, 2026
18bb26b
rename tokenSub to connectionSub
Junjiequan Jun 16, 2026
90be823
minor css change
Junjiequan Jun 16, 2026
be20f52
minor css change and update tool-tip message for live update button
Junjiequan Jun 18, 2026
6c1300b
remove snack bar
Junjiequan Jun 19, 2026
bdcab3a
udpate payload type
Junjiequan Jun 22, 2026
54282ae
if payload proposalId do not match current proposal page do not fetch
Junjiequan Jun 22, 2026
92c5b23
extend row hightlight effect
Junjiequan Jun 23, 2026
ad84ee1
hightlight effect for multiple new items
Junjiequan Jun 23, 2026
405568a
increase css maximumError to 20kb
Junjiequan Jun 23, 2026
8057f97
lint fix
Junjiequan Jun 23, 2026
fb0a27d
update tooltip for toggle button
Junjiequan Jun 23, 2026
cd27713
fix lint
Junjiequan Jun 23, 2026
cdcaa82
apply sourcery comment
Junjiequan Jun 23, 2026
9758a0b
address copilot review
Junjiequan Jun 23, 2026
caac11c
added connection error icon && option to disable live update from config
Junjiequan Jun 24, 2026
b5c82a7
minor clean up
Junjiequan Jun 24, 2026
f619436
Merge branch 'master' into sse-integration
Junjiequan Jul 6, 2026
2e49e46
remove unused import
Junjiequan Jul 6, 2026
3dbd16f
Merge branch 'master' into sse-integration
Junjiequan Aug 19, 2026
95ee430
property name fix: realTimeEnabledChange to realTimeToggle
Junjiequan Aug 20, 2026
041a1b2
Open SSE stream with a short-lived ticket and added retry logic on error
Junjiequan Aug 20, 2026
1adfb79
added unit tests
Junjiequan Aug 20, 2026
a217b43
Merge branch 'master' into sse-integration
Junjiequan Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
{
"type": "anyComponentStyle",
"maximumWarning": "8kb",
"maximumError": "10kb"
"maximumError": "20kb"
}
],
"fileReplacements": [
Expand Down
2 changes: 1 addition & 1 deletion src/app/datasets/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
import { distinctUntilChanged, filter, map, take } from "rxjs/operators";
import { MatDialog } from "@angular/material/dialog";
import { MatSidenav } from "@angular/material/sidenav";
import { AddDatasetDialogComponent } from "datasets/add-dataset-dialog/add-dataset-dialog.component";
import { combineLatest, Subscription, lastValueFrom } from "rxjs";
import {
selectProfile,
Expand Down Expand Up @@ -58,6 +57,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
private readyToFetch$ = this.store
.select(selectHasPrefilledFilters)
.pipe(filter((has) => has));

loggedIn$ = this.store.select(selectIsLoggedIn);
selectedSets$ = this.store.select(selectSelectedDatasets);
selectColumns$ = this.store.select(selectColumns);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@
class="mat-elevation-z2 proposal-dataset-table"
[emptyMessage]="'No datasets available'"
[emptyIcon]="'folder'"
[showRealTimeToggle]="isLoggedIn$ | async"
[latestUpdatedId]="latestUpdatedId$ | async"
[realTimeEnabled]="realTimeEnabled"
(realTimeEnabledChange)="onRealTimeToggle($event)"
>
</dynamic-mat-table>
138 changes: 96 additions & 42 deletions src/app/proposals/proposal-datasets/proposal-datasets.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { ActivatedRoute, Router } from "@angular/router";
import { Store } from "@ngrx/store";
import { OutputDatasetObsoleteDto } from "@scicatproject/scicat-sdk-ts-angular";
import { AppConfigService } from "app-config.service";
import { BehaviorSubject, lastValueFrom, Subscription, take } from "rxjs";
import {
BehaviorSubject,
filter,
lastValueFrom,
Subscription,
take,
} from "rxjs";
import { PrintConfig } from "shared/modules/dynamic-material-table/models/print-config.model";
import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model";
import {
Expand All @@ -28,7 +34,11 @@ import { DatasetsListService } from "shared/services/datasets-list.service";
import { TableConfigService } from "shared/services/table-config.service";
import { fetchProposalDatasetsAction } from "state-management/actions/proposals.actions";
import { selectViewProposalPageViewModel } from "state-management/selectors/proposals.selectors";
import { selectColumnsWithHasFetchedSettings } from "state-management/selectors/user.selectors";
import {
selectColumnsWithHasFetchedSettings,
selectIsLoggedIn,
} from "state-management/selectors/user.selectors";
import { EventsService } from "shared/events.service";

export interface TableData {
pid: string;
Expand All @@ -47,16 +57,20 @@ export interface TableData {
standalone: false,
})
export class ProposalDatasetsComponent implements OnInit, OnDestroy {
isLoggedIn$ = this.store.select(selectIsLoggedIn);
proposalDatasets$ = this.store.select(selectViewProposalPageViewModel);
latestUpdatedId$ = this.eventsService.latestUpdatedId$;

subscription: Subscription;
subscriptions: Subscription[] = [];
@Input() proposalId: string;

appConfig = this.appConfigService.getConfig();
selectColumnsWithFetchedSettings$ = this.store.select(
selectColumnsWithHasFetchedSettings,
);

realTimeEnabled = false;

tableName = "proposalDatasetsTable";

columns: TableField<any>[];
Expand All @@ -75,9 +89,6 @@ export class ProposalDatasetsComponent implements OnInit, OnDestroy {

showNoData = true;

//dataSource: BehaviorSubject<TableData[]> = new BehaviorSubject<TableData[]>(
// [],
//);
dataSource: BehaviorSubject<OutputDatasetObsoleteDto[]> = new BehaviorSubject<
OutputDatasetObsoleteDto[]
>([]);
Expand Down Expand Up @@ -127,9 +138,31 @@ export class ProposalDatasetsComponent implements OnInit, OnDestroy {
private store: Store,
private tableConfigService: TableConfigService,
private datasetsListService: DatasetsListService,
private eventsService: EventsService,
) {}

ngOnInit(): void {
this.subscriptions.push(
this.eventsService.message$
.pipe(
filter((payload) => {
return payload.type === "Dataset.created";
}),
)
.subscribe((payload: Record<string, any>) => {
if (!payload.data.proposalIds.includes(this.proposalId)) return;
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
this.store.dispatch(
fetchProposalDatasetsAction({
proposalId: this.proposalId,
skip: 0,
limit: this.defaultPageSize,
sortColumn: "creationTime",
sortDirection: "desc",
}),
);
}),
);

this.store.dispatch(
fetchProposalDatasetsAction({
proposalId: this.proposalId,
Expand All @@ -138,44 +171,46 @@ export class ProposalDatasetsComponent implements OnInit, OnDestroy {
}),
);

this.subscription = this.proposalDatasets$.subscribe(async (data) => {
this.dataSource.next(data.datasets);
this.pending = false;
this.subscriptions.push(
this.proposalDatasets$.subscribe(async (data) => {
this.dataSource.next(data.datasets);
this.pending = false;

const defaultTableColumns = await lastValueFrom(
this.selectColumnsWithFetchedSettings$.pipe(take(1)),
);

const defaultConfigColumns =
this.appConfig?.defaultDatasetsListSettings?.columns;

const userTableConfigColumns =
this.datasetsListService.convertSavedDatasetColumns(
defaultTableColumns.columns,
const defaultTableColumns = await lastValueFrom(
this.selectColumnsWithFetchedSettings$.pipe(take(1)),
);

this.tableDefaultSettingsConfig.settingList[0].columnSetting =
this.datasetsListService.convertSavedDatasetColumns(
defaultConfigColumns as TableColumn[],
);

const tableSettingsConfig =
this.tableConfigService.getTableSettingsConfig(
this.tableName,
this.tableDefaultSettingsConfig,
userTableConfigColumns,
);
const paginationConfig = {
pageSizeOptions: [5, 10, 25, 100],
pageIndex: data.currentPage || 0,
pageSize: data.datasetsPerPage || this.defaultPageSize,
length: data.datasetCount,
};

if (tableSettingsConfig?.settingList.length) {
this.initTable(tableSettingsConfig, paginationConfig);
}
});
const defaultConfigColumns =
this.appConfig?.defaultDatasetsListSettings?.columns;

const userTableConfigColumns =
this.datasetsListService.convertSavedDatasetColumns(
defaultTableColumns.columns,
);

this.tableDefaultSettingsConfig.settingList[0].columnSetting =
this.datasetsListService.convertSavedDatasetColumns(
defaultConfigColumns as TableColumn[],
);

const tableSettingsConfig =
this.tableConfigService.getTableSettingsConfig(
this.tableName,
this.tableDefaultSettingsConfig,
userTableConfigColumns,
);
const paginationConfig = {
pageSizeOptions: [5, 10, 25, 100],
pageIndex: data.currentPage || 0,
pageSize: data.datasetsPerPage || this.defaultPageSize,
length: data.datasetCount,
};

if (tableSettingsConfig?.settingList.length) {
this.initTable(tableSettingsConfig, paginationConfig);
}
}),
);
}

initTable(
Expand All @@ -191,6 +226,24 @@ export class ProposalDatasetsComponent implements OnInit, OnDestroy {
this.pagination = paginationConfig;
}

onRealTimeToggle = (enabled: boolean) => {
this.realTimeEnabled = enabled;
if (enabled) {
this.eventsService.connect();
this.store.dispatch(
fetchProposalDatasetsAction({
proposalId: this.proposalId,
skip: 0,
limit: this.defaultPageSize,
sortColumn: "creationTime",
sortDirection: "desc",
}),
);
} else {
this.eventsService.disconnect();
}
};

formatTableData(datasets: OutputDatasetObsoleteDto[]): TableData[] {
let tableData: TableData[] = [];
if (datasets) {
Expand Down Expand Up @@ -269,6 +322,7 @@ export class ProposalDatasetsComponent implements OnInit, OnDestroy {
}

ngOnDestroy() {
this.subscription.unsubscribe();
this.eventsService.disconnect();
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
}
}
69 changes: 69 additions & 0 deletions src/app/shared/events.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable, NgZone } from "@angular/core";
import { Store } from "@ngrx/store";
import { fetchScicatTokenAction } from "state-management/actions/user.actions";
import { selectScicatToken } from "state-management/selectors/user.selectors";
import {
distinctUntilChanged,
map,
startWith,
Subject,
Subscription,
switchMap,
timer,
} from "rxjs";
Comment thread
Junjiequan marked this conversation as resolved.

@Injectable({ providedIn: "root" })
export class EventsService {
Comment thread
Junjiequan marked this conversation as resolved.
private eventSource: EventSource | null = null;
private connectionSub: Subscription | null = null;
private messageSubject = new Subject<Record<string, unknown>>();

message$ = this.messageSubject.asObservable();

latestUpdatedId$ = this.messageSubject.pipe(
map((m) => (m["data"] as { _id: string })._id),
Comment thread
Junjiequan marked this conversation as resolved.
);
Comment thread
Junjiequan marked this conversation as resolved.

constructor(
private ngZone: NgZone,
private store: Store,
) {}

connect() {
if (this.connectionSub) return;

this.store.dispatch(fetchScicatTokenAction());

this.connectionSub = this.store
.select(selectScicatToken)
.pipe(distinctUntilChanged())
.subscribe((token) => {
if (token) {
this.openConnection(token);
} else {
this.closeConnection();
}
});
}

disconnect() {
this.closeConnection();
this.connectionSub?.unsubscribe();
this.connectionSub = null;
}

private openConnection(token: string) {

Check failure on line 55 in src/app/shared/events.service.ts

View workflow job for this annotation

GitHub Actions / eslint

Member openConnection should be declared before all public instance method definitions
this.closeConnection();

this.eventSource = new EventSource(`/api/v3/events/stream?token=${token}`);
this.eventSource.onmessage = (event) => {
this.ngZone.run(() => this.messageSubject.next(JSON.parse(event.data)));
};
this.eventSource.onerror = () => this.closeConnection();
}
Comment thread
Junjiequan marked this conversation as resolved.
Outdated

private closeConnection() {

Check failure on line 65 in src/app/shared/events.service.ts

View workflow job for this annotation

GitHub Actions / eslint

Member closeConnection should be declared before all public instance method definitions
this.eventSource?.close();
this.eventSource = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export class TableCoreDirective<T extends TableRow> {
@Input() globalTextSearchPlaceholder = "Search...";
@Input() selectionIds = [];
@Input() disableBorder: boolean;
@Input() showRealTimeToggle = false;
@Input() realTimeEnabled = false;
@Output() realTimeEnabledChange = new EventEmitter<boolean>();
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
@Output() onTableEvent: EventEmitter<ITableEvent> = new EventEmitter();
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
class="table-header-controls"
[class.with-side-filter]="sideFilterCollapsed"
>
<div class="real-time-toggle-wrapper" *ngIf="showRealTimeToggle">
<mat-slide-toggle
[checked]="realTimeEnabled"
(change)="realTimeEnabledChange.emit($event.checked)"
matTooltip="When on, the table watches for new datasets and shows a notice when updates arrive."
matTooltipPosition="above"
data-cy="real-time-toggle"
>
Comment thread
Junjiequan marked this conversation as resolved.
Live updates
</mat-slide-toggle>
</div>
<div class="global-search-wrapper" *ngIf="showGlobalTextSearch">
<mat-form-field>
<mat-icon matPrefix style="color: gray" *ngIf="!globalTextSearch"
Expand Down Expand Up @@ -428,6 +439,7 @@
[class.row-selection]="
rowSelectionModel ? rowSelectionModel.isSelected(row) : false
"
[class.row-highlight]="highlighted.has(row._id)"
(contextmenu)="onContextMenu($event, null, row)"
>
</mat-row>
Expand Down
Loading
Loading