Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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)`;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';

Expand All @@ -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 =>
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -78,9 +152,7 @@ export class JobOverviewDrawerWatermarksComponent implements OnInit, OnDestroy {
}
return list;
}),
catchError(() => {
return of([] as WatermarkData[]);
})
catchError(() => of([] as WatermarkData[]))
)
),
takeUntil(this.destroy$)
Expand Down