Skip to content

Commit ea4ffb1

Browse files
committed
fix(monitor): show initialization failures and support retry
1 parent 4f67aec commit ea4ffb1

6 files changed

Lines changed: 293 additions & 120 deletions

File tree

‎src/app/pages/monitor/monitor.component.html‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@
3535
</ul>
3636
</div>
3737
<div #contentWindow style="width: 100%; height: calc(100% - 30px)">
38+
<div class="monitor-state" *ngIf="loading" role="status" aria-live="polite">
39+
<p>{{ 'Loading monitor…' | translate }}</p>
40+
<p>{{ loadingStage | translate }}</p>
41+
</div>
42+
<div class="monitor-state" *ngIf="errorMessage" role="alert">
43+
<h3>{{ 'Unable to load monitor' | translate }}</h3>
44+
<p>{{ loadingStage | translate }}:{{ errorMessage | translate }}</p>
45+
<p *ngIf="loginRequired">{{ 'Sign in in the new tab, then return here and click Retry.' | translate }}</p>
46+
<div class="monitor-actions">
47+
<a *ngIf="loginRequired" nz-button [href]="loginUrl" target="_blank" rel="noopener noreferrer">
48+
{{ 'Sign in again' | translate }}
49+
</a>
50+
<button nz-button nzType="primary" (click)="loadMonitor()">{{ 'Retry' | translate }}</button>
51+
</div>
52+
</div>
3853
<iframe
3954
#terminal
4055
[src]="iframeURL | safeUrl"

‎src/app/pages/monitor/monitor.component.scss‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,25 @@ iframe {
6060
border-width: 0;
6161
background: var(--el-editor-bg-color);
6262
}
63+
64+
.monitor-state {
65+
display: flex;
66+
flex-direction: column;
67+
justify-content: center;
68+
align-items: center;
69+
min-height: 240px;
70+
height: 100%;
71+
padding: 24px;
72+
text-align: center;
73+
color: var(--el-text-color-light);
74+
background: var(--el-editor-bg-color);
75+
76+
h3 {
77+
color: inherit;
78+
}
79+
}
80+
81+
.monitor-actions {
82+
display: flex;
83+
gap: 12px;
84+
}

‎src/app/pages/monitor/monitor.component.ts‎

Lines changed: 140 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
1+
import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
22
import {HttpService, I18nService, SettingService} from '@app/services';
33
import {ActivatedRoute} from '@angular/router';
44
import {Asset, Session, Ticket, User} from '@app/model';
55
import {NzNotificationService} from 'ng-zorro-antd/notification';
66
import {getWaterMarkContent} from '@app/utils/common';
77
import {joinEndpointUrl} from '@app/utils/path';
8+
import {combineLatest, firstValueFrom, from, Subject, TimeoutError} from 'rxjs';
9+
import {takeUntil, timeout} from 'rxjs/operators';
810

