diff --git a/CHANGELOG.md b/CHANGELOG.md index 891dba6b..e76de1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Replaced the package for custom svg icons with the MatIconRegistry - Replaced the lodash package with custom helpers +- Replaced the moment.js package with date-fns ## [0.37.0] - 2025-01-17 diff --git a/angular.json b/angular.json index ea310838..b19999f4 100644 --- a/angular.json +++ b/angular.json @@ -18,14 +18,12 @@ "builder": "@angular-devkit/build-angular:browser", "options": { "allowedCommonJsDependencies": [ - "lodash", "zen-observable", "dagre", - "moment-timezone", "webcola", "fast-json-stable-stringify", - "marked", "moment", + "marked", "cron-parser", "@tweenjs/tween.js" ], diff --git a/karma.conf.js b/karma.conf.js index 38ba43fa..d1585116 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -30,4 +30,5 @@ module.exports = function (config) { singleRun: false, restartOnFileChange: true, }); + process.env.TZ = "Europe/Kyiv"; }; diff --git a/package-lock.json b/package-lock.json index ae833943..19ce13da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,14 +31,15 @@ "@swimlane/ngx-graph": "8.4.0", "angular2-multiselect-dropdown": "^10.0.0", "apollo-angular": "^5.0.2", - "autoprefixer": "^10.4.20", + "autoprefixer": "10.4.5", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", "clipboard": "^2.0.11", "cron-parser": "^4.9.0", "d3-scale": "^4.0.2", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "graphql": "^16.10.0", - "moment-timezone": "^0.5.46", "monaco-editor": "^0.41.0", "ng-multiselect-dropdown": "^1.0.0", "ngx-highlightjs": "^7.0.1", @@ -7257,9 +7258,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.5.tgz", + "integrity": "sha512-Fvd8yCoA7lNX/OUllvS+aS1I7WRBclGXsepbvT8ZaPgrH24rgXpZzF0/6Hh3ZEkwg+0AES/Osd196VZmYoEFtw==", "funding": [ { "type": "opencollective", @@ -7268,18 +7269,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", + "browserslist": "^4.20.2", + "caniuse-lite": "^1.0.30001332", + "fraction.js": "^4.2.0", "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -9365,6 +9362,23 @@ "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", "dev": true }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -14380,17 +14394,7 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.46", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", - "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", - "dependencies": { - "moment": "^2.29.4" - }, + "peer": true, "engines": { "node": "*" } diff --git a/package.json b/package.json index 8f1f2301..b3c64395 100644 --- a/package.json +++ b/package.json @@ -42,14 +42,15 @@ "@swimlane/ngx-graph": "8.4.0", "angular2-multiselect-dropdown": "^10.0.0", "apollo-angular": "^5.0.2", - "autoprefixer": "^10.4.20", + "autoprefixer": "10.4.5", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", "clipboard": "^2.0.11", "cron-parser": "^4.9.0", "d3-scale": "^4.0.2", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "graphql": "^16.10.0", - "moment-timezone": "^0.5.46", "monaco-editor": "^0.41.0", "ng-multiselect-dropdown": "^1.0.0", "ngx-highlightjs": "^7.0.1", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8432b0af..a7de5ea8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -15,7 +15,6 @@ import { AppUIConfigFeatureFlags, LoginMethod } from "./app-config.model"; import { LoginService } from "./auth/login/login.service"; import { loadErrorMessages } from "@apollo/client/dev"; import { isDevMode } from "@angular/core"; -import moment from "moment"; import { LoggedUserService } from "./auth/logged-user.service"; import packageFile from "../../package.json"; import { LocalStorageService } from "./services/local-storage.service"; @@ -70,7 +69,6 @@ export class AppComponent extends BaseComponent implements OnInit { } this.registerMaterialIcons(); this.outputAppVersion(); - this.setMomentOptions(); this.readConfiguration(); this.checkView(); @@ -92,12 +90,6 @@ export class AppComponent extends BaseComponent implements OnInit { }); } - private setMomentOptions(): void { - moment.relativeTimeThreshold("s", 59); - moment.relativeTimeThreshold("m", 59); - moment.relativeTimeThreshold("h", 23); - } - private readConfiguration(): void { this.featureFlags = this.appConfigService.featureFlags; this.loginMethods = this.loginService.loginMethods; diff --git a/src/app/common/app.helpers.spec.ts b/src/app/common/app.helpers.spec.ts index ebc8ea5c..470255ee 100644 --- a/src/app/common/app.helpers.spec.ts +++ b/src/app/common/app.helpers.spec.ts @@ -1,6 +1,5 @@ import { MaybeNullOrUndefined } from "src/app/common/app.types"; import { fakeAsync, flush, tick } from "@angular/core/testing"; -import moment from "moment"; import { isEqual, isNil, @@ -10,6 +9,7 @@ import { requireValue, } from "./app.helpers"; import AppValues from "./app.values"; +import { subDays } from "date-fns"; describe("AppHelpers", () => { it("should check non-null requireValue", () => { @@ -59,7 +59,7 @@ describe("AppHelpers", () => { it("should convert data to today ", () => { const result = momentConvertDateToLocalWithFormat({ - date: Date.now(), + date: new Date(), format: AppValues.DISPLAY_DATE_FORMAT, isTextDate: true, }); @@ -68,7 +68,7 @@ describe("AppHelpers", () => { it("should convert data to yesterday ", () => { const result = momentConvertDateToLocalWithFormat({ - date: moment().subtract(1, "day").toDate(), + date: subDays(new Date(), 1), format: AppValues.DISPLAY_DATE_FORMAT, isTextDate: true, }); diff --git a/src/app/common/app.helpers.ts b/src/app/common/app.helpers.ts index afd2314a..dee67525 100644 --- a/src/app/common/app.helpers.ts +++ b/src/app/common/app.helpers.ts @@ -1,10 +1,10 @@ -import moment from "moment"; import cronParser from "cron-parser"; import { MaybeNull, MaybeNullOrUndefined } from "./app.types"; import { DataSchema } from "../api/kamu.graphql.interface"; import { DatasetSchema } from "../interface/dataset.interface"; import AppValues from "./app.values"; +import { format, isSameDay, subDays } from "date-fns"; export function requireValue(input: MaybeNull) { if (input === null) throw Error("value is required!"); @@ -35,25 +35,25 @@ export function isMobileView(): boolean { */ export function momentConvertDateToLocalWithFormat(dateParams: { date: Date | number; - format?: string; + format: string; isTextDate?: boolean; }): string { const stringDate: Date = new Date(dateParams.date); // solution for all browsers - const UTCStringDate: string = moment(stringDate).format("YYYY-MM-DDTHH:mm:ss.sss"); + const UTCStringDate: string = format(stringDate, "dd MMM yyyy"); const ISOStringDate: string = new Date(String(UTCStringDate)).toISOString(); if (dateParams.isTextDate) { - if (moment(dateParams.date).isSame(moment().subtract(1, "day"), "day")) { + if (isSameDay(dateParams.date, subDays(new Date(), 1))) { return "Yesterday"; } - if (moment(dateParams.date).isSame(moment(), "day")) { + if (isSameDay(dateParams.date, new Date())) { return "Today"; } } - return moment(ISOStringDate).format(dateParams.format); + return format(ISOStringDate, dateParams.format); } export function parseCurrentSchema(data: MaybeNullOrUndefined): MaybeNull { @@ -70,7 +70,7 @@ export function excludeAgoWord(value: string): string { export function cronExpressionNextTime(cronExpression: string): string { const date = cronParser.parseExpression(cronExpression).next().toDate(); - return moment(date).format(AppValues.CRON_EXPRESSION_DATE_FORMAT as string); + return format(date, AppValues.CRON_EXPRESSION_DATE_FORMAT); } export function convertSecondsToHumanReadableFormat(seconds: number): string { diff --git a/src/app/common/app.values.ts b/src/app/common/app.values.ts index 57eeaff9..0b99c162 100644 --- a/src/app/common/app.values.ts +++ b/src/app/common/app.values.ts @@ -17,11 +17,10 @@ export default class AppValues { public static readonly SCHEMA_NAME_PATTERN = /^[a-zA-Z0-9]+[a-zA-Z0-9\s(_)]*$/i; public static readonly SPLIT_ARGUMENTS_PATTERN = /[^\s"']+|"([^"]*)"+|'([^']*)'/g; - public static readonly DISPLAY_DATE_FORMAT = "DD MMM YYYY"; - public static readonly DISPLAY_TOOLTIP_DATE_FORMAT = "MMM D, YYYY, HH:mm A"; - public static readonly CRON_EXPRESSION_DATE_FORMAT = "MMM Do YYYY, h:mm:ss A ZZ"; + public static readonly DISPLAY_DATE_FORMAT = "dd MMM yyyy"; + public static readonly CRON_EXPRESSION_DATE_FORMAT = "MMM do yyyy pppp"; public static readonly DISPLAY_FLOW_DATE_FORMAT = "y-MM-dd, h:mm:ss a"; - public static readonly TIME_FORMAT = "h:mm:ss A"; + public static readonly TIME_FORMAT = "h:mm:ss a"; public static readonly UNIMPLEMENTED_MESSAGE = "Feature coming soon"; public static readonly SAMPLE_DATA_LIMIT = 10; public static readonly SQL_QUERY_LIMIT = 50; diff --git a/src/app/common/components/flows-table/flows-table.helpers.mock.ts b/src/app/common/components/flows-table/flows-table.helpers.mock.ts index 004e1452..57ec614f 100644 --- a/src/app/common/components/flows-table/flows-table.helpers.mock.ts +++ b/src/app/common/components/flows-table/flows-table.helpers.mock.ts @@ -418,24 +418,24 @@ export const mockFlowSummaryDataFragmentTooltipAndDurationText: FlowSummaryDataF export const durationBlockTextResults: string[] = [ "waiting for 58 minutes ", - "wake up time: in an hour", + "wake up time: in 1 hour", "wake up time: in 2 hours", - "deadline time: in an hour", + "deadline time: in 1 hour", "running for 48 minutes ", - "finished in an hour", + "finished in 1 hour", "aborted 2 hours ago", "failed 2 hours ago", ]; export const tooltipTextResults: string[] = [ - "waiting for: Mar 14th 2024, 12:24:29 PM +0200", - "Wake up time: Mar 14th 2024, 2:24:29 PM +0200", - "Wake up time: Mar 14th 2024, 3:24:29 PM +0200", - "Deadline time: Mar 14th 2024, 2:24:29 PM +0200", - "Start running time: Mar 14th 2024, 12:34:29 PM +0200", - "Completed time: Mar 14th 2024, 2:24:29 PM +0200", - "Aborted time: Mar 14th 2024, 11:24:29 AM +0200", - "Start running time: Mar 14th 2024, 11:24:29 AM +0200", + "waiting for: Mar 14th 2024 12:24:29 PM GMT+02:00", + "Wake up time: Mar 14th 2024 2:24:29 PM GMT+02:00", + "Wake up time: Mar 14th 2024 3:24:29 PM GMT+02:00", + "Deadline time: Mar 14th 2024 2:24:29 PM GMT+02:00", + "Start running time: Mar 14th 2024 12:34:29 PM GMT+02:00", + "Completed time: Mar 14th 2024 2:24:29 PM GMT+02:00", + "Aborted time: Mar 14th 2024 11:24:29 AM GMT+02:00", + "Start running time: Mar 14th 2024 11:24:29 AM GMT+02:00", ]; export const mockDatasets: DatasetListFlowsDataFragment[] = [ diff --git a/src/app/common/components/flows-table/flows-table.helpers.spec.ts b/src/app/common/components/flows-table/flows-table.helpers.spec.ts index 8473f574..22fc54f9 100644 --- a/src/app/common/components/flows-table/flows-table.helpers.spec.ts +++ b/src/app/common/components/flows-table/flows-table.helpers.spec.ts @@ -19,20 +19,15 @@ import { tooltipTextResults, } from "./flows-table.helpers.mock"; import timekeeper from "timekeeper"; -import moment from "moment"; import { mockDatasetMainDataId } from "src/app/search/mock.data"; describe("DatasetFlowTableHelpers", () => { beforeAll(() => { timekeeper.freeze("2024-03-14T11:22:29+00:00"); - moment.relativeTimeThreshold("s", 59); - moment.relativeTimeThreshold("m", 59); - moment.relativeTimeThreshold("h", 23); - moment.tz.setDefault("Europe/Kiev"); }); afterAll(() => { - moment.tz.setDefault(); + timekeeper.reset(); }); it("should check waiting block text with FlowStartConditionThrottling typename", () => { diff --git a/src/app/common/components/flows-table/flows-table.helpers.ts b/src/app/common/components/flows-table/flows-table.helpers.ts index 75935df8..20e20f89 100644 --- a/src/app/common/components/flows-table/flows-table.helpers.ts +++ b/src/app/common/components/flows-table/flows-table.helpers.ts @@ -1,4 +1,3 @@ -import moment from "moment"; import { DatasetListFlowsDataFragment, FlowStartCondition, @@ -9,6 +8,8 @@ import { MaybeNull } from "src/app/common/app.types"; import AppValues from "src/app/common/app.values"; import { DataHelpers } from "src/app/common/data.helpers"; import { excludeAgoWord, isNil } from "../../app.helpers"; +import { format } from "date-fns/format"; +import { formatDistanceToNowStrict } from "date-fns"; export class DatasetFlowTableHelpers { public static descriptionColumnTableOptions(element: FlowSummaryDataFragment): { icon: string; class: string } { @@ -148,7 +149,8 @@ export class DatasetFlowTableHelpers { } case "FlowAbortedResult": - return `Aborted at ${moment(element.timing.finishedAt).format( + return `Aborted at ${format( + element.timing.finishedAt as string, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; @@ -210,27 +212,39 @@ export class DatasetFlowTableHelpers { case FlowStatus.Waiting: switch (node.startCondition?.__typename) { case "FlowStartConditionExecutor": - return `waiting for ${excludeAgoWord(moment(node.timing.awaitingExecutorSince ?? "").fromNow())}`; + return `waiting for ${excludeAgoWord(formatDistanceToNowStrict(node.timing.awaitingExecutorSince as string, { addSuffix: true }))}`; case "FlowStartConditionThrottling": case "FlowStartConditionSchedule": { - return `wake up time: ${moment(node.startCondition.wakeUpAt).fromNow()}`; + return `wake up time: ${formatDistanceToNowStrict(node.startCondition.wakeUpAt, { addSuffix: true })}`; } case "FlowStartConditionBatching": - return `deadline time: ${moment(node.startCondition.batchingDeadline).fromNow()}`; + return `deadline time: ${formatDistanceToNowStrict(node.startCondition.batchingDeadline, { addSuffix: true })}`; /* istanbul ignore next */ default: return "initializing..."; } case FlowStatus.Running: - return "running for " + excludeAgoWord(moment(node.timing.runningSince).fromNow()); + return ( + "running for " + + excludeAgoWord(formatDistanceToNowStrict(node.timing.runningSince as string, { addSuffix: true })) + ); case FlowStatus.Finished: switch (node.outcome?.__typename) { case "FlowSuccessResult": - return "finished " + moment(node.timing.finishedAt).fromNow(); + return ( + "finished " + + formatDistanceToNowStrict(node.timing.finishedAt as string, { addSuffix: true }) + ); case "FlowAbortedResult": - return "aborted " + moment(node.timing.finishedAt).fromNow(); + return ( + "aborted " + + formatDistanceToNowStrict(node.timing.finishedAt as string, { addSuffix: true }) + ); case "FlowFailedError": - return "failed " + moment(node.timing.runningSince).fromNow(); + return ( + "failed " + + formatDistanceToNowStrict(node.timing.runningSince as string, { addSuffix: true }) + ); /* istanbul ignore next */ default: throw new Error("Unknown flow outsome"); @@ -266,18 +280,18 @@ export class DatasetFlowTableHelpers { switch (node.startCondition?.__typename) { case "FlowStartConditionExecutor": return `waiting for: ${excludeAgoWord( - moment(node.timing.awaitingExecutorSince ?? "").format( - AppValues.CRON_EXPRESSION_DATE_FORMAT, - ), + format(node.timing.awaitingExecutorSince as string, AppValues.CRON_EXPRESSION_DATE_FORMAT), )}`; case "FlowStartConditionThrottling": case "FlowStartConditionSchedule": { - return `Wake up time: ${moment(node.startCondition.wakeUpAt).format( + return `Wake up time: ${format( + node.startCondition.wakeUpAt, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; } case "FlowStartConditionBatching": - return `Deadline time: ${moment(node.startCondition.batchingDeadline).format( + return `Deadline time: ${format( + node.startCondition.batchingDeadline, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; /* istanbul ignore next */ @@ -287,15 +301,18 @@ export class DatasetFlowTableHelpers { case FlowStatus.Finished: switch (node.outcome?.__typename) { case "FlowSuccessResult": - return `Completed time: ${moment(node.timing.finishedAt).format( + return `Completed time: ${format( + node.timing.finishedAt as string, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; case "FlowAbortedResult": - return `Aborted time: ${moment(node.timing.finishedAt).format( + return `Aborted time: ${format( + node.timing.finishedAt as string, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; case "FlowFailedError": - return `Start running time: ${moment(node.timing.runningSince).format( + return `Start running time: ${format( + node.timing.runningSince as string, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; /* istanbul ignore next */ @@ -304,7 +321,8 @@ export class DatasetFlowTableHelpers { } case FlowStatus.Running: - return `Start running time: ${moment(node.timing.runningSince).format( + return `Start running time: ${format( + node.timing.runningSince as string, AppValues.CRON_EXPRESSION_DATE_FORMAT, )}`; /* istanbul ignore next */ diff --git a/src/app/common/data.helpers.ts b/src/app/common/data.helpers.ts index 8923e365..50873675 100644 --- a/src/app/common/data.helpers.ts +++ b/src/app/common/data.helpers.ts @@ -12,10 +12,10 @@ import { MaybeUndefined } from "./app.types"; import { RxwebValidators } from "@rxweb/reactive-form-validators"; import { isValidCronExpression } from "./cron-expression-validator.helper"; import { ErrorPolicy, WatchQueryFetchPolicy } from "@apollo/client"; -import moment from "moment"; import { convertSecondsToHumanReadableFormat, removeAllLineBreaks } from "./app.helpers"; import { SliceUnit } from "../dataset-view/additional-components/dataset-settings-component/tabs/compacting/dataset-settings-compacting-tab.types"; import { DataRow, DatasetSchema, OperationColumnClassEnum } from "../interface/dataset.interface"; +import { differenceInSeconds } from "date-fns"; export class DataHelpers { public static readonly BLOCK_DESCRIBE_SEED = "Dataset initialized"; @@ -226,7 +226,7 @@ export class DataHelpers { } public static durationTask(startTime: string, endTime: string): string { - const durationSeconds = Math.round(moment.duration(moment(endTime).diff(moment(startTime))).asSeconds()); + const durationSeconds = Math.round(differenceInSeconds(endTime, startTime)); const result = convertSecondsToHumanReadableFormat(durationSeconds); return result ? result : "less than 1 second"; } diff --git a/src/app/components/display-time/display-time.component.spec.ts b/src/app/components/display-time/display-time.component.spec.ts index dd4e628b..f48af7e4 100644 --- a/src/app/components/display-time/display-time.component.spec.ts +++ b/src/app/components/display-time/display-time.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import timekeeper from "timekeeper"; - import { DisplayTimeComponent } from "./display-time.component"; import { SharedTestModule } from "src/app/common/shared-test.module"; @@ -30,7 +29,6 @@ describe("DisplayTimeComponent", () => { fixture = TestBed.createComponent(DisplayTimeComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it("should create", () => { @@ -41,28 +39,29 @@ describe("DisplayTimeComponent", () => { timekeeper.freeze(FROZEN_TIME); }); - function testRelativeTime(expectedResult: string, date: Date, threshold?: moment.argThresholdOpts): void { + function testRelativeTime(expectedResult: string, date: Date): void { component.data = date.toISOString(); - component.threshold = threshold; + fixture.detectChanges(); + expect(component.relativeTime).toBe(expectedResult); } [ { date: FROZEN_TIME, - expectedResult: "a few seconds ago", + expectedResult: "0 seconds ago", }, { date: TEN_SEC_AGO, - expectedResult: "a few seconds ago", + expectedResult: "10 seconds ago", }, { date: ALMOST_MINUTE_AGO, - expectedResult: "a few seconds ago", + expectedResult: "1 second ago", }, { date: MINUTE_AGO, - expectedResult: "a minute ago", + expectedResult: "1 minute ago", }, { date: TEN_MINUTES_AGO, @@ -70,7 +69,7 @@ describe("DisplayTimeComponent", () => { }, { date: HOUR_AGO, - expectedResult: "an hour ago", + expectedResult: "1 hour ago", }, { date: FIVE_HOURS_AGO, @@ -78,7 +77,7 @@ describe("DisplayTimeComponent", () => { }, { date: DAY_AGO, - expectedResult: "a day ago", + expectedResult: "1 day ago", }, { date: THREE_DAYS_AGO, @@ -90,11 +89,11 @@ describe("DisplayTimeComponent", () => { }, { date: MONTH_AGO, - expectedResult: "a month ago", + expectedResult: "1 month ago", }, { date: YEAR_AGO, - expectedResult: "a year ago", + expectedResult: "1 year ago", }, ].forEach(({ date, expectedResult: expectedResult }) => { it("#relativeTime no threshold", () => { @@ -105,7 +104,7 @@ describe("DisplayTimeComponent", () => { [ { date: DAY_AGO, - expectedResult: "a day ago", + expectedResult: "1 day ago", }, { date: TWO_DAYS_AGO, @@ -113,15 +112,15 @@ describe("DisplayTimeComponent", () => { }, { date: THREE_DAYS_AGO, - expectedResult: "28 Sep 2022", + expectedResult: "3 days ago", }, { date: WEEK_AGO, - expectedResult: "24 Sep 2022", + expectedResult: "7 days ago", }, ].forEach(({ date, expectedResult }) => { it("#relativeTime days threshold", () => { - testRelativeTime(expectedResult, date, { d: 3 }); + testRelativeTime(expectedResult, date); }); }); @@ -132,15 +131,15 @@ describe("DisplayTimeComponent", () => { }, { date: WEEK_AGO, - expectedResult: "24 Sep 2022", + expectedResult: "7 days ago", }, { date: EIGHT_DAYS_AGO, - expectedResult: "23 Sep 2022", + expectedResult: "8 days ago", }, ].forEach(({ date, expectedResult }) => { it("#relativeTime weeks threshold", () => { - testRelativeTime(expectedResult, date, { w: 1 }); + testRelativeTime(expectedResult, date); }); }); diff --git a/src/app/components/display-time/display-time.component.ts b/src/app/components/display-time/display-time.component.ts index 81ee5689..f8ee28c1 100644 --- a/src/app/components/display-time/display-time.component.ts +++ b/src/app/components/display-time/display-time.component.ts @@ -1,7 +1,7 @@ import { BasePropertyComponent } from "src/app/dataset-block/metadata-block/components/event-details/components/common/base-property/base-property.component"; import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; -import moment from "moment"; import AppValues from "src/app/common/app.values"; +import { format, formatDistanceStrict } from "date-fns"; @Component({ selector: "app-display-time", @@ -11,35 +11,19 @@ import AppValues from "src/app/common/app.values"; export class DisplayTimeComponent extends BasePropertyComponent { @Input({ required: true }) public data: string; @Input() public class?: string; - @Input() public threshold?: moment.argThresholdOpts; @Input() public dataTestId: string; get relativeTime(): string { - return this.convertToRelativeTime(this.data, this.threshold); + return this.dateTime(this.data); } get formatTitle(): string { - return moment(this.data).format(AppValues.DISPLAY_TOOLTIP_DATE_FORMAT); + return format(this.data, AppValues.CRON_EXPRESSION_DATE_FORMAT); } private dateTime(rfc3339: string): string { - const dt = moment(rfc3339); - return dt.format(AppValues.DISPLAY_DATE_FORMAT); - } - - private convertToRelativeTime(rfc3339: string, threshold?: moment.argThresholdOpts): string { - const dt = moment(rfc3339); - const delta = moment.duration(dt.diff(moment())); - if (threshold?.d) { - if (Math.abs(delta.asDays()) >= threshold.d) { - return this.dateTime(rfc3339); - } - } - if (threshold?.w) { - if (Math.abs(delta.asWeeks()) >= threshold.w) { - return this.dateTime(rfc3339); - } - } - return delta.humanize(true); + return formatDistanceStrict(rfc3339, new Date(), { + addSuffix: true, + }); } } diff --git a/src/app/components/lineage-graph/lineage-graph.component.spec.ts b/src/app/components/lineage-graph/lineage-graph.component.spec.ts index fb5c743a..6a32bb5f 100644 --- a/src/app/components/lineage-graph/lineage-graph.component.spec.ts +++ b/src/app/components/lineage-graph/lineage-graph.component.spec.ts @@ -116,13 +116,13 @@ describe("LineageGraphComponent", () => { expect(license?.textContent?.trim()).toEqual("No license"); const createdDate = findElementByDataTestId(fixture, "side-panel-dataset-created"); - expect(createdDate?.textContent?.trim()).toEqual("a month ago"); + expect(createdDate?.textContent?.trim()).toEqual("26 days ago"); const updatedDate = findElementByDataTestId(fixture, "side-panel-dataset-updated"); - expect(updatedDate?.textContent?.trim()).toEqual("a month ago"); + expect(updatedDate?.textContent?.trim()).toEqual("26 days ago"); const watermark = findElementByDataTestId(fixture, "side-panel-dataset-watermark"); - expect(watermark?.textContent?.trim()).toEqual("a month ago"); + expect(watermark?.textContent?.trim()).toEqual("26 days ago"); }); it("should check height for the lineage graph", () => { diff --git a/src/app/dataset-block/metadata-block/components/event-details/components/common/time-property/time-property.component.spec.ts b/src/app/dataset-block/metadata-block/components/event-details/components/common/time-property/time-property.component.spec.ts index a772b5e7..0cc1197f 100644 --- a/src/app/dataset-block/metadata-block/components/event-details/components/common/time-property/time-property.component.spec.ts +++ b/src/app/dataset-block/metadata-block/components/event-details/components/common/time-property/time-property.component.spec.ts @@ -15,6 +15,7 @@ describe("TimePropertyComponent", () => { fixture = TestBed.createComponent(TimePropertyComponent); component = fixture.componentInstance; + component.data = new Date("2021-10-03 12:00:00").toISOString(); fixture.detectChanges(); }); diff --git a/src/app/dataset-flow/dataset-flow-details/grafana-logs.service.ts b/src/app/dataset-flow/dataset-flow-details/grafana-logs.service.ts index fee9f476..b8489458 100644 --- a/src/app/dataset-flow/dataset-flow-details/grafana-logs.service.ts +++ b/src/app/dataset-flow/dataset-flow-details/grafana-logs.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { DatasetFlowByIdResponse, GrafanaFieldDescriptor } from "./dataset-flow-details.types"; -import moment from "moment"; import { FlowEventTaskChanged } from "src/app/api/kamu.graphql.interface"; +import { addSeconds, subSeconds } from "date-fns"; @Injectable({ providedIn: "root", @@ -36,14 +36,16 @@ export class GrafanaLogsService { this.availableFields.push({ key: "taskId", value: (events[0] as FlowEventTaskChanged).taskId }); this.availableFields.push({ key: "fromTime", - value: moment(flowDetails.flow.timing.runningSince).subtract(30, "seconds").valueOf().toString(), + value: subSeconds(flowDetails.flow.timing.runningSince as string, 30) + .valueOf() + .toString(), }); const finishedTime = flowDetails.flow.timing.finishedAt; this.availableFields.push({ key: "toTime", value: finishedTime - ? moment(finishedTime).add(30, "seconds").valueOf().toString() - : moment.now().toString(), + ? addSeconds(finishedTime, 30).valueOf().toString() + : Date.now().valueOf().toString(), }); } } diff --git a/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.mock.ts b/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.mock.ts index 7668ffda..8f9e1e6a 100644 --- a/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.mock.ts +++ b/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.mock.ts @@ -311,13 +311,13 @@ export const flowEventSubMessageResults: string[] = [ "", "Task #1", "", - "Wake up time at Feb 12th 2024, 8:22:30 PM +0200, shifted from 8:22:29 PM", - "Activating at Mar 13th 2024, 4:54:30 PM +0200", + "Wake up time at Feb 12th 2024 8:22:30 PM GMT+02:00, shifted from 8:22:29 PM", + "Activating at Mar 13th 2024 4:54:30 PM GMT+02:00", "Triggered by kamu", "Input dataset: kamu/alberta.case-details", - "Accumulated 100/500 records. Watermark modified. Deadline at Aug 6th 2022, 12:17:30 AM +0300", //1 + "Accumulated 100/500 records. Watermark modified. Deadline at Aug 6th 2022 12:17:30 AM GMT+03:00", //1 "Task #5", - "Wake up time at Mar 13th 2024, 5:54:30 PM +0200", + "Wake up time at Mar 13th 2024 5:54:30 PM GMT+02:00", "An error occurred, see logs for more details", "Dataset is up-to-date", "Ingested 100 new records in 10 new blocks", diff --git a/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.spec.ts b/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.spec.ts index 0ffd608a..cb96d1b9 100644 --- a/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.spec.ts +++ b/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.spec.ts @@ -10,17 +10,17 @@ import { mockFlowSummaryDataFragmentIngestResult, mockHistoryFragmentWithFinishedStatus, } from "./flow-details-history-tab.helpers.mock"; -import moment from "moment"; import { FlowStatus } from "src/app/api/kamu.graphql.interface"; import { mockDatasetExecuteTransformFlowSummaryData } from "src/app/common/components/flows-table/flows-table.helpers.mock"; +import timekeeper from "timekeeper"; describe("DatasetFlowDetailsHelpers", () => { beforeAll(() => { - moment.tz.setDefault("Europe/Kiev"); + timekeeper.freeze("2024-03-14T11:22:29+00:00"); }); afterAll(() => { - moment.tz.setDefault(); + timekeeper.reset(); }); mockFlowHistoryDataFragmentForDescriptions.forEach((item, index) => { diff --git a/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.ts b/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.ts index 916b87ae..1d1de42e 100644 --- a/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.ts +++ b/src/app/dataset-flow/dataset-flow-details/tabs/flow-details-history-tab/flow-details-history-tab.helpers.ts @@ -1,4 +1,4 @@ -import moment from "moment"; +import { format } from "date-fns/format"; import { FlowConfigSnapshotModified, FlowEventInitiated, @@ -132,9 +132,7 @@ export class DatasetFlowDetailsHelpers { return ""; case "FlowEventScheduledForActivation": { const event = flowEvent as FlowEventScheduledForActivation; - return `Activating at ${moment(event.scheduledForActivationAt).format( - AppValues.CRON_EXPRESSION_DATE_FORMAT, - )}`; + return `Activating at ${format(event.scheduledForActivationAt, AppValues.CRON_EXPRESSION_DATE_FORMAT)}`; } case "FlowEventTaskChanged": { const event = flowEvent as FlowEventTaskChanged; @@ -302,23 +300,20 @@ export class DatasetFlowDetailsHelpers { const startCondition: FlowStartCondition = startConditionEvent.startCondition; switch (startCondition.__typename) { case "FlowStartConditionThrottling": - return `Wake up time at ${moment(startCondition.wakeUpAt).format( + return `Wake up time at ${format( + startCondition.wakeUpAt, AppValues.CRON_EXPRESSION_DATE_FORMAT, - )}, shifted from ${moment(startCondition.shiftedFrom).format(AppValues.TIME_FORMAT)}`; + )}, shifted from ${format(startCondition.shiftedFrom, AppValues.TIME_FORMAT)}`; case "FlowStartConditionBatching": return `Accumulated ${startCondition.accumulatedRecordsCount}/${ startCondition.activeBatchingRule.minRecordsToAwait } records. Watermark ${ startCondition.watermarkModified ? "modified" : "unchanged" - }. Deadline at ${moment(startCondition.batchingDeadline).format( - AppValues.CRON_EXPRESSION_DATE_FORMAT, - )}`; + }. Deadline at ${format(startCondition.batchingDeadline, AppValues.CRON_EXPRESSION_DATE_FORMAT)}`; case "FlowStartConditionExecutor": return `Task #${startCondition.taskId}`; case "FlowStartConditionSchedule": - return `Wake up time at ${moment(startCondition.wakeUpAt).format( - AppValues.CRON_EXPRESSION_DATE_FORMAT, - )}`; + return `Wake up time at ${format(startCondition.wakeUpAt, AppValues.CRON_EXPRESSION_DATE_FORMAT)}`; /* istanbul ignore next */ default: throw new Error("Unknown start condition typename"); diff --git a/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.html b/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.html index 7caf4abe..16502dab 100644 --- a/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.html +++ b/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.html @@ -29,7 +29,7 @@
Time zone:
diff --git a/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts b/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts index db6316bc..729d1c6c 100644 --- a/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts +++ b/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts @@ -2,7 +2,6 @@ import { MaybeNullOrUndefined } from "../../../../../common/app.types"; import { ChangeDetectionStrategy, Component, inject, Input, OnInit } from "@angular/core"; import { OWL_DATE_TIME_FORMATS } from "@danielmoncada/angular-datetime-picker"; import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; -import moment from "moment-timezone"; import { DatasetBasicsFragment } from "src/app/api/kamu.graphql.interface"; import { BaseComponent } from "src/app/common/base.component"; import { MY_MOMENT_FORMATS } from "src/app/common/data.helpers"; @@ -10,6 +9,8 @@ import { DatasetCommitService } from "../../services/dataset-commit.service"; import { LoggedUserService } from "src/app/auth/logged-user.service"; import { finalize } from "rxjs"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { format, isAfter } from "date-fns"; +import { toZonedTime } from "date-fns-tz"; @Component({ selector: "app-edit-watermark-modal", @@ -33,19 +34,15 @@ export class EditWatermarkModalComponent extends BaseComponent implements OnInit } public get isDateValid(): boolean { - return moment(this.currentWatermark).isAfter(this.date); + return isAfter(this.currentWatermark as string, this.date); } public get currentTimeZone(): string { - return moment.tz.guess(); - } - - private pad(value: number): string { - return value < 10 ? "0" + value.toString() : value.toString(); + return Intl.DateTimeFormat().resolvedOptions().timeZone; } public createOffset(): string { - return moment().format("Z"); + return format(new Date(), "zzzz"); } public get minLocalWatermark(): string { @@ -53,7 +50,7 @@ export class EditWatermarkModalComponent extends BaseComponent implements OnInit } public commitSetWatermarkEvent(): void { - const date = moment.utc(this.date).tz(this.timeZone).format(); + const date = toZonedTime(this.date.toISOString(), this.timeZone).toISOString(); this.datasetCommitService .updateWatermark({ accountId: this.loggedUserService.currentlyLoggedInUser.id,