diff --git a/flink-runtime-web/web-dashboard/src/app/components/humanize-watermark.pipe.ts b/flink-runtime-web/web-dashboard/src/app/components/humanize-watermark.pipe.ts
index 516dd54a6528c..50caca308644c 100644
--- a/flink-runtime-web/web-dashboard/src/app/components/humanize-watermark.pipe.ts
+++ b/flink-runtime-web/web-dashboard/src/app/components/humanize-watermark.pipe.ts
@@ -21,7 +21,8 @@ import { Pipe, PipeTransform } from '@angular/core';
import { ConfigService } from '@flink-runtime-web/services';
@Pipe({
- name: 'humanizeWatermark'
+ name: 'humanizeWatermark',
+ standalone: true
})
export class HumanizeWatermarkPipe implements PipeTransform {
constructor(private readonly configService: ConfigService) {}
@@ -36,16 +37,68 @@ export class HumanizeWatermarkPipe implements PipeTransform {
}
@Pipe({
- name: 'humanizeWatermarkToDatetime'
+ name: 'humanizeWatermarkToDatetime',
+ standalone: true
})
export class HumanizeWatermarkToDatetimePipe implements PipeTransform {
constructor(private readonly configService: ConfigService) {}
- public transform(value: number): number | string {
+ public transform(value: number, timezone: string = 'UTC'): number | string {
if (value == null || isNaN(value) || value <= this.configService.LONG_MIN_VALUE) {
return 'N/A';
- } else {
- return new Date(value).toLocaleString();
+ }
+
+ try {
+ const date = new Date(value);
+
+ // Use Intl.DateTimeFormat for proper timezone handling including DST
+ // This native browser API automatically handles daylight saving time transitions
+ // Use undefined as locale to respect the user's browser locale settings
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
+ const dateFormatter = new Intl.DateTimeFormat(undefined, {
+ timeZone: timezone,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ hour12: false,
+ minute: '2-digit',
+ second: '2-digit'
+ });
+
+ // Get timezone abbreviation (e.g., PST, PDT, EST, EDT)
+ // The abbreviation automatically reflects DST status (e.g., PST vs PDT)
+ const timezoneFormatter = new Intl.DateTimeFormat(undefined, {
+ timeZone: timezone,
+ timeZoneName: 'short'
+ });
+
+ // Format the date parts
+ const parts = dateFormatter.formatToParts(date);
+ const year = parts.find(p => p.type === 'year')?.value;
+ const month = parts.find(p => p.type === 'month')?.value;
+ const day = parts.find(p => p.type === 'day')?.value;
+ const hour = parts.find(p => p.type === 'hour')?.value;
+ const minute = parts.find(p => p.type === 'minute')?.value;
+ const second = parts.find(p => p.type === 'second')?.value;
+
+ // Extract timezone abbreviation which includes DST information
+ // For example: PST (standard) vs PDT (daylight saving)
+ const timezoneParts = timezoneFormatter.formatToParts(date);
+ const timezoneAbbr = timezoneParts.find(p => p.type === 'timeZoneName')?.value || timezone;
+
+ return `${year}-${month}-${day} ${hour}:${minute}:${second} (${timezoneAbbr})`;
+ } catch (error) {
+ // Fallback to UTC if timezone is invalid, so using UTC
+ console.error('[HumanizeWatermarkToDatetimePipe] Error formatting date, falling back to UTC:', error);
+ const date = new Date(value);
+ const year = date.getUTCFullYear();
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
+ const day = String(date.getUTCDate()).padStart(2, '0');
+ const hour = String(date.getUTCHours()).padStart(2, '0');
+ const minute = String(date.getUTCMinutes()).padStart(2, '0');
+ const second = String(date.getUTCSeconds()).padStart(2, '0');
+ return `${year}-${month}-${day} ${hour}:${minute}:${second} (UTC)`;
}
}
}
diff --git a/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.html b/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.html
index 4241e5387dd5c..ef368ac8a9e73 100644
--- a/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.html
+++ b/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.html
@@ -16,6 +16,27 @@
~ limitations under the License.
-->
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
@@ -51,7 +72,7 @@
| {{ watermark.subTaskIndex }} |
{{ watermark.watermark | humanizeWatermark }} |
- {{ watermark.watermark | humanizeWatermarkToDatetime }} |
+ {{ watermark.watermark | humanizeWatermarkToDatetime: selectedTimezone }} |
diff --git a/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.less b/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.less
index 2e996cab4ed7a..7ee65c5351cf8 100644
--- a/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.less
+++ b/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.less
@@ -37,3 +37,21 @@
}
}
}
+
+.timezone-selector {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ padding: 4px 0;
+
+ label {
+ margin-right: 8px;
+ color: @text-color;
+ font-size: @font-size-sm;
+ white-space: nowrap;
+ }
+
+ nz-select {
+ flex-shrink: 0;
+ }
+}
diff --git a/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.ts b/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.ts
index 3153cbd537086..b243f251256d6 100644
--- a/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.ts
+++ b/flink-runtime-web/web-dashboard/src/app/pages/job/overview/watermarks/job-overview-drawer-watermarks.component.ts
@@ -16,8 +16,9 @@
* limitations under the License.
*/
-import { NgIf } from '@angular/common';
+import { NgForOf, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
import { of, Subject } from 'rxjs';
import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators';
@@ -28,6 +29,7 @@ import {
import { MetricsService } from '@flink-runtime-web/services';
import { typeDefinition } from '@flink-runtime-web/utils';
import { NzIconModule } from 'ng-zorro-antd/icon';
+import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTooltipModule } from 'ng-zorro-antd/tooltip';
@@ -43,7 +45,18 @@ interface WatermarkData {
templateUrl: './job-overview-drawer-watermarks.component.html',
styleUrls: ['./job-overview-drawer-watermarks.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [NzTableModule, NgIf, HumanizeWatermarkPipe, HumanizeWatermarkToDatetimePipe, NzIconModule, NzTooltipModule]
+ standalone: true,
+ imports: [
+ NgIf,
+ NgForOf,
+ FormsModule,
+ NzSelectModule,
+ NzTableModule,
+ NzIconModule,
+ NzTooltipModule,
+ HumanizeWatermarkPipe,
+ HumanizeWatermarkToDatetimePipe
+ ]
})
export class JobOverviewDrawerWatermarksComponent implements OnInit, OnDestroy {
public readonly trackBySubtaskIndex = (_: number, node: { subTaskIndex: number; watermark: number }): number =>
@@ -54,6 +67,14 @@ export class JobOverviewDrawerWatermarksComponent implements OnInit, OnDestroy {
public virtualItemSize = 36;
public readonly narrowLogData = typeDefinition();
+ // Timezone related properties
+ // Using browser's native Intl API to get all supported timezones
+ // This provides a complete timezone list and properly handles Daylight Saving Time (DST)
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf
+ public timezoneOptions: Array<{ label: string; value: string }> = [];
+
+ public selectedTimezone: string = '';
+
private readonly destroy$ = new Subject();
constructor(
@@ -62,7 +83,60 @@ export class JobOverviewDrawerWatermarksComponent implements OnInit, OnDestroy {
private readonly cdr: ChangeDetectorRef
) {}
+ private getBrowserTimezone(): string {
+ // Get browser's IANA timezone identifier
+ // This will properly handle DST changes
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions
+ try {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
+ } catch (error) {
+ console.error('[getBrowserTimezone] Error getting browser timezone, falling back to UTC:', error);
+ // As a last resort, rely on runtime environment offset
+ // Note: Etc/GMT uses POSIX-style signs (opposite of ISO-8601):
+ // - Etc/GMT-8 = UTC+8 (East of Greenwich)
+ // - Etc/GMT+8 = UTC-8 (West of Greenwich)
+ // Reference: https://github.com/eggert/tz/blob/main/etcetera
+ const offset = new Date().getTimezoneOffset();
+ const sign = offset > 0 ? '+' : '-';
+ const hours = String(Math.floor(Math.abs(offset) / 60));
+ return `Etc/GMT${sign}${hours}`;
+ }
+ }
+
+ private initializeTimezoneOptions(): void {
+ // Use modern browser API to get all supported timezones
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf
+ try {
+ // Cast to access supportedValuesOf which is available in modern browsers but not in ES2018 type definitions
+ const intl = Intl as typeof Intl & { supportedValuesOf?: (key: string) => string[] };
+ if (typeof intl.supportedValuesOf === 'function') {
+ const timezones: string[] = intl.supportedValuesOf('timeZone');
+ this.timezoneOptions = timezones
+ .map((tz: string) => ({ label: tz, value: tz }))
+ .sort((a: { label: string }, b: { label: string }) => a.label.localeCompare(b.label));
+ } else {
+ // Fallback for browsers that don't support Intl.supportedValuesOf
+ console.warn(
+ '[initializeTimezoneOptions] Intl.supportedValuesOf not supported, falling back to browser local timezone'
+ );
+ const browserTz = this.getBrowserTimezone();
+ this.timezoneOptions = [{ label: browserTz, value: browserTz }];
+ }
+ } catch (error) {
+ console.error('[initializeTimezoneOptions] Error initializing timezone options:', error);
+ // Fallback to browser local timezone on error
+ const browserTz = this.getBrowserTimezone();
+ this.timezoneOptions = [{ label: browserTz, value: browserTz }];
+ }
+ }
+
public ngOnInit(): void {
+ // Initialize timezone options
+ this.initializeTimezoneOptions();
+
+ // Set default timezone to browser's timezone
+ this.selectedTimezone = this.getBrowserTimezone();
+
this.jobLocalService
.jobWithVertexChanges()
.pipe(
@@ -78,9 +152,7 @@ export class JobOverviewDrawerWatermarksComponent implements OnInit, OnDestroy {
}
return list;
}),
- catchError(() => {
- return of([] as WatermarkData[]);
- })
+ catchError(() => of([] as WatermarkData[]))
)
),
takeUntil(this.destroy$)