911
@Component({
1012
standalone: false,
1113
selector: 'pages-monitor',
1214
templateUrl: 'monitor.component.html',
1315
styleUrls: ['monitor.component.scss']
1416
})
15-
export class PagesMonitorComponent implements OnInit {
16-
@ViewChild('contentWindow', {static: false}) windowRef: ElementRef;
17+
export class PagesMonitorComponent implements OnInit, OnDestroy {
18+
@ViewChild('contentWindow', {static: true}) windowRef: ElementRef;
1719
iframeURL: string;
1820
sessionDetail: Session = null;
1921
sessionID: string;
@@ -22,76 +24,158 @@ export class PagesMonitorComponent implements OnInit {
2224
ticketDetail: Ticket;
2325
supportedLock: boolean = false;
2426
user: User;
27+
loading = false;
28+
errorMessage = '';
29+
loginRequired = false;
30+
loadingStage = '';
31+
private readonly destroy$ = new Subject<void>();
32+
private readonly loadCancelled$ = new Subject<void>();
33+
private requestController: AbortController;
34+
private loadSequence = 0;
35+
private readonly requestTimeout = 15000;
2536

2637
constructor(private _settingSvc: SettingService,
2738
private _http: HttpService,
2839
private _route: ActivatedRoute,
2940
private _toastr: NzNotificationService,
30-
private _i18n: I18nService) {
31-
this.getCurrentUser();
41+
private _i18n: I18nService) {}
42+
43+
ngOnInit() {
44+
combineLatest([this._route.params, this._route.queryParams])
45+
.pipe(takeUntil(this.destroy$))
46+
.subscribe(([params, queryParams]) => {
47+
this.sessionID = params['sid'];
48+
this.ticketID = queryParams['ticket_id'];
49+
this.loadMonitor();
50+
});
3251
}
3352

34-
getCurrentUser() {
35-
this._http.getUserProfile().then(user => {
36-
this.user = user;
37-
});
53+
ngOnDestroy() {
54+
this.loadSequence++;
55+
this.requestController?.abort();
56+
this.loadCancelled$.next();
57+
this.loadCancelled$.complete();
58+
this.destroy$.next();
59+
this.destroy$.complete();
3860
}
3961

40-
async ngOnInit() {
41-
this._route.params.subscribe(params => {
42-
this.sessionID = params['sid'];
43-
this.generateMonitorURL().then(async () => {
44-
const sessionObj = this.sessionDetail;
45-
let asset: any = null;
46-
try {
47-
asset = await this._http.getAssetDetail(sessionObj.asset_id).toPromise();
48-
} catch (error) {
49-
asset = new Asset();
50-
}
51-
let sessionUser: User = null;
52-
try {
53-
sessionUser = await this._http.getUserDetail(sessionObj.user_id);
54-
} catch (error) {
55-
sessionUser = new User();
62+
get loginUrl(): string {
63+
return this._http.getLoginUrl();
64+
}
65+
66+
private waitFor<T>(request: Promise<T>, milliseconds = this.requestTimeout): Promise<T> {
67+
return firstValueFrom(from(request).pipe(timeout(milliseconds), takeUntil(this.loadCancelled$)));
68+
}
69+
70+
private async optionalWatermarkDetail<T>(request: Promise<T>, fallback: T): Promise<T> {
71+
try {
72+
return await this.waitFor(request, 5000) || fallback;
73+
} catch (error) {
74+
if (error.status === 401) {
75+
throw error;
76+
}
77+
// Keep the existing fallback for unavailable asset/session-user metadata.
78+
return fallback;
79+
}
80+
}
81+
82+
async loadMonitor() {
83+
const sequence = ++this.loadSequence;
84+
this.requestController?.abort();
85+
this.loadCancelled$.next();
86+
const controller = new AbortController();
87+
this.requestController = controller;
88+
const control = {signal: controller.signal, timeout: this.requestTimeout};
89+
this.loading = true;
90+
this.errorMessage = '';
91+
this.loginRequired = false;
92+
this.iframeURL = null;
93+
this.sessionDetail = null;
94+
this.ticketDetail = null;
95+
this.supportedLock = false;
96+
this.loadingStage = 'Session details';
97+
try {
98+
if (!this.sessionID) {
99+
throw new Error('Invalid session ID');
100+
}
101+
const session = await this.waitFor(this._http.getSessionDetail(this.sessionID, control));
102+
if (sequence !== this.loadSequence) {
103+
return;
104+
}
105+
if (!session?.terminal?.type || !session.type?.value) {
106+
throw new Error('Invalid session response');
107+
}
108+
this.sessionDetail = session;
109+
this.loadingStage = 'Monitor initialization';
110+
const protocol = window.location.protocol.replace(':', '');
111+
const data = {assetId: '', sessionId: this.sessionID, token: ''};
112+
const [user, endpoint, , ticket] = await this.waitFor(Promise.all([
113+
this._http.getUserProfile(control),
114+
this._http.getSmartEndpoint(data, protocol, control),
115+
this._settingSvc.init(control),
116+
this.ticketID ? this._http.getTicketDetail(this.ticketID, control) : Promise.resolve(null)
117+
]));
118+
if (sequence !== this.loadSequence) {
119+
return;
120+
}
121+
if (!user?.username || !endpoint) {
122+
throw new Error('Invalid monitor response');
123+
}
124+
const iframeURL = this.generateMonitorURL(endpoint.getUrl(), session.terminal.type);
125+
this.user = user;
126+
this.ticketDetail = ticket;
127+
if (this._settingSvc.globalSetting.SECURITY_WATERMARK_ENABLED) {
128+
this.loadingStage = 'Watermark initialization';
129+
const detailControl = {...control, timeout: 5000};
130+
const [asset, sessionUser] = await Promise.all([
131+
this.optionalWatermarkDetail(this._http.getAssetDetail(session.asset_id, detailControl).toPromise(), new Asset()),
132+
this.optionalWatermarkDetail(this._http.getUserDetail(session.user_id, detailControl), new User())
133+
]);
134+
if (sequence !== this.loadSequence) {
135+
return;
56136
}
57-
const auditorUser = `${this._i18n.instant('Viewer')}: ${this.user.name}(${this.user.username})`;
137+
const auditorUser = `${this._i18n.instant('Viewer')}: ${user.name}(${user.username})`;
58138
const sessionContent = getWaterMarkContent(sessionUser, asset, this._settingSvc);
59-
const content = `${auditorUser}\n${sessionContent}`;
60-
this._settingSvc.createWaterMarkIfNeed(
61-
this.windowRef.nativeElement, content);
62-
});
63-
});
64-
this._route.queryParams.subscribe(params => {
65-
this.ticketID = params['ticket_id'];
66-
if (this.ticketID) {
67-
this._http.getTicketDetail(this.ticketID).then((res) => {
68-
this.ticketDetail = res;
69-
});
139+
await this.waitFor(this._settingSvc.createWaterMarkIfNeed(
140+
this.windowRef.nativeElement, `${auditorUser}\n${sessionContent}`, control));
141+
if (sequence !== this.loadSequence) {
142+
return;
143+
}
144+
}
145+
this.isPaused = session.is_locked;
146+
this.supportedLock = ['koko', 'lion', 'chen'].includes(session.terminal.type) && session.type.value === 'normal';
147+
this.iframeURL = iframeURL;
148+
} catch (error) {
149+
if (sequence !== this.loadSequence) {
150+
return;
70151
}
71-
});
152+
this.loginRequired = error.status === 401;
153+
const messages = {
154+
0: 'Unable to reach the server. Check your network and retry.',
155+
401: 'Your login has expired or you are not signed in.',
156+
403: 'You do not have permission to access the requested resource.',
157+
404: 'The requested resource does not exist or is no longer available.'
158+
};
159+
this.errorMessage = error instanceof TimeoutError ? 'The request timed out. Please retry.' :
160+
messages[error.status] || 'Initialization failed. Please retry or contact the administrator.';
161+
console.warn('Monitor initialization failed', {stage: this.loadingStage, status: error.status});
162+
} finally {
163+
// Cancel every outstanding request in this attempt, including siblings of a failed request.
164+
controller.abort();
165+
if (sequence === this.loadSequence) {
166+
this.loading = false;
167+
}
168+
}
72169
}
73170

74-
async generateMonitorURL() {
75-
this.sessionDetail = await this._http.getSessionDetail(this.sessionID);
76-
const supportedType = ['koko', 'lion', 'chen'];
77-
const isSupportComponent = supportedType.includes(this.sessionDetail.terminal.type);
78-
const isNormalSession = this.sessionDetail.type.value === 'normal';
79-
this.supportedLock = isSupportComponent && isNormalSession;
80-
this.isPaused = this.sessionDetail.is_locked;
81-
const protocol = window.location.protocol.replace(':', '');
82-
const data = {'assetId': '', 'appId': '', 'sessionId': this.sessionID, 'token': ''};
83-
const smartEndpoint = await this._http.getSmartEndpoint(data, protocol);
84-
const baseUrl = smartEndpoint.getUrl();
85-
const terminal_type = this.sessionDetail.terminal.type;
86-
switch (terminal_type) {
171+
private generateMonitorURL(baseUrl: string, terminalType: string): string {
172+
switch (terminalType) {
87173
case 'razor':
88-
this.iframeURL = joinEndpointUrl(baseUrl, `/razor/monitor/${this.sessionID}/`);
89-
break;
174+
return joinEndpointUrl(baseUrl, `/razor/monitor/${this.sessionID}/`);
90175
case 'lion':
91-
this.iframeURL = joinEndpointUrl(baseUrl, `/lion/monitor/?session=${this.sessionID}`);
92-
break;
176+
return joinEndpointUrl(baseUrl, `/lion/monitor/?session=${this.sessionID}`);
93177
default:
94-
this.iframeURL = joinEndpointUrl(baseUrl, `/koko/monitor/${this.sessionID}/`);
178+
return joinEndpointUrl(baseUrl, `/koko/monitor/${this.sessionID}/`);
95179
}
96180
}
97181

‎src/app/services/app.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Account, Asset, AuthInfo, ConnectData, Endpoint, Organization, View } f
1010
import * as CryptoJS from 'crypto-js';
1111
import { OrganizationService } from './organization';
1212
import { I18nService } from '@app/services/i18n';
13-
import { getAppBasePath, getAppRoutePath, withSitePrefix } from '@app/utils/path';
13+
import { getAppRoutePath, withSitePrefix } from '@app/utils/path';
1414

1515
declare function unescape(s: string): string;
1616

@@ -95,6 +95,7 @@ export class AppService {
9595
await this._http.get(`/api/v1/users/profile/?fields_size=mini`).toPromise();
9696
status = 'ok';
9797
User.logined = true;
98+
this._http.resetLoginNotification();
9899
} catch (err) {
99100
status = 'error'; // 默认错误状态
100101
if (err.status === 401) {
@@ -115,12 +116,14 @@ export class AppService {
115116
const status = await this.getProfileStatus(recheck);
116117
if (['unauthorized', 'badrequest', 'error'].includes(status)) {
117118
clearInterval(this.checkIntervalId);
118-
const ok = confirm(this._i18n.instant(this.getErrorMsg(status)));
119-
if (ok && !this.newLoginHasOpen) {
120-
const loginUrl = new URL(withSitePrefix('/core/auth/login/'), window.location.origin);
121-
loginUrl.searchParams.set('next', getAppBasePath());
122-
window.open(loginUrl.toString(), '_blank');
123-
this.newLoginHasOpen = true;
119+
if (status === 'unauthorized') {
120+
this._http.notifyLoginRequired();
121+
} else if (!this.newLoginHasOpen) {
122+
const ok = confirm(this._i18n.instant(this.getErrorMsg(status)));
123+
if (ok) {
124+
window.open(this._http.getLoginUrl(), '_blank', 'noopener');
125+
this.newLoginHasOpen = true;
126+
}
124127
}
125128
setTimeout(() => this.doCheckProfile(true), 5000);
126129
this._logger.debug(`${status}, redirect to login`);

0 commit comments

Comments
 (0)