This is an automated email from the ASF dual-hosted git repository.

zhuzh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git

commit 9b7c95c8e0a3f49bd583a2db6d65d64c8175e7e4
Author: Yi Zhang <[email protected]>
AuthorDate: Wed Dec 10 15:30:51 2025 +0800

    [FLINK-38977][ui] Expose exceptions for applications
---
 .../application-exception.ts}                      |  24 ++---
 .../web-dashboard/src/app/interfaces/public-api.ts |   1 +
 .../app/pages/application/application.config.ts    |   5 +-
 .../application-exceptions.component.html          |  30 ++++++
 .../application-exceptions.component.less}         |  64 ++++++++----
 .../exceptions/application-exceptions.component.ts | 112 +++++++++++++++++++++
 .../modules/completed-application/routes.ts        |  10 ++
 .../modules/running-application/routes.ts          |  10 ++
 .../src/app/services/application.service.ts        |   7 ++
 9 files changed, 231 insertions(+), 32 deletions(-)

diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
 b/flink-runtime-web/web-dashboard/src/app/interfaces/application-exception.ts
similarity index 60%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
copy to 
flink-runtime-web/web-dashboard/src/app/interfaces/application-exception.ts
index e5a659e429c..fa181919b3f 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/interfaces/application-exception.ts
@@ -16,17 +16,17 @@
  * limitations under the License.
  */
 
-import { InjectionToken } from '@angular/core';
+export interface ApplicationExceptions {
+  exceptionHistory: ApplicationExceptionHistory;
+}
 
-import { ModuleConfig } from '@flink-runtime-web/core/module-config';
+export interface ApplicationExceptionHistory {
+  entries: ApplicationExceptionInfo[];
+}
 
-export type ApplicationModuleConfig = Pick<ModuleConfig, 'routerTabs'>;
-
-export const APPLICATION_MODULE_DEFAULT_CONFIG: 
Required<ApplicationModuleConfig> = {
-  routerTabs: [{ title: 'Overview', path: 'overview' }]
-};
-
-export const APPLICATION_MODULE_CONFIG = new 
InjectionToken<ApplicationModuleConfig>('application-module-config', {
-  providedIn: 'root',
-  factory: () => APPLICATION_MODULE_DEFAULT_CONFIG
-});
+export interface ApplicationExceptionInfo {
+  exceptionName: string;
+  stacktrace: string;
+  timestamp: number;
+  jobId?: string;
+}
diff --git a/flink-runtime-web/web-dashboard/src/app/interfaces/public-api.ts 
b/flink-runtime-web/web-dashboard/src/app/interfaces/public-api.ts
index 83451266fe2..38d072557e1 100644
--- a/flink-runtime-web/web-dashboard/src/app/interfaces/public-api.ts
+++ b/flink-runtime-web/web-dashboard/src/app/interfaces/public-api.ts
@@ -35,3 +35,4 @@ export * from './job-manager';
 export * from './job-metrics';
 export * from './application-overview';
 export * from './application-detail';
+export * from './application-exception';
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
index e5a659e429c..f24371a48e4 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
@@ -23,7 +23,10 @@ import { ModuleConfig } from 
'@flink-runtime-web/core/module-config';
 export type ApplicationModuleConfig = Pick<ModuleConfig, 'routerTabs'>;
 
 export const APPLICATION_MODULE_DEFAULT_CONFIG: 
Required<ApplicationModuleConfig> = {
-  routerTabs: [{ title: 'Overview', path: 'overview' }]
+  routerTabs: [
+    { title: 'Overview', path: 'overview' },
+    { title: 'Exceptions', path: 'exceptions' }
+  ]
 };
 
 export const APPLICATION_MODULE_CONFIG = new 
InjectionToken<ApplicationModuleConfig>('application-module-config', {
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.html
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.html
new file mode 100644
index 00000000000..0cbb8a849bc
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.html
@@ -0,0 +1,30 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<nz-tabs [nzSize]="'small'" [nzAnimated]="{ inkBar: true, tabPane: false }">
+  <nz-tab nzTitle="Root Exception">
+    <ng-template nz-tab>
+      <nz-code-editor
+        flinkAutoResize
+        [nzLoading]="isLoading"
+        [ngModel]="rootException"
+        [nzEditorOption]="editorOptions"
+      ></nz-code-editor>
+    </ng-template>
+  </nz-tab>
+</nz-tabs>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.less
similarity index 58%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.less
index 6daac893d51..e7fc8121c5b 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.less
@@ -16,23 +16,49 @@
  * limitations under the License.
  */
 
-import { Routes } from '@angular/router';
-
-import { ApplicationDetailComponent } from 
'@flink-runtime-web/pages/application/application-detail/application-detail.component';
-
-export const COMPLETED_APPLICATION_ROUES: Routes = [
-  {
-    path: '',
-    component: ApplicationDetailComponent,
-    children: [
-      {
-        path: 'overview',
-        loadChildren: () => import('../../overview/routes').then(m => 
m.APPLICATION_OVERVIEW_ROUTES),
-        data: {
-          path: 'overview'
-        }
-      },
-      { path: '**', redirectTo: 'overview', pathMatch: 'full' }
-    ]
+@import "theme";
+
+:host {
+  display: flex;
+  flex: 1;
+  flex-flow: column nowrap;
+
+  ::ng-deep {
+    .ant-tabs-nav-list {
+      padding: 4px 16px;
+    }
+  }
+
+  nz-tabs {
+    position: relative;
+    flex: 1;
+  }
+
+  nz-code-editor {
+    position: relative;
+    height: calc(~"100vh - 310px");
+
+    &.subtask {
+      position: relative;
+      top: 0;
+      height: 300px;
+    }
+  }
+
+  nz-table {
+    margin-top: -@margin-md;
+  }
+
+  nz-tag {
+    margin-bottom: 5px;
+  }
+
+  .expand-td {
+    display: block;
+    padding: 0 !important;
+  }
+
+  .exception-select {
+    width: 300px;
   }
-];
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.ts
new file mode 100644
index 00000000000..896203a9c1b
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/exceptions/application-exceptions.component.ts
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { formatDate } from '@angular/common';
+import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, 
OnDestroy } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { Subject } from 'rxjs';
+import { distinctUntilChanged, mergeMap, takeUntil, tap } from 
'rxjs/operators';
+
+import { AutoResizeDirective } from 
'@flink-runtime-web/components/editor/auto-resize.directive';
+import { flinkEditorOptions } from 
'@flink-runtime-web/components/editor/editor-config';
+import { ExceptionInfo } from '@flink-runtime-web/interfaces';
+import { ApplicationService } from '@flink-runtime-web/services';
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzCodeEditorModule, EditorOptions } from 'ng-zorro-antd/code-editor';
+import { NzIconModule } from 'ng-zorro-antd/icon';
+import { NzSelectModule } from 'ng-zorro-antd/select';
+import { NzTableModule } from 'ng-zorro-antd/table';
+import { NzTabsModule } from 'ng-zorro-antd/tabs';
+import { NzTagModule } from 'ng-zorro-antd/tag';
+import { NzTooltipModule } from 'ng-zorro-antd/tooltip';
+
+import { ApplicationLocalService } from '../application-local.service';
+
+@Component({
+  selector: 'flink-application-exceptions',
+  templateUrl: './application-exceptions.component.html',
+  styleUrls: ['./application-exceptions.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  imports: [
+    NzTabsModule,
+    NzCodeEditorModule,
+    AutoResizeDirective,
+    NzTableModule,
+    NzSelectModule,
+    NzTooltipModule,
+    FormsModule,
+    NzIconModule,
+    NzButtonModule,
+    NzTagModule
+  ]
+})
+export class ApplicationExceptionsComponent implements OnInit, OnDestroy {
+  public readonly trackByTimestamp = (_: number, node: ExceptionInfo): number 
=> node.timestamp;
+
+  public rootException = '';
+  public isLoading = false;
+  public total = 0;
+  public editorOptions: EditorOptions = flinkEditorOptions;
+
+  private readonly destroy$ = new Subject<void>();
+
+  constructor(
+    private readonly applicationService: ApplicationService,
+    private readonly applicationLocalService: ApplicationLocalService,
+    private readonly cdr: ChangeDetectorRef
+  ) {}
+
+  public ngOnInit(): void {
+    this.loadMore();
+  }
+
+  public ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  public loadMore(): void {
+    this.isLoading = true;
+    this.applicationLocalService
+      .applicationDetailChanges()
+      .pipe(
+        distinctUntilChanged((pre, next) => pre.id === next.id),
+        mergeMap(application => 
this.applicationService.loadExceptions(application.id)),
+        tap(() => {
+          this.isLoading = false;
+          this.cdr.markForCheck();
+        }),
+        takeUntil(this.destroy$)
+      )
+      .subscribe(data => {
+        // @ts-ignore
+        const exceptionHistory = data.exceptionHistory;
+        if (exceptionHistory.entries.length > 0) {
+          const exceptionInfo = exceptionHistory.entries[0];
+          let exceptionMessage = `${formatDate(exceptionInfo.timestamp, 
'yyyy-MM-dd HH:mm:ss', 'en')}\n`;
+          if (exceptionInfo.jobId) {
+            exceptionMessage += `Related Job: ${exceptionInfo.jobId}\n`;
+          }
+          exceptionMessage += exceptionInfo.stacktrace;
+          this.rootException = exceptionMessage;
+        } else {
+          this.rootException = 'No Root Exception';
+        }
+      });
+  }
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
index 6daac893d51..3824d1ba3c0 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
@@ -32,6 +32,16 @@ export const COMPLETED_APPLICATION_ROUES: Routes = [
           path: 'overview'
         }
       },
+      {
+        path: 'exceptions',
+        loadComponent: () =>
+          
import('@flink-runtime-web/pages/application/exceptions/application-exceptions.component').then(
+            m => m.ApplicationExceptionsComponent
+          ),
+        data: {
+          path: 'exceptions'
+        }
+      },
       { path: '**', redirectTo: 'overview', pathMatch: 'full' }
     ]
   }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
index e070a02b524..d1675386d5e 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
@@ -34,6 +34,16 @@ export const RUNNING_APPLICATION_ROUTES: Routes = [
           path: 'overview'
         }
       },
+      {
+        path: 'exceptions',
+        loadComponent: () =>
+          
import('@flink-runtime-web/pages/application/exceptions/application-exceptions.component').then(
+            m => m.ApplicationExceptionsComponent
+          ),
+        data: {
+          path: 'exceptions'
+        }
+      },
       { path: '**', redirectTo: 'overview', pathMatch: 'full' }
     ]
   }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/services/application.service.ts 
b/flink-runtime-web/web-dashboard/src/app/services/application.service.ts
index d9b7855556b..9a2394f3835 100644
--- a/flink-runtime-web/web-dashboard/src/app/services/application.service.ts
+++ b/flink-runtime-web/web-dashboard/src/app/services/application.service.ts
@@ -28,6 +28,7 @@ import {
   JobStatus,
   TaskStatus
 } from '@flink-runtime-web/interfaces';
+import { ApplicationExceptions } from 
'@flink-runtime-web/interfaces/application-exception';
 
 import { ConfigService } from './config.service';
 
@@ -92,4 +93,10 @@ export class ApplicationService {
       catchError(() => EMPTY)
     );
   }
+
+  public loadExceptions(applicationId: string): 
Observable<ApplicationExceptions> {
+    return this.httpClient.get<ApplicationExceptions>(
+      `${this.configService.BASE_URL}/applications/${applicationId}/exceptions`
+    );
+  }
 }

Reply via email to