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

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


The following commit(s) were added to refs/heads/master by this push:
     new 64d0316aad2 [FLINK-39042][webui] Make watermark timestamps 
timezone-aware and configurable (#27552)
64d0316aad2 is described below

commit 64d0316aad22e83888b65e6c3036d0a980ae33a9
Author: Feat Zhang <[email protected]>
AuthorDate: Mon Mar 23 18:42:33 2026 +0800

    [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.
---
 .../src/app/components/humanize-watermark.pipe.ts  | 63 +++++++++++++++--
 .../job-detail/status/job-status.component.less    |  4 +-
 .../job-overview-drawer-watermarks.component.html  | 25 ++++++-
 .../job-overview-drawer-watermarks.component.less  | 18 +++++
 .../job-overview-drawer-watermarks.component.ts    | 82 ++++++++++++++++++++--
 5 files changed, 178 insertions(+), 14 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 516dd54a652..50caca30864 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/job-detail/status/job-status.component.less
 
b/flink-runtime-web/web-dashboard/src/app/pages/job/job-detail/status/job-status.component.less
index 9a2ef00ce4a..8285f117954 100644
--- 
a/flink-runtime-web/web-dashboard/src/app/pages/job/job-detail/status/job-status.component.less
+++ 
b/flink-runtime-web/web-dashboard/src/app/pages/job/job-detail/status/job-status.component.less
@@ -42,10 +42,10 @@
   .back-link {
     display: inline-flex;
     align-items: center;
-    color: @link-color;
-    text-decoration: none;
     margin-right: @margin-xs;
+    color: @link-color;
     font-size: @font-size-base;
+    text-decoration: none;
     cursor: pointer;
 
     &:hover {
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 4241e5387dd..ef368ac8a9e 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.
   -->
 
+<!-- Timezone selector -->
+<div class="timezone-selector">
+  <label>Timezone:</label>
+  <nz-select
+    [(ngModel)]="selectedTimezone"
+    nzSize="small"
+    nzShowSearch
+    [nzPlaceHolder]="'Select Timezone'"
+    style="width: 300px"
+  >
+    <nz-option
+      *ngFor="let option of timezoneOptions"
+      [nzLabel]="option.label"
+      [nzValue]="option.value"
+      nzCustomContent
+    >
+      <span [title]="option.label">{{ option.label }}</span>
+    </nz-option>
+  </nz-select>
+</div>
+
 <nz-table
   class="no-border small full-height"
   nzSize="small"
@@ -39,7 +60,7 @@
           class="header-icon"
           nz-icon
           nz-tooltip
-          nzTooltipTitle="This column shows the datetime that is parsed from 
watermark timestamp with local time zone. Note that the time zone is obtained 
through your browser. "
+          nzTooltipTitle="This column shows the datetime that is parsed from 
watermark timestamp. You can select the desired timezone from the dropdown 
above."
           nzType="info-circle"
         ></i>
       </th>
@@ -51,7 +72,7 @@
         <tr>
           <td>{{ watermark.subTaskIndex }}</td>
           <td>{{ watermark.watermark | humanizeWatermark }}</td>
-          <td>{{ watermark.watermark | humanizeWatermarkToDatetime }}</td>
+          <td>{{ watermark.watermark | humanizeWatermarkToDatetime: 
selectedTimezone }}</td>
         </tr>
       </ng-container>
     </ng-template>
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 2e996cab4ed..7ee65c5351c 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 3153cbd5370..b243f251256 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<WatermarkData>();
 
+  // 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<void>();
 
   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$)

Reply via email to