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';
