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 898b6bf70299ccc713d20a4a3d1d5dcc2c95de4f
Author: Yi Zhang <[email protected]>
AuthorDate: Mon Dec 8 15:06:48 2025 +0800

    [FLINK-38778][ui] Application Web UI
---
 .../web-dashboard/src/app/app.component.html       |  24 ++++
 .../application-badge.component.html}              |  16 +--
 .../application-badge.component.less}              |  10 +-
 .../application-badge.component.ts}                |  21 +++-
 .../application-list.component.html                |  59 ++++++++++
 .../application-list.component.less}               |   9 +-
 .../application-list/application-list.component.ts | 124 +++++++++++++++++++++
 .../jobs-badge/jobs-badge.component.html}          |  25 ++---
 .../jobs-badge/jobs-badge.component.less}          |  10 +-
 .../components/jobs-badge/jobs-badge.component.ts  |  43 +++++++
 .../application-detail.ts}                         |  26 ++++-
 .../application-overview.ts}                       |  31 +++++-
 .../web-dashboard/src/app/interfaces/public-api.ts |   2 +
 .../application-detail.component.html}             |  33 +++---
 .../application-detail.component.less}             |  32 +++++-
 .../application-detail.component.ts                |  82 ++++++++++++++
 .../status/application-status.component.html       |  78 +++++++++++++
 .../status/application-status.component.less}      |  25 ++++-
 .../status/application-status.component.ts         | 112 +++++++++++++++++++
 .../application-local.service.ts}                  |  20 +++-
 .../application.component.html}                    |  24 ++--
 .../application.component.less}                    |   8 +-
 .../app/pages/application/application.component.ts |  78 +++++++++++++
 .../application/application.config.ts}             |  22 ++--
 .../modules/completed-application/routes.ts}       |  28 +++--
 .../modules/running-application}/routes.ts         |  33 +++---
 .../running-application.guard.ts                   |  55 +++++++++
 .../overview/application-overview.component.html}  |   1 -
 .../overview/application-overview.component.less}  |   5 +-
 .../overview/application-overview.component.ts}    |  29 ++---
 .../overview/routes.ts}                            |  14 ++-
 .../src/app/pages/application/routes.ts            |  52 +++++++++
 .../src/app/pages/overview/overview.component.html |  20 ++--
 .../src/app/pages/overview/overview.component.less |   2 +-
 .../src/app/pages/overview/overview.component.ts   |  18 +--
 flink-runtime-web/web-dashboard/src/app/routes.ts  |   1 +
 .../src/app/services/application.service.ts        |  95 ++++++++++++++++
 .../web-dashboard/src/app/services/public-api.ts   |   1 +
 38 files changed, 1112 insertions(+), 156 deletions(-)

diff --git a/flink-runtime-web/web-dashboard/src/app/app.component.html 
b/flink-runtime-web/web-dashboard/src/app/app.component.html
index b14ff6f4693..1910b9ff91b 100644
--- a/flink-runtime-web/web-dashboard/src/app/app.component.html
+++ b/flink-runtime-web/web-dashboard/src/app/app.component.html
@@ -30,6 +30,30 @@
           <span>Overview</span>
         </span>
       </li>
+      <li nz-submenu [nzOpen]="true" nzTitle="Applications" nzIcon="bars">
+        <ul>
+          <li
+            nz-menu-item
+            routerLinkActive="ant-menu-item-selected"
+            [routerLink]="['/application/running']"
+          >
+            <span>
+              <i nz-icon nzType="play-circle"></i>
+              <span>Running Applications</span>
+            </span>
+          </li>
+          <li
+            nz-menu-item
+            routerLinkActive="ant-menu-item-selected"
+            [routerLink]="['/application/completed']"
+          >
+            <span>
+              <i nz-icon nzType="check-circle"></i>
+              <span>Completed Applications</span>
+            </span>
+          </li>
+        </ul>
+      </li>
       <li nz-submenu [nzOpen]="true" nzTitle="Jobs" nzIcon="bars">
         <ul>
           <li
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
 
b/flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.html
similarity index 67%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
copy to 
flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.html
index 70123c601be..24422a84adc 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.html
@@ -16,16 +16,6 @@
   ~ limitations under the License.
   -->
 
-<flink-overview-statistic></flink-overview-statistic>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Running Job List'"
-  [completed]="false"
-  (navigate)="navigateToJob(['job', 'running', $event.jid])"
-></flink-job-list>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Completed Job List'"
-  [completed]="true"
-  (navigate)="navigateToJob(['job', 'completed', $event.jid])"
-></flink-job-list>
+<div class="background">
+  <span [style.background]="backgroundColor(status)">{{ status }}</span>
+</div>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.less
similarity index 87%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.less
index 9d5ec33bbac..95f669ee66c 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.less
@@ -16,7 +16,11 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+span {
+  width: 30px;
+  padding: 3px 5px;
+  color: #fff;
+  font-weight: 700;
+  text-align: center;
+  cursor: default;
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.ts
similarity index 56%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.ts
index 9d5ec33bbac..df1dc7ea3d5 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/application-badge/application-badge.component.ts
@@ -16,7 +16,22 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+import { ColorKey, ConfigService } from '@flink-runtime-web/services';
+
+@Component({
+  selector: 'flink-application-badge',
+  templateUrl: './application-badge.component.html',
+  styleUrls: ['./application-badge.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ApplicationBadgeComponent {
+  @Input() public status: string;
+
+  constructor(private readonly configService: ConfigService) {}
+
+  public backgroundColor(status: string): string {
+    return this.configService.COLOR_MAP[status as ColorKey];
+  }
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.html
 
b/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.html
new file mode 100644
index 00000000000..5fccbf60b8f
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.html
@@ -0,0 +1,59 @@
+<!--
+  ~ 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-card [nzBordered]="false" [nzTitle]="title" [nzLoading]="isLoading">
+  <nz-table
+    #table
+    class="no-border"
+    [nzSize]="'small'"
+    [nzData]="listOfApplication"
+    [nzFrontPagination]="false"
+    [nzShowPagination]="false"
+  >
+    <thead>
+      <tr>
+        <th [nzSortFn]="sortApplicationNameFn" nzWidth="40%">Application 
Name</th>
+        <th [nzSortFn]="sortStartTimeFn" [nzSortOrder]="completed ? null : 
'descend'">
+          Start Time
+        </th>
+        <th [nzSortFn]="sortDurationFn">Duration</th>
+        <th [nzSortFn]="sortEndTimeFn" [nzSortOrder]="completed ? 'descend' : 
null">End Time</th>
+        <th>Jobs</th>
+        <th [nzSortFn]="sortStateFn">Status</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr
+        *ngFor="let application of table.data; trackBy: trackApplicationBy"
+        (click)="navigateToApplication(application)"
+        class="clickable"
+      >
+        <td>{{ application.name }}</td>
+        <td>{{ application['start-time'] | humanizeDate: 'yyyy-MM-dd 
HH:mm:ss.SSS' }}</td>
+        <td>{{ application.duration | humanizeDuration }}</td>
+        <td>{{ application['end-time'] | humanizeDate: 'yyyy-MM-dd 
HH:mm:ss.SSS' }}</td>
+        <td>
+          <flink-jobs-badge [jobs]="application.jobs"></flink-jobs-badge>
+        </td>
+        <td>
+          <flink-application-badge 
[status]="application.status"></flink-application-badge>
+        </td>
+      </tr>
+    </tbody>
+  </nz-table>
+</nz-card>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.less
similarity index 91%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.less
index 9d5ec33bbac..45c0bbd3c96 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.less
@@ -16,7 +16,10 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+:host {
+  ::ng-deep {
+    .ant-card-body {
+      padding: 24px 16px;
+    }
+  }
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.ts
 
b/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.ts
new file mode 100644
index 00000000000..7e15f4dc4ae
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/application-list/application-list.component.ts
@@ -0,0 +1,124 @@
+/*
+ * 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 { NgForOf } from '@angular/common';
+import {
+  ChangeDetectionStrategy,
+  ChangeDetectorRef,
+  Component,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnDestroy,
+  OnInit,
+  Output,
+  SimpleChanges
+} from '@angular/core';
+import { Observable, Subject } from 'rxjs';
+import { mergeMap, takeUntil } from 'rxjs/operators';
+
+import { ApplicationBadgeComponent } from 
'@flink-runtime-web/components/application-badge/application-badge.component';
+import { HumanizeDatePipe } from 
'@flink-runtime-web/components/humanize-date.pipe';
+import { HumanizeDurationPipe } from 
'@flink-runtime-web/components/humanize-duration.pipe';
+import { JobsBadgeComponent } from 
'@flink-runtime-web/components/jobs-badge/jobs-badge.component';
+import { ApplicationItem } from '@flink-runtime-web/interfaces';
+import { ApplicationService, StatusService } from 
'@flink-runtime-web/services';
+import { NzCardModule } from 'ng-zorro-antd/card';
+import { NzTableModule } from 'ng-zorro-antd/table';
+
+@Component({
+  selector: 'flink-application-list',
+  templateUrl: './application-list.component.html',
+  styleUrls: ['./application-list.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  imports: [
+    NzCardModule,
+    NzTableModule,
+    JobsBadgeComponent,
+    ApplicationBadgeComponent,
+    NgForOf,
+    HumanizeDatePipe,
+    HumanizeDurationPipe
+  ]
+})
+export class ApplicationListComponent implements OnInit, OnDestroy, OnChanges {
+  listOfApplication: ApplicationItem[] = [];
+  isLoading = true;
+  destroy$ = new Subject<void>();
+  @Input() completed = false;
+  @Input() title: string;
+  @Input() applicationData$: Observable<ApplicationItem[]>;
+  @Output() navigate = new EventEmitter<ApplicationItem>();
+
+  sortApplicationNameFn = (pre: ApplicationItem, next: ApplicationItem): 
number => pre.name.localeCompare(next.name);
+  sortStartTimeFn = (pre: ApplicationItem, next: ApplicationItem): number => 
pre['start-time'] - next['start-time'];
+  sortDurationFn = (pre: ApplicationItem, next: ApplicationItem): number => 
pre.duration - next.duration;
+  sortEndTimeFn = (pre: ApplicationItem, next: ApplicationItem): number => 
pre['end-time'] - next['end-time'];
+  sortStateFn = (pre: ApplicationItem, next: ApplicationItem): number => 
pre.status.localeCompare(next.status);
+
+  trackApplicationBy(_: number, node: ApplicationItem): string {
+    return node.id;
+  }
+
+  navigateToApplication(application: ApplicationItem): void {
+    this.navigate.emit(application);
+  }
+
+  constructor(
+    private statusService: StatusService,
+    private applicationService: ApplicationService,
+    private cdr: ChangeDetectorRef
+  ) {}
+
+  ngOnInit(): void {
+    this.applicationData$ =
+      this.applicationData$ ||
+      this.statusService.refresh$.pipe(
+        takeUntil(this.destroy$),
+        mergeMap(() => this.applicationService.loadApplications())
+      );
+    this.applicationData$.subscribe(data => {
+      this.isLoading = false;
+      this.listOfApplication = data.filter(item => item.completed === 
this.completed);
+
+      // Apply default sorting based on completed status
+      if (this.completed) {
+        // Sort completed applications by end time (descending - most recent 
first)
+        this.listOfApplication.sort((a, b) => b['end-time'] - a['end-time']);
+      } else {
+        // Sort running applications by start time (descending - most recent 
first)
+        this.listOfApplication.sort((a, b) => b['start-time'] - 
a['start-time']);
+      }
+
+      this.cdr.markForCheck();
+    });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    const { completed } = changes;
+    if (completed) {
+      this.isLoading = true;
+      this.cdr.markForCheck();
+    }
+  }
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
 
b/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.html
similarity index 67%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
copy to 
flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.html
index 70123c601be..898e68c43df 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.html
@@ -16,16 +16,15 @@
   ~ limitations under the License.
   -->
 
-<flink-overview-statistic></flink-overview-statistic>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Running Job List'"
-  [completed]="false"
-  (navigate)="navigateToJob(['job', 'running', $event.jid])"
-></flink-job-list>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Completed Job List'"
-  [completed]="true"
-  (navigate)="navigateToJob(['job', 'completed', $event.jid])"
-></flink-job-list>
+<div class="background" *ngIf="jobs">
+  <ng-container *ngFor="let status of statusList">
+    <span
+      *ngIf="jobs[status]"
+      nz-tooltip
+      [nzTooltipTitle]="status"
+      [style.background]="colorMap[status]"
+    >
+      {{ jobs[status] }}
+    </span>
+  </ng-container>
+</div>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.less
similarity index 86%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.less
index 9d5ec33bbac..bcc836b91c1 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.less
@@ -16,7 +16,11 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+span {
+  min-width: 32px;
+  padding: 3px 5px;
+  color: #fff;
+  font-weight: 700;
+  text-align: center;
+  cursor: default;
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.ts
 
b/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.ts
new file mode 100644
index 00000000000..7d7788d168a
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/components/jobs-badge/jobs-badge.component.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { NgForOf, NgIf } from '@angular/common';
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+import { JobStatus } from '@flink-runtime-web/interfaces';
+import { ConfigService } from '@flink-runtime-web/services';
+import { NzTooltipModule } from 'ng-zorro-antd/tooltip';
+
+@Component({
+  selector: 'flink-jobs-badge',
+  templateUrl: './jobs-badge.component.html',
+  styleUrls: ['./jobs-badge.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  imports: [NgForOf, NgIf, NzTooltipModule]
+})
+export class JobsBadgeComponent {
+  @Input() jobs: JobStatus;
+  statusList = Object.keys(this.configService.COLOR_MAP);
+
+  constructor(private readonly configService: ConfigService) {}
+
+  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+  get colorMap() {
+    return this.configService.COLOR_MAP;
+  }
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 b/flink-runtime-web/web-dashboard/src/app/interfaces/application-detail.ts
similarity index 59%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to flink-runtime-web/web-dashboard/src/app/interfaces/application-detail.ts
index 9d5ec33bbac..e95136ae461 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ b/flink-runtime-web/web-dashboard/src/app/interfaces/application-detail.ts
@@ -16,7 +16,27 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+import { JobStatus } from '@flink-runtime-web/interfaces/application-overview';
+import { JobsItem } from '@flink-runtime-web/interfaces/job-overview';
+
+interface TimestampsStatus {
+  FINISHED: number;
+  FAILING: number;
+  CREATED: number;
+  CANCELLING: number;
+  FAILED: number;
+  CANCELED: number;
+  RUNNING: number;
+}
+
+export interface ApplicationDetail {
+  id: string;
+  name: string;
+  status: string;
+  'start-time': number;
+  'end-time': number;
+  duration: number;
+  timestamps: TimestampsStatus;
+  jobs: JobsItem[];
+  'status-counts'?: JobStatus;
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 b/flink-runtime-web/web-dashboard/src/app/interfaces/application-overview.ts
similarity index 59%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/interfaces/application-overview.ts
index 9d5ec33bbac..5e24c0e43df 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ b/flink-runtime-web/web-dashboard/src/app/interfaces/application-overview.ts
@@ -16,7 +16,32 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+export interface ApplicationOverview {
+  applications: ApplicationItem[];
+}
+
+export interface ApplicationItem {
+  id: string;
+  name: string;
+  status: string;
+  'start-time': number;
+  'end-time': number;
+  duration: number;
+  jobs: JobStatus;
+  completed?: boolean;
+}
+
+export interface JobStatus {
+  CANCELED: number;
+  CANCELING: number;
+  CREATED: number;
+  FAILED: number;
+  FAILING: number;
+  FINISHED: number;
+  RECONCILING: number;
+  RUNNING: number;
+  RESTARTING: number;
+  INITIALIZING: number;
+  SUSPENDED: number;
+  TOTAL: number;
 }
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 46aa79f35b5..83451266fe2 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
@@ -33,3 +33,5 @@ export * from './task-manager';
 export * from './job-accumulators';
 export * from './job-manager';
 export * from './job-metrics';
+export * from './application-overview';
+export * from './application-detail';
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.html
similarity index 58%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.html
index 70123c601be..b65ac6a9c7f 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.html
@@ -16,16 +16,23 @@
   ~ limitations under the License.
   -->
 
-<flink-overview-statistic></flink-overview-statistic>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Running Job List'"
-  [completed]="false"
-  (navigate)="navigateToJob(['job', 'running', $event.jid])"
-></flink-job-list>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Completed Job List'"
-  [completed]="true"
-  (navigate)="navigateToJob(['job', 'completed', $event.jid])"
-></flink-job-list>
+<ng-container *ngIf="!isError">
+  <flink-application-status [isLoading]="isLoading"></flink-application-status>
+  <div class="content">
+    <nz-skeleton [nzActive]="true" *ngIf="isLoading"></nz-skeleton>
+    <div class="router" *ngIf="!isLoading">
+      <router-outlet></router-outlet>
+    </div>
+  </div>
+</ng-container>
+
+<nz-alert
+  *ngIf="isError"
+  nzShowIcon
+  nzType="warning"
+  nzMessage="Application failed during initialization"
+  [nzDescription]="descriptionTemplateRef"
+></nz-alert>
+<ng-template #descriptionTemplateRef>
+  <pre>{{ errorDetails }}</pre>
+</ng-template>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.less
similarity index 60%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.less
index 9d5ec33bbac..55e415dff0d 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.less
@@ -16,7 +16,33 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+@import "theme";
+
+:host {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+
+  .content {
+    flex: 0 1 auto;
+    box-sizing: border-box;
+    margin-top: @margin-md;
+    padding: 0;
+    border: 1px solid @border-color-split;
+    border-radius: 2px;
+    background: @component-background;
+    list-style: none;
+    font-size: @font-size-base;
+    line-height: 1.5;
+
+    nz-skeleton {
+      padding: @padding-md @padding-lg;
+    }
+
+    .router {
+      display: flex;
+      flex: 1;
+      flex-flow: column nowrap;
+    }
+  }
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.ts
new file mode 100644
index 00000000000..91fa1c0a51b
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/application-detail.component.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { NgIf } from '@angular/common';
+import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, 
OnDestroy } from '@angular/core';
+import { ActivatedRoute, RouterOutlet } from '@angular/router';
+import { EMPTY, Subject } from 'rxjs';
+import { catchError, mergeMap, takeUntil, tap } from 'rxjs/operators';
+
+import { ApplicationStatusComponent } from 
'@flink-runtime-web/pages/application/application-detail/status/application-status.component';
+import { ApplicationLocalService } from 
'@flink-runtime-web/pages/application/application-local.service';
+import { ApplicationService, StatusService } from 
'@flink-runtime-web/services';
+import { NzAlertModule } from 'ng-zorro-antd/alert';
+import { NzSkeletonModule } from 'ng-zorro-antd/skeleton';
+
+@Component({
+  selector: 'flink-application-detail',
+  templateUrl: './application-detail.component.html',
+  styleUrls: ['./application-detail.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  imports: [NgIf, ApplicationStatusComponent, NzSkeletonModule, RouterOutlet, 
NzAlertModule]
+})
+export class ApplicationDetailComponent implements OnInit, OnDestroy {
+  isLoading = true;
+  isError = false;
+  errorDetails: string;
+
+  private readonly destroy$ = new Subject<void>();
+
+  constructor(
+    private readonly applicationService: ApplicationService,
+    private readonly applicationLocalService: ApplicationLocalService,
+    private readonly statusService: StatusService,
+    private activatedRoute: ActivatedRoute,
+    private readonly cdr: ChangeDetectorRef
+  ) {}
+
+  ngOnInit(): void {
+    this.statusService.refresh$
+      .pipe(
+        takeUntil(this.destroy$),
+        mergeMap(() =>
+          
this.applicationService.loadApplication(this.activatedRoute.snapshot.params['id']).pipe(
+            tap(application => {
+              this.applicationLocalService.setApplicationDetail(application);
+            }),
+            catchError(() => {
+              this.isError = true;
+              this.isLoading = false;
+              this.cdr.markForCheck();
+              return EMPTY;
+            })
+          )
+        )
+      )
+      .subscribe(() => {
+        this.isLoading = false;
+        this.isError = false;
+        this.cdr.markForCheck();
+      });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.html
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.html
new file mode 100644
index 00000000000..805bf9cefe7
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.html
@@ -0,0 +1,78 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-template #extraTpl>
+  <div class="operate-action">
+    <span *ngIf="statusTips">{{ statusTips }}</span>
+    <ng-container *ngIf="!statusTips">
+      <a
+        nz-popconfirm
+        nzPopconfirmTitle="Cancel Application?"
+        nzOkText="Yes"
+        nzCancelText="No"
+        (nzOnConfirm)="cancelApplication()"
+        *ngIf="
+          webCancelEnabled &&
+          (applicationDetail.status === 'RUNNING' || applicationDetail.status 
=== 'CREATED')
+        "
+      >
+        Cancel Application
+      </a>
+    </ng-container>
+  </div>
+</ng-template>
+
+<ng-container *ngIf="applicationDetail && !isLoading">
+  <nz-descriptions
+    [nzTitle]="applicationDetail.name"
+    [nzExtra]="extraTpl"
+    nzBordered
+    nzSize="small"
+    [nzColumn]="{ xxl: 3, xl: 3, lg: 2, md: 2, sm: 1, xs: 1 }"
+  >
+    <nz-descriptions-item nzTitle="Application ID">{{ applicationDetail.id 
}}</nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Application State">
+      <div class="status-wrapper">
+        <flink-application-badge 
[status]="applicationDetail.status"></flink-application-badge>
+        <nz-divider nzType="vertical"></nz-divider>
+        <flink-jobs-badge 
[jobs]="applicationDetail['status-counts']"></flink-jobs-badge>
+      </div>
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Actions">
+      <a
+        *ngIf="!isHistoryServer"
+        [routerLink]="['/job-manager', 'logs']"
+        [queryParamsHandling]="'preserve'"
+      >
+        Job Manager Log
+      </a>
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Start Time">
+      {{ applicationDetail['start-time'] | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="End Time" 
*ngIf="applicationDetail['end-time'] > -1">
+      {{ applicationDetail['end-time'] | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Duration">
+      {{ applicationDetail.duration | humanizeDuration }}
+    </nz-descriptions-item>
+  </nz-descriptions>
+
+  <flink-navigation [listOfNavigation]="listOfNavigation"></flink-navigation>
+</ng-container>
+<nz-skeleton [nzActive]="true" *ngIf="isLoading"></nz-skeleton>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.less
similarity index 68%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.less
index 9d5ec33bbac..776a1de5d9d 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.less
@@ -16,7 +16,26 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+@import "theme";
+
+:host {
+  display: flex;
+  flex-direction: column;
+  margin: -24px -24px 0;
+  padding: 16px 32px 0;
+  border-bottom: 1px solid @border-color-split;
+  background: @component-background;
+
+  nz-descriptions {
+    margin-bottom: @margin-xs;
+  }
+
+  .operate-action {
+    font-size: 24px;
+  }
+
+  .status-wrapper {
+    display: flex;
+    align-items: center;
+  }
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.component.ts
new file mode 100644
index 00000000000..07dcda59579
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-detail/status/application-status.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 { DatePipe, NgIf } from '@angular/common';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, 
OnDestroy, OnInit } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { merge, Subject } from 'rxjs';
+import { distinctUntilKeyChanged, takeUntil, tap } from 'rxjs/operators';
+
+import { ApplicationBadgeComponent } from 
'@flink-runtime-web/components/application-badge/application-badge.component';
+import { HumanizeDurationPipe } from 
'@flink-runtime-web/components/humanize-duration.pipe';
+import { JobsBadgeComponent } from 
'@flink-runtime-web/components/jobs-badge/jobs-badge.component';
+import { NavigationComponent } from 
'@flink-runtime-web/components/navigation/navigation.component';
+import { RouterTab } from '@flink-runtime-web/core/module-config';
+import { ApplicationDetail } from '@flink-runtime-web/interfaces';
+import { ApplicationLocalService } from 
'@flink-runtime-web/pages/application/application-local.service';
+import {
+  APPLICATION_MODULE_CONFIG,
+  APPLICATION_MODULE_DEFAULT_CONFIG,
+  ApplicationModuleConfig
+} from '@flink-runtime-web/pages/application/application.config';
+import { ApplicationService, StatusService } from 
'@flink-runtime-web/services';
+import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
+import { NzDividerModule } from 'ng-zorro-antd/divider';
+import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
+import { NzSkeletonModule } from 'ng-zorro-antd/skeleton';
+
+@Component({
+  selector: 'flink-application-status',
+  templateUrl: './application-status.component.html',
+  styleUrls: ['./application-status.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  imports: [
+    NgIf,
+    NzPopconfirmModule,
+    NzDescriptionsModule,
+    NzDividerModule,
+    ApplicationBadgeComponent,
+    JobsBadgeComponent,
+    DatePipe,
+    NavigationComponent,
+    NzSkeletonModule,
+    HumanizeDurationPipe,
+    RouterLink
+  ]
+})
+export class ApplicationStatusComponent implements OnInit, OnDestroy {
+  @Input() isLoading = true;
+  statusTips: string;
+  applicationDetail: ApplicationDetail;
+  readonly listOfNavigation: RouterTab[];
+
+  webCancelEnabled = this.statusService.configuration.features['web-cancel'];
+  isHistoryServer = this.statusService.configuration.features['web-history'];
+
+  private destroy$ = new Subject<void>();
+
+  constructor(
+    private readonly applicationService: ApplicationService,
+    private readonly applicationLocalService: ApplicationLocalService,
+    private readonly statusService: StatusService,
+    private readonly cdr: ChangeDetectorRef,
+    @Inject(APPLICATION_MODULE_CONFIG) readonly moduleConfig: 
ApplicationModuleConfig
+  ) {
+    this.listOfNavigation = moduleConfig.routerTabs || 
APPLICATION_MODULE_DEFAULT_CONFIG.routerTabs;
+  }
+
+  ngOnInit(): void {
+    const updateList$ = 
this.applicationLocalService.applicationDetailChanges().pipe(
+      tap(data => {
+        this.applicationDetail = data;
+        this.cdr.markForCheck();
+      })
+    );
+    const updateTip$ = 
this.applicationLocalService.applicationDetailChanges().pipe(
+      distinctUntilKeyChanged('status'),
+      tap(() => {
+        this.statusTips = '';
+        this.cdr.markForCheck();
+      })
+    );
+
+    merge(updateList$, updateTip$).pipe(takeUntil(this.destroy$)).subscribe();
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  cancelApplication(): void {
+    
this.applicationService.cancelApplication(this.applicationDetail.id).subscribe(()
 => {
+      this.statusTips = 'Cancelling...';
+      this.cdr.markForCheck();
+    });
+  }
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-local.service.ts
similarity index 58%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application-local.service.ts
index 9d5ec33bbac..ae84448cf52 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application-local.service.ts
@@ -16,7 +16,21 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+import { Injectable } from '@angular/core';
+import { Observable, ReplaySubject } from 'rxjs';
+
+import { ApplicationDetail } from '@flink-runtime-web/interfaces';
+
+@Injectable()
+export class ApplicationLocalService {
+  /** Current activated job. */
+  private readonly applicationDetail$ = new 
ReplaySubject<ApplicationDetail>(1);
+
+  applicationDetailChanges(): Observable<ApplicationDetail> {
+    return this.applicationDetail$.asObservable();
+  }
+
+  setApplicationDetail(applicationDetail: ApplicationDetail): void {
+    this.applicationDetail$.next(applicationDetail);
+  }
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.html
similarity index 67%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application.component.html
index 70123c601be..326aef6c197 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.html
@@ -16,16 +16,14 @@
   ~ limitations under the License.
   -->
 
-<flink-overview-statistic></flink-overview-statistic>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Running Job List'"
-  [completed]="false"
-  (navigate)="navigateToJob(['job', 'running', $event.jid])"
-></flink-job-list>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Completed Job List'"
-  [completed]="true"
-  (navigate)="navigateToJob(['job', 'completed', $event.jid])"
-></flink-job-list>
+<ng-container *ngIf="!applicationIdSelected; else applicationTemplate">
+  <flink-application-list
+    [completed]="isCompleted"
+    [title]="cardTitle"
+    (navigate)="navigateToApplication($event)"
+  ></flink-application-list>
+</ng-container>
+
+<ng-template #applicationTemplate>
+  <router-outlet></router-outlet>
+</ng-template>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.less
similarity index 90%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application.component.less
index 9d5ec33bbac..a489513f228 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.less
@@ -15,8 +15,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@import "theme";
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
+:host {
+  display: flex;
+  flex-flow: column nowrap;
+  height: 100%;
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.ts
new file mode 100644
index 00000000000..abfa8e92c63
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.component.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { NgIf } from '@angular/common';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, 
OnInit } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from 
'@angular/router';
+import { Subject } from 'rxjs';
+import { filter, takeUntil } from 'rxjs/operators';
+
+import { ApplicationListComponent } from 
'@flink-runtime-web/components/application-list/application-list.component';
+import { ApplicationItem } from '@flink-runtime-web/interfaces';
+
+@Component({
+  selector: 'flink-application',
+  templateUrl: './application.component.html',
+  styleUrls: ['./application.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  imports: [NgIf, ApplicationListComponent, RouterOutlet]
+})
+export class ApplicationComponent implements OnInit, OnDestroy {
+  applicationIdSelected?: string;
+  isCompleted = false;
+
+  private readonly destroy$ = new Subject<void>();
+
+  constructor(
+    private activatedRoute: ActivatedRoute,
+    private router: Router,
+    private readonly cdr: ChangeDetectorRef
+  ) {}
+
+  get cardTitle(): string {
+    return this.isCompleted ? 'Completed Applications' : 'Running 
Applications';
+  }
+
+  ngOnInit(): void {
+    this.updateApplicationIdSelected();
+    this.router.events
+      .pipe(
+        filter(event => event instanceof NavigationEnd),
+        takeUntil(this.destroy$)
+      )
+      .subscribe(() => {
+        this.updateApplicationIdSelected();
+      });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  navigateToApplication(application: ApplicationItem): void {
+    this.router.navigate([application.id], { relativeTo: this.activatedRoute 
}).then();
+  }
+
+  private updateApplicationIdSelected(): void {
+    const segments = 
this.router.parseUrl(this.router.url).root.children.primary.segments;
+    this.applicationIdSelected = segments[2]?.toString();
+    this.isCompleted = segments[1].path === 'completed';
+    this.cdr.markForCheck();
+  }
+}
diff --git a/flink-runtime-web/web-dashboard/src/app/services/public-api.ts 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
similarity index 60%
copy from flink-runtime-web/web-dashboard/src/app/services/public-api.ts
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
index 5289fc927a3..e5a659e429c 100644
--- a/flink-runtime-web/web-dashboard/src/app/services/public-api.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/application.config.ts
@@ -16,11 +16,17 @@
  * limitations under the License.
  */
 
-export * from './status.service';
-export * from './overview.service';
-export * from './job.service';
-export * from './jar.service';
-export * from './job-manager.service';
-export * from './task-manager.service';
-export * from './metrics.service';
-export * from './config.service';
+import { InjectionToken } from '@angular/core';
+
+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' }]
+};
+
+export const APPLICATION_MODULE_CONFIG = new 
InjectionToken<ApplicationModuleConfig>('application-module-config', {
+  providedIn: 'root',
+  factory: () => APPLICATION_MODULE_DEFAULT_CONFIG
+});
diff --git a/flink-runtime-web/web-dashboard/src/app/services/public-api.ts 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
similarity index 58%
copy from flink-runtime-web/web-dashboard/src/app/services/public-api.ts
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
index 5289fc927a3..6daac893d51 100644
--- a/flink-runtime-web/web-dashboard/src/app/services/public-api.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/completed-application/routes.ts
@@ -16,11 +16,23 @@
  * limitations under the License.
  */
 
-export * from './status.service';
-export * from './overview.service';
-export * from './job.service';
-export * from './jar.service';
-export * from './job-manager.service';
-export * from './task-manager.service';
-export * from './metrics.service';
-export * from './config.service';
+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' }
+    ]
+  }
+];
diff --git a/flink-runtime-web/web-dashboard/src/app/routes.ts 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
similarity index 54%
copy from flink-runtime-web/web-dashboard/src/app/routes.ts
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
index 88c39342501..e070a02b524 100644
--- a/flink-runtime-web/web-dashboard/src/app/routes.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/routes.ts
@@ -18,20 +18,23 @@
 
 import { Routes } from '@angular/router';
 
-export const APP_ROUTES: Routes = [
-  {
-    path: 'overview',
-    loadComponent: () => import('./pages/overview/overview.component').then(m 
=> m.OverviewComponent)
-  },
-  { path: 'submit', loadComponent: () => 
import('./pages/submit/submit.component').then(m => m.SubmitComponent) },
-  {
-    path: 'job-manager',
-    loadChildren: () => import('./pages/job-manager/routes').then(m => 
m.JOB_MANAGER_ROUTES)
-  },
+import { ApplicationDetailComponent } from 
'@flink-runtime-web/pages/application/application-detail/application-detail.component';
+import { RunningApplicationGuard } from 
'@flink-runtime-web/pages/application/modules/running-application/running-application.guard';
+
+export const RUNNING_APPLICATION_ROUTES: Routes = [
   {
-    path: 'task-manager',
-    loadChildren: () => import('./pages/task-manager/routes').then(m => 
m.TASK_MANAGER_ROUTES)
-  },
-  { path: 'job', loadChildren: () => import('./pages/job/routes').then(m => 
m.JOB_ROUTES) },
-  { path: '**', redirectTo: 'overview', pathMatch: 'full' }
+    path: '',
+    component: ApplicationDetailComponent,
+    canActivate: [RunningApplicationGuard],
+    children: [
+      {
+        path: 'overview',
+        loadChildren: () => import('../../overview/routes').then(m => 
m.APPLICATION_OVERVIEW_ROUTES),
+        data: {
+          path: 'overview'
+        }
+      },
+      { path: '**', redirectTo: 'overview', pathMatch: 'full' }
+    ]
+  }
 ];
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/running-application.guard.ts
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/running-application.guard.ts
new file mode 100644
index 00000000000..e5d37527c5f
--- /dev/null
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/modules/running-application/running-application.guard.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, 
UrlTree } from '@angular/router';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { ApplicationService } from '@flink-runtime-web/services';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RunningApplicationGuard implements CanActivate {
+  constructor(private readonly applicationService: ApplicationService, private 
router: Router) {}
+
+  canActivate(
+    route: ActivatedRouteSnapshot,
+    _: RouterStateSnapshot
+  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | 
UrlTree {
+    const applicationId = route.params['id'];
+    if (!applicationId) {
+      return false;
+    }
+    return this.applicationService.loadApplications().pipe(
+      map(applications => {
+        const applicationItem = applications.find(application => 
application.id === applicationId);
+        if (!applicationItem) {
+          this.router.navigate(['/', 'application', 'running']).then();
+          return false;
+        }
+        if (applicationItem.completed) {
+          this.router.navigate(['/', 'application', 'completed', 
applicationId]).then();
+          return false;
+        }
+        return true;
+      })
+    );
+  }
+}
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.html
similarity index 95%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.html
index 70123c601be..010d4951b46 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.html
@@ -16,7 +16,6 @@
   ~ limitations under the License.
   -->
 
-<flink-overview-statistic></flink-overview-statistic>
 <flink-job-list
   [jobData$]="jobData$"
   [title]="'Running Job List'"
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.less
similarity index 89%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.less
index 9d5ec33bbac..dd999280508 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.less
@@ -16,7 +16,10 @@
  * limitations under the License.
  */
 
+@import "theme";
+
 flink-job-list {
   display: block;
-  margin-bottom: 24px;
+  padding-bottom: 24px;
+  background-color: darken(@component-background, 5%);
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.ts
similarity index 65%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.ts
index b037306298f..37428acbf23 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/application-overview.component.ts
@@ -18,37 +18,32 @@
 
 import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from 
'@angular/core';
 import { Router } from '@angular/router';
-import { Observable, Subject } from 'rxjs';
-import { mergeMap, share, takeUntil } from 'rxjs/operators';
+import { Observable, shareReplay, Subject } from 'rxjs';
+import { map, takeUntil } from 'rxjs/operators';
 
 import { JobListComponent } from 
'@flink-runtime-web/components/job-list/job-list.component';
 import { JobsItem } from '@flink-runtime-web/interfaces';
-import { OverviewStatisticComponent } from 
'@flink-runtime-web/pages/overview/statistic/overview-statistic.component';
-import { JobService, StatusService } from '@flink-runtime-web/services';
+import { ApplicationLocalService } from 
'@flink-runtime-web/pages/application/application-local.service';
 
 @Component({
-  selector: 'flink-overview',
-  templateUrl: './overview.component.html',
-  styleUrls: ['./overview.component.less'],
+  selector: 'application-overview',
+  templateUrl: './application-overview.component.html',
+  styleUrls: ['./application-overview.component.less'],
   changeDetection: ChangeDetectionStrategy.OnPush,
-  imports: [OverviewStatisticComponent, JobListComponent]
+  imports: [JobListComponent]
 })
-export class OverviewComponent implements OnInit, OnDestroy {
+export class ApplicationOverviewComponent implements OnInit, OnDestroy {
   public jobData$: Observable<JobsItem[]>;
 
   private readonly destroy$ = new Subject<void>();
 
-  constructor(
-    private readonly statusService: StatusService,
-    private readonly jobService: JobService,
-    private router: Router
-  ) {}
+  constructor(private readonly applicationLocalService: 
ApplicationLocalService, private router: Router) {}
 
   public ngOnInit(): void {
-    this.jobData$ = this.statusService.refresh$.pipe(
+    this.jobData$ = 
this.applicationLocalService.applicationDetailChanges().pipe(
       takeUntil(this.destroy$),
-      mergeMap(() => this.jobService.loadJobs()),
-      share()
+      map(data => data.jobs),
+      shareReplay({ bufferSize: 1, refCount: true })
     );
   }
 
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/routes.ts
similarity index 76%
copy from 
flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
copy to 
flink-runtime-web/web-dashboard/src/app/pages/application/overview/routes.ts
index 9d5ec33bbac..fddeb9309c6 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/application/overview/routes.ts
@@ -16,7 +16,13 @@
  * limitations under the License.
  */
 
-flink-job-list {
-  display: block;
-  margin-bottom: 24px;
-}
+import { Routes } from '@angular/router';
+
+import { ApplicationOverviewComponent } from 
'./application-overview.component';
+
+export const APPLICATION_OVERVIEW_ROUTES: Routes = [
+  {
+    path: '',
+    component: ApplicationOverviewComponent
+  }
+];
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/application/routes.ts 
b/flink-runtime-web/web-dashboard/src/app/pages/application/routes.ts
new file mode 100644
index 00000000000..ec741cebd4d
--- /dev/null
+++ b/flink-runtime-web/web-dashboard/src/app/pages/application/routes.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { Routes } from '@angular/router';
+
+import { ApplicationLocalService } from 
'@flink-runtime-web/pages/application/application-local.service';
+import { ApplicationComponent } from 
'@flink-runtime-web/pages/application/application.component';
+
+export const APPLICATION_ROUTES: Routes = [
+  {
+    path: '',
+    providers: [ApplicationLocalService],
+    children: [
+      {
+        path: 'running',
+        component: ApplicationComponent,
+        children: [
+          {
+            path: ':id',
+            loadChildren: () => 
import('./modules/running-application/routes').then(m => 
m.RUNNING_APPLICATION_ROUTES)
+          }
+        ]
+      },
+      {
+        path: 'completed',
+        component: ApplicationComponent,
+        children: [
+          {
+            path: ':id',
+            loadChildren: () =>
+              import('./modules/completed-application/routes').then(m => 
m.COMPLETED_APPLICATION_ROUES)
+          }
+        ]
+      }
+    ]
+  }
+];
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
 
b/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
index 70123c601be..0371feb6b64 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.html
@@ -17,15 +17,15 @@
   -->
 
 <flink-overview-statistic></flink-overview-statistic>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Running Job List'"
+<flink-application-list
+  [applicationData$]="applicationData$"
+  [title]="'Running Application List'"
   [completed]="false"
-  (navigate)="navigateToJob(['job', 'running', $event.jid])"
-></flink-job-list>
-<flink-job-list
-  [jobData$]="jobData$"
-  [title]="'Completed Job List'"
+  (navigate)="navigateToApplication(['application', 'running', $event.id])"
+></flink-application-list>
+<flink-application-list
+  [applicationData$]="applicationData$"
+  [title]="'Completed Application List'"
   [completed]="true"
-  (navigate)="navigateToJob(['job', 'completed', $event.jid])"
-></flink-job-list>
+  (navigate)="navigateToApplication(['application', 'completed', $event.id])"
+></flink-application-list>
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
index 9d5ec33bbac..a5b0089f1a4 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.less
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-flink-job-list {
+flink-application-list {
   display: block;
   margin-bottom: 24px;
 }
diff --git 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts 
b/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts
index b037306298f..9bad37bbe7c 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/overview/overview.component.ts
@@ -21,33 +21,33 @@ import { Router } from '@angular/router';
 import { Observable, Subject } from 'rxjs';
 import { mergeMap, share, takeUntil } from 'rxjs/operators';
 
-import { JobListComponent } from 
'@flink-runtime-web/components/job-list/job-list.component';
-import { JobsItem } from '@flink-runtime-web/interfaces';
+import { ApplicationListComponent } from 
'@flink-runtime-web/components/application-list/application-list.component';
+import { ApplicationItem } from '@flink-runtime-web/interfaces';
 import { OverviewStatisticComponent } from 
'@flink-runtime-web/pages/overview/statistic/overview-statistic.component';
-import { JobService, StatusService } from '@flink-runtime-web/services';
+import { ApplicationService, StatusService } from 
'@flink-runtime-web/services';
 
 @Component({
   selector: 'flink-overview',
   templateUrl: './overview.component.html',
   styleUrls: ['./overview.component.less'],
   changeDetection: ChangeDetectionStrategy.OnPush,
-  imports: [OverviewStatisticComponent, JobListComponent]
+  imports: [OverviewStatisticComponent, ApplicationListComponent]
 })
 export class OverviewComponent implements OnInit, OnDestroy {
-  public jobData$: Observable<JobsItem[]>;
+  public applicationData$: Observable<ApplicationItem[]>;
 
   private readonly destroy$ = new Subject<void>();
 
   constructor(
     private readonly statusService: StatusService,
-    private readonly jobService: JobService,
+    private readonly applicationService: ApplicationService,
     private router: Router
   ) {}
 
   public ngOnInit(): void {
-    this.jobData$ = this.statusService.refresh$.pipe(
+    this.applicationData$ = this.statusService.refresh$.pipe(
       takeUntil(this.destroy$),
-      mergeMap(() => this.jobService.loadJobs()),
+      mergeMap(() => this.applicationService.loadApplications()),
       share()
     );
   }
@@ -57,7 +57,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
     this.destroy$.complete();
   }
 
-  public navigateToJob(commands: string[]): void {
+  public navigateToApplication(commands: string[]): void {
     this.router.navigate(commands).then();
   }
 }
diff --git a/flink-runtime-web/web-dashboard/src/app/routes.ts 
b/flink-runtime-web/web-dashboard/src/app/routes.ts
index 88c39342501..e6a0cc2e529 100644
--- a/flink-runtime-web/web-dashboard/src/app/routes.ts
+++ b/flink-runtime-web/web-dashboard/src/app/routes.ts
@@ -33,5 +33,6 @@ export const APP_ROUTES: Routes = [
     loadChildren: () => import('./pages/task-manager/routes').then(m => 
m.TASK_MANAGER_ROUTES)
   },
   { path: 'job', loadChildren: () => import('./pages/job/routes').then(m => 
m.JOB_ROUTES) },
+  { path: 'application', loadChildren: () => 
import('./pages/application/routes').then(m => m.APPLICATION_ROUTES) },
   { 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
new file mode 100644
index 00000000000..d9b7855556b
--- /dev/null
+++ b/flink-runtime-web/web-dashboard/src/app/services/application.service.ts
@@ -0,0 +1,95 @@
+/*
+ * 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 { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { EMPTY, Observable } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+import {
+  ApplicationDetail,
+  ApplicationOverview,
+  ApplicationItem,
+  JobStatus,
+  TaskStatus
+} from '@flink-runtime-web/interfaces';
+
+import { ConfigService } from './config.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ApplicationService {
+  constructor(private readonly httpClient: HttpClient, private readonly 
configService: ConfigService) {}
+
+  public cancelApplication(applicationId: string): Observable<void> {
+    return 
this.httpClient.post<void>(`${this.configService.BASE_URL}/applications/${applicationId}/cancel`,
 {});
+  }
+
+  public loadApplications(): Observable<ApplicationItem[]> {
+    return 
this.httpClient.get<ApplicationOverview>(`${this.configService.BASE_URL}/applications/overview`).pipe(
+      map(data => {
+        data.applications.forEach(application => {
+          let total = 0;
+          for (const key in application.jobs) {
+            total += application.jobs[key as keyof JobStatus];
+          }
+          application.jobs['TOTAL'] = total;
+          application.completed = ['FINISHED', 'FAILED', 
'CANCELED'].indexOf(application.status) > -1;
+        });
+        return data.applications || [];
+      }),
+      catchError(() => EMPTY)
+    );
+  }
+
+  public loadApplication(applicationId: string): Observable<ApplicationDetail> 
{
+    return 
this.httpClient.get<ApplicationDetail>(`${this.configService.BASE_URL}/applications/${applicationId}`).pipe(
+      map(data => {
+        const statusCounts: JobStatus = {
+          CANCELED: 0,
+          CANCELING: 0,
+          CREATED: 0,
+          FAILED: 0,
+          FAILING: 0,
+          FINISHED: 0,
+          RECONCILING: 0,
+          RUNNING: 0,
+          RESTARTING: 0,
+          INITIALIZING: 0,
+          SUSPENDED: 0,
+          TOTAL: 0
+        };
+        data.jobs.forEach(job => {
+          statusCounts[job.state as keyof JobStatus] += 1;
+          statusCounts['TOTAL'] += 1;
+          for (const key in job.tasks) {
+            const upperCaseKey = key.toUpperCase() as keyof TaskStatus;
+            job.tasks[upperCaseKey] = job.tasks[key as keyof TaskStatus];
+            delete job.tasks[key as keyof TaskStatus];
+          }
+          job.tasks['PENDING'] = job['pending-operators'] || 0;
+          job.completed = ['FINISHED', 'FAILED', 
'CANCELED'].indexOf(job.state) > -1;
+        });
+        data['status-counts'] = statusCounts;
+        return data;
+      }),
+      catchError(() => EMPTY)
+    );
+  }
+}
diff --git a/flink-runtime-web/web-dashboard/src/app/services/public-api.ts 
b/flink-runtime-web/web-dashboard/src/app/services/public-api.ts
index 5289fc927a3..df66d9dcaba 100644
--- a/flink-runtime-web/web-dashboard/src/app/services/public-api.ts
+++ b/flink-runtime-web/web-dashboard/src/app/services/public-api.ts
@@ -24,3 +24,4 @@ export * from './job-manager.service';
 export * from './task-manager.service';
 export * from './metrics.service';
 export * from './config.service';
+export * from './application.service';

Reply via email to