From 012e6b2b500463287206900e8c7ed90aeba3d846 Mon Sep 17 00:00:00 2001 From: FeatZhang Date: Mon, 23 Mar 2026 18:42:33 +0800 Subject: [PATCH 1/3] [FLINK-39042][webui] Make watermark timestamps timezone-aware and configurable (#27552) * [FLINK-39042][ui] Make watermark timestamps timezone-aware and configurable * [FLINK-39042][webui] Use Intl API for timezone handling and support DST Address review feedback: - Use native Intl.DateTimeFormat API instead of manual offset calculation - Replace fixed UTC offsets with IANA timezone names (e.g., America/Los_Angeles) - IANA timezones automatically handle daylight saving time transitions - Remove redundant console.log statements and improve code quality - Add documentation comments with reference to IANA timezone database * [FLINK-39042][webui] Make watermark timestamps timezone-aware and configurable * [FLINK-39042][webui] Address code review comments - Add MDN reference for Intl.DateTimeFormat API - Clarify DST handling in timezone abbreviations (e.g., PST vs PDT) - Improve error messages to explicitly mention UTC fallback - Update comments to better explain timezone resolution logic * Address review comments - Remove 'pure: false' from HumanizeWatermarkToDatetimePipe as template already passes timezone argument - Use Intl.supportedValuesOf('timeZone') to get all supported timezones instead of hardcoded list - Keep browser local timezone as default - Revert unnecessary changes in job-status.component.less * fix: improve timezone fallback handling and CSS property ordering - Enhance timezone detection fallback logic in watermark component\n- Fix CSS property ordering in job status component for better maintainability * style: improve code formatting for better readability - Split long lines in timezone options initialization for improved code readability * style: optimize CSS property ordering for better maintainability - Reorder CSS properties in job status component for improved code organization * fix: add type assertion for Intl.supportedValuesOf to fix TS compilation error * [FLINK-39042][webui] Use browser locale instead of hardcoded en-US in watermark datetime pipe Replace hardcoded 'en-US' locale with undefined in Intl.DateTimeFormat to respect the user's browser locale settings. This ensures the date display order follows the user's regional preferences rather than being fixed to the en-US format. Suggested by @davidradl in code review. --- .../app/components/humanize-watermark.pipe.ts | 63 ++++++++++++-- ...-overview-drawer-watermarks.component.html | 25 +++++- ...-overview-drawer-watermarks.component.less | 18 ++++ ...ob-overview-drawer-watermarks.component.ts | 82 +++++++++++++++++-- 4 files changed, 176 insertions(+), 12 deletions(-) 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$) From 50828925e8252b8ee0d3d441f823a7ae88da9160 Mon Sep 17 00:00:00 2001 From: FeatZhang Date: Mon, 4 May 2026 02:46:32 +0800 Subject: [PATCH 2/3] [hotfix][ci] Trigger Azure CI re-run after flaky MinioTestContainerTest failure The previous CI build #74753 failed only in the 'Test - connect' stage due to MinioTestContainerTest failing to launch minio:RELEASE.2022-02-07T08-17-33Z Docker container (InvalidFormatException on Docker daemon timestamps with 9-digit nanosecond precision). This is a well-known infrastructure flaky unrelated to this PR, which only touches flink-runtime-web/web-dashboard TypeScript/LESS/HTML files. Pushing an empty commit to re-trigger a fresh Azure build on a different agent. From 2d64000cae5a08dee1ade9a76f4275d48105a85e Mon Sep 17 00:00:00 2001 From: FeatZhang Date: Mon, 4 May 2026 14:53:51 +0800 Subject: [PATCH 3/3] [hotfix][ci] Re-trigger Azure CI (2nd attempt) after MinioTestContainer flaky Previous Azure build #74758 failed again in the exact same infrastructure flaky: flink-s3-fs-base MinioTestContainerTest could not launch the MinIO Docker container because testcontainers (bundled on release-1.20) cannot parse the 9-digit nanosecond timestamp returned by the Docker daemon. No code change in this PR touches flink-s3-fs-base. Re-running.