diff --git a/app/scripts/modules/core/src/cache/cacheInitializer.service.ts b/app/scripts/modules/core/src/cache/cacheInitializer.service.ts index ef88fb3be7f..bb37c67abad 100644 --- a/app/scripts/modules/core/src/cache/cacheInitializer.service.ts +++ b/app/scripts/modules/core/src/cache/cacheInitializer.service.ts @@ -1,6 +1,6 @@ -import * as moment from 'moment'; import { cloneDeep, uniq } from 'lodash'; import { module, noop } from 'angular'; +import { Duration } from 'luxon'; import { ApplicationReader } from 'core/application/service/ApplicationReader'; import { AccountService } from 'core/account/AccountService'; @@ -31,7 +31,7 @@ export class CacheInitializerService { private setConfigDefaults(key: string, config: ICacheConfig) { config.version = config.version || 1; - config.maxAge = config.maxAge || moment.duration(2, 'days').asMilliseconds(); + config.maxAge = config.maxAge || Duration.fromObject({ days: 2 }).as('milliseconds'); config.initializers = config.initializers || this.initializers[key] || ([] as any[]); config.onReset = config.onReset || [noop]; } diff --git a/app/scripts/modules/core/src/cache/deckCacheFactory.ts b/app/scripts/modules/core/src/cache/deckCacheFactory.ts index b6381a72124..8579ede7ba1 100644 --- a/app/scripts/modules/core/src/cache/deckCacheFactory.ts +++ b/app/scripts/modules/core/src/cache/deckCacheFactory.ts @@ -1,5 +1,5 @@ import { Cache, CacheFactory, CacheOptions, ItemInfo } from 'cachefactory'; -import * as moment from 'moment'; +import { Duration } from 'luxon'; import { SETTINGS } from 'core/config/settings'; @@ -154,7 +154,7 @@ export class DeckCacheFactory { private static getStats(cache: Cache): IStats { const keys = cache.keys(); - let ageMin = moment.now(), + let ageMin = Date.now(), ageMax = 0; keys.forEach((key: string) => { @@ -178,8 +178,8 @@ export class DeckCacheFactory { DeckCacheFactory.clearPreviousVersions(namespace, cacheId, currentVersion, cacheFactory); cacheFactory.createCache(key, { deleteOnExpire: 'aggressive', - maxAge: cacheConfig.maxAge || moment.duration(2, 'days').asMilliseconds(), - recycleFreq: moment.duration(5, 'seconds').asMilliseconds(), + maxAge: cacheConfig.maxAge || Duration.fromObject({ days: 2 }).as('milliseconds'), + recycleFreq: Duration.fromObject({ seconds: 5 }).as('milliseconds'), storageImpl: new SelfClearingLocalStorage(this.cacheProxy), storageMode: 'localStorage', storagePrefix: DeckCacheFactory.getStoragePrefix(key, currentVersion), diff --git a/app/scripts/modules/core/src/cache/infrastructureCacheConfig.ts b/app/scripts/modules/core/src/cache/infrastructureCacheConfig.ts index 8b4e9e0f9aa..c1b9d441f21 100644 --- a/app/scripts/modules/core/src/cache/infrastructureCacheConfig.ts +++ b/app/scripts/modules/core/src/cache/infrastructureCacheConfig.ts @@ -1,4 +1,4 @@ -import * as moment from 'moment'; +import { Duration } from 'luxon'; export interface IMaxAgeConfig { maxAge: number; @@ -36,16 +36,16 @@ export const INFRASTRUCTURE_CACHE_CONFIG: IInfrastructureCacheConfig = { version: 2, }, applications: { - maxAge: moment.duration(30, 'days').asMilliseconds(), // it gets refreshed every time the user goes to the application list, anyway + maxAge: Duration.fromObject({ days: 30 }).as('milliseconds'), // it gets refreshed every time the user goes to the application list, anyway }, loadBalancers: { - maxAge: moment.duration(1, 'hour').asMilliseconds(), + maxAge: Duration.fromObject({ hours: 1 }).as('milliseconds'), }, securityGroups: { version: 2, // increment to force refresh of cache on next page load - can be added to any cache }, instanceTypes: { - maxAge: moment.duration(7, 'days').asMilliseconds(), + maxAge: Duration.fromObject({ days: 7 }).as('milliseconds'), version: 2, }, healthChecks: { diff --git a/app/scripts/modules/core/src/entityTag/notifications/EphemeralPopover.tsx b/app/scripts/modules/core/src/entityTag/notifications/EphemeralPopover.tsx index ddd9acbc8ff..40e62d23313 100644 --- a/app/scripts/modules/core/src/entityTag/notifications/EphemeralPopover.tsx +++ b/app/scripts/modules/core/src/entityTag/notifications/EphemeralPopover.tsx @@ -2,8 +2,7 @@ import * as React from 'react'; import { HoverablePopover } from 'core/presentation'; import { IEntityTags } from 'core/domain'; - -import * as moment from 'moment'; +import { relativeTime } from 'core/utils/timeFormatters'; export interface IEphemeralPopoverProps { entity?: any; @@ -36,7 +35,7 @@ export class EphemeralPopover extends React.Component { const { ttl } = this.state; const isInPast = !!ttl && Date.now() > ttl; - const ttlPhrase = moment(ttl).fromNow(); + const ttlPhrase = relativeTime(ttl); return (
diff --git a/app/scripts/modules/core/src/history/recentHistory.service.ts b/app/scripts/modules/core/src/history/recentHistory.service.ts index 26ca65efbdc..ae3a174b004 100644 --- a/app/scripts/modules/core/src/history/recentHistory.service.ts +++ b/app/scripts/modules/core/src/history/recentHistory.service.ts @@ -1,5 +1,5 @@ import { module } from 'angular'; -import * as moment from 'moment'; +import { Duration } from 'luxon'; import { omit, omitBy, isUndefined, sortBy, find } from 'lodash'; import { UUIDGenerator } from 'core/utils/uuid.service'; @@ -27,7 +27,7 @@ const MAX_ITEMS = 5; export class RecentHistoryService { private static cache: ICache = DeckCacheFactory.createCache('history', 'user', { version: 3, - maxAge: moment.duration(90, 'days').asMilliseconds(), + maxAge: Duration.fromObject({ days: 90 }).as('milliseconds'), }); public static getItems(type: any): IRecentHistoryEntry[] { diff --git a/app/scripts/modules/core/src/orchestratedItem/orchestratedItem.transformer.ts b/app/scripts/modules/core/src/orchestratedItem/orchestratedItem.transformer.ts index bd6044a6003..b53fb7a895c 100644 --- a/app/scripts/modules/core/src/orchestratedItem/orchestratedItem.transformer.ts +++ b/app/scripts/modules/core/src/orchestratedItem/orchestratedItem.transformer.ts @@ -1,4 +1,4 @@ -import { duration } from 'moment'; +import { distanceInWords } from 'date-fns'; import { $log } from 'ngimport'; import { IOrchestratedItem, IOrchestratedItemVariable, ITask, ITaskStep } from 'core/domain'; @@ -87,7 +87,11 @@ export class OrchestratedItemTransformer { }, }, runningTime: { - get: () => duration(this.calculateRunningTime(item)()).humanize(), + get: () => { + const now = Date.now(); + const start = new Date(now - this.calculateRunningTime(item)()); + return distanceInWords(start, now, { includeSeconds: true }); + }, configurable: true, }, runningTimeInMs: { diff --git a/app/scripts/modules/core/src/pagerDuty/Pager.tsx b/app/scripts/modules/core/src/pagerDuty/Pager.tsx index 24455d98517..14adafd1561 100644 --- a/app/scripts/modules/core/src/pagerDuty/Pager.tsx +++ b/app/scripts/modules/core/src/pagerDuty/Pager.tsx @@ -4,7 +4,7 @@ import { UISref } from '@uirouter/react'; import SearchApi from 'js-worker-search'; import { groupBy } from 'lodash'; import { Debounce } from 'lodash-decorators'; -import * as moment from 'moment'; +import { DateTime } from 'luxon'; import { Observable } from 'rxjs'; import { AutoSizer, @@ -20,6 +20,7 @@ import { } from 'react-virtualized'; import { ApplicationReader, IApplicationSummary } from 'core/application'; +import { relativeTime } from 'core/utils/timeFormatters'; import { IOnCall, IPagerDutyService, PagerDutyReader } from './pagerDuty.read.service'; import { ReactInjector } from 'core/reactShims'; import { SETTINGS } from 'core/config'; @@ -40,7 +41,7 @@ export interface IUserList { export interface IOnCallsByService { users?: IUserList; applications: IApplicationSummary[]; - last: moment.Moment; + last: DateTime; service: IPagerDutyService; searchString: string; } @@ -125,13 +126,13 @@ export class Pager extends React.Component { return a.service.name.localeCompare(b.service.name); } if (sortBy === 'last') { - if (!a.last.isValid()) { + if (!a.last.isValid) { return 1; } - if (!b.last.isValid()) { + if (!b.last.isValid) { return -1; } - return a.last.isBefore(b.last) ? 1 : a.last.isAfter(b.last) ? -1 : 0; + return a.last.toMillis() < b.last.toMillis() ? 1 : a.last.toMillis() > b.last.toMillis() ? -1 : 0; } return 0; } @@ -256,7 +257,7 @@ export class Pager extends React.Component { users, applications: associatedApplications, service, - last: moment((service as any).lastIncidentTimestamp), + last: DateTime.fromISO((service as any).lastIncidentTimestamp), searchString: searchTokens.join(' '), }; if (onCallsByService.service.integration_key) { @@ -300,8 +301,8 @@ export class Pager extends React.Component { }; private lastIncidentRenderer = (data: TableCellProps): React.ReactNode => { - const time: moment.Moment = data.cellData; - return
{time.isValid() ? time.fromNow() : 'Never'}
; + const time: DateTime = data.cellData; + return
{time.isValid ? relativeTime(time.toMillis()) : 'Never'}
; }; private applicationRenderer = (data: TableCellProps): React.ReactNode => { diff --git a/app/scripts/modules/core/src/pipeline/config/stages/deploy/deployExecutionDetails.controller.js b/app/scripts/modules/core/src/pipeline/config/stages/deploy/deployExecutionDetails.controller.js index 4669f5c93de..3ed191f20b6 100644 --- a/app/scripts/modules/core/src/pipeline/config/stages/deploy/deployExecutionDetails.controller.js +++ b/app/scripts/modules/core/src/pipeline/config/stages/deploy/deployExecutionDetails.controller.js @@ -1,6 +1,6 @@ 'use strict'; -import moment from 'moment'; +import { Duration } from 'luxon'; import _ from 'lodash'; import { CloudProviderRegistry } from 'core/cloudProvider'; @@ -129,7 +129,8 @@ module.exports = angular $scope.showWaitingMessage = true; $scope.waitingForUpInstances = activeWaitTask.status === 'RUNNING'; const lastCapacity = stage.context.lastCapacityCheck; - const waitDurationExceeded = activeWaitTask.runningTimeInMs > moment.duration(5, 'minutes').asMilliseconds(); + const waitDurationExceeded = + activeWaitTask.runningTimeInMs > Duration.fromObject({ minutes: 5 }).as('milliseconds'); lastCapacity.total = lastCapacity.up + lastCapacity.down + diff --git a/app/scripts/modules/core/src/pipeline/config/stages/executionWindows/atlasGraph.component.ts b/app/scripts/modules/core/src/pipeline/config/stages/executionWindows/atlasGraph.component.ts index 1ab7fe7b28a..0b9bdee4c31 100644 --- a/app/scripts/modules/core/src/pipeline/config/stages/executionWindows/atlasGraph.component.ts +++ b/app/scripts/modules/core/src/pipeline/config/stages/executionWindows/atlasGraph.component.ts @@ -1,9 +1,9 @@ import { IController, module } from 'angular'; -import * as momentTimezone from 'moment-timezone'; import { has } from 'lodash'; import { Subject } from 'rxjs'; import { SETTINGS } from 'core/config/settings'; +import { DateTime, Duration } from 'luxon'; interface IExecutionWindow { displayStart: Date; @@ -258,27 +258,29 @@ class ExecutionWindowAtlasGraphController implements IController { } private createWindow(window: IExecutionWindow, dayOffset: number): IWindowData { - const today = new Date(); const zone: string = SETTINGS.defaultTimeZone; - - const start = momentTimezone - .tz(today, zone) - .hour(window.displayStart.getHours()) - .minute(window.displayStart.getMinutes()) - .seconds(window.displayStart.getSeconds()) - .milliseconds(window.displayStart.getMilliseconds()) - .subtract(dayOffset, 'days') - .toDate() - .getTime(), - end = momentTimezone - .tz(today, zone) - .hour(window.displayEnd.getHours()) - .minute(window.displayEnd.getMinutes()) - .seconds(window.displayEnd.getSeconds()) - .milliseconds(window.displayEnd.getMilliseconds()) - .subtract(dayOffset, 'days') - .toDate() - .getTime(); + const { displayEnd, displayStart } = window; + + const start = DateTime.local() + .setZone(zone) + .set({ + hour: displayStart.getHours(), + minute: displayStart.getMinutes(), + second: displayStart.getSeconds(), + millisecond: displayStart.getMilliseconds(), + }) + .minus(Duration.fromObject({ days: dayOffset })) + .toMillis(), + end = DateTime.local() + .setZone(zone) + .set({ + hour: displayEnd.getHours(), + minute: displayEnd.getMinutes(), + second: displayEnd.getSeconds(), + millisecond: displayEnd.getMilliseconds(), + }) + .minus(Duration.fromObject({ days: dayOffset })) + .toMillis(); return { start, end }; } diff --git a/app/scripts/modules/core/src/pipeline/config/stages/travis/travisStage.ts b/app/scripts/modules/core/src/pipeline/config/stages/travis/travisStage.ts index 6edde928bbc..d65ea29d3fe 100644 --- a/app/scripts/modules/core/src/pipeline/config/stages/travis/travisStage.ts +++ b/app/scripts/modules/core/src/pipeline/config/stages/travis/travisStage.ts @@ -1,12 +1,12 @@ import { IController, IScope, module } from 'angular'; import { IModalService } from 'angular-ui-bootstrap'; -import * as moment from 'moment'; import { Registry } from 'core/registry'; import { SETTINGS } from 'core/config/settings'; import { IgorService, BuildServiceType } from 'core/ci/igor.service'; import { IJobConfig, IParameterDefinitionList, IStage } from 'core/domain'; import { TravisExecutionLabel } from './TravisExecutionLabel'; +import { Duration } from 'luxon'; export interface ITravisStageViewState { mastersLoaded: boolean; @@ -175,7 +175,7 @@ module(TRAVIS_STAGE, []) const lines = stage.masterStage.context.buildInfo.number ? 1 : 0; return lines + (stage.masterStage.context.buildInfo.testResults || []).length; }, - defaultTimeoutMs: moment.duration(2, 'hours').asMilliseconds(), + defaultTimeoutMs: Duration.fromObject({ hours: 2 }).as('milliseconds'), validators: [{ type: 'requiredField', fieldName: 'job' }], strategy: true, }); diff --git a/app/scripts/modules/core/src/pipeline/config/stages/wercker/werckerStage.ts b/app/scripts/modules/core/src/pipeline/config/stages/wercker/werckerStage.ts index 50ca219e621..558f46910c9 100644 --- a/app/scripts/modules/core/src/pipeline/config/stages/wercker/werckerStage.ts +++ b/app/scripts/modules/core/src/pipeline/config/stages/wercker/werckerStage.ts @@ -1,12 +1,12 @@ import { IController, IScope, module } from 'angular'; import { IModalService } from 'angular-ui-bootstrap'; -import * as moment from 'moment'; import { Registry } from 'core/registry'; import { IgorService, BuildServiceType } from 'core/ci/igor.service'; import { IJobConfig, IParameterDefinitionList, IStage } from 'core/domain'; import { SETTINGS } from 'core/config/settings'; import { WerckerExecutionLabel } from './WerckerExecutionLabel'; +import { Duration } from 'luxon'; export interface IWerckerStageViewState { mastersLoaded: boolean; @@ -226,7 +226,7 @@ module(WERCKER_STAGE, []) const lines = stage.masterStage.context.buildInfo.number ? 1 : 0; return lines + (stage.masterStage.context.buildInfo.testResults || []).length; }, - defaultTimeoutMs: moment.duration(2, 'hours').asMilliseconds(), + defaultTimeoutMs: Duration.fromObject({ hours: 2 }).as('milliseconds'), validators: [{ type: 'requiredField', fieldName: 'job' }], strategy: true, }); diff --git a/app/scripts/modules/core/src/pipeline/filter/executionFilter.service.ts b/app/scripts/modules/core/src/pipeline/filter/executionFilter.service.ts index ced060a3d47..3a4da0d5ac4 100644 --- a/app/scripts/modules/core/src/pipeline/filter/executionFilter.service.ts +++ b/app/scripts/modules/core/src/pipeline/filter/executionFilter.service.ts @@ -1,8 +1,8 @@ import { chain, compact, find, flattenDeep, forOwn, get, groupBy, includes, uniq } from 'lodash'; import { Debounce } from 'lodash-decorators'; import { Subject } from 'rxjs'; -import * as moment from 'moment'; import { $log } from 'ngimport'; +import { DateTime, Duration } from 'luxon'; import { Application } from 'core/application/application.model'; import { IExecution, IExecutionGroup, IPipeline } from 'core/domain'; @@ -11,25 +11,51 @@ import { FilterModelService, ISortFilter } from 'core/filterModel'; import { Registry } from 'core/registry'; const boundaries = [ - { name: 'Today', after: () => moment().startOf('day') }, + { + name: 'Today', + after: () => + DateTime.local() + .startOf('day') + .toMillis(), + }, { name: 'Yesterday', after: () => - moment() + DateTime.local() .startOf('day') - .subtract(1, 'days'), + .minus(Duration.fromObject({ days: 1 })) + .toMillis(), + }, + { + name: 'This Week', + after: () => + DateTime.local() + .startOf('week') + .toMillis(), }, - { name: 'This Week', after: () => moment().startOf('week') }, { name: 'Last Week', after: () => - moment() + DateTime.local() .startOf('week') - .subtract(1, 'weeks'), + .minus(Duration.fromObject({ weeks: 1 })) + .toMillis(), + }, + { + name: 'Last Month', + after: () => + DateTime.local() + .startOf('month') + .toMillis(), + }, + { + name: 'This Year', + after: () => + DateTime.local() + .startOf('year') + .toMillis(), }, - { name: 'Last Month', after: () => moment().startOf('month') }, - { name: 'This Year', after: () => moment().startOf('year') }, - { name: 'Prior Years', after: () => moment(0) }, + { name: 'Prior Years', after: () => 0 }, ]; export class ExecutionFilterService { @@ -43,9 +69,10 @@ export class ExecutionFilterService { return groupBy( executions, execution => - boundaries.find(boundary => - // executions that were cancelled before ever starting will not have a startTime, just a buildTime - moment(execution.startTime || execution.buildTime).isAfter(boundary.after()), + boundaries.find( + boundary => + // executions that were cancelled before ever starting will not have a startTime, just a buildTime + (execution.startTime || execution.buildTime) > boundary.after(), ).name, ); } diff --git a/app/scripts/modules/core/src/pipeline/service/ExecutionsTransformer.ts b/app/scripts/modules/core/src/pipeline/service/ExecutionsTransformer.ts index 3a080072b5e..b7b68f6576b 100644 --- a/app/scripts/modules/core/src/pipeline/service/ExecutionsTransformer.ts +++ b/app/scripts/modules/core/src/pipeline/service/ExecutionsTransformer.ts @@ -1,4 +1,4 @@ -import { duration } from 'moment'; +import { duration } from 'core/utils/timeFormatters'; import { find, findLast, flattenDeep, get, has, maxBy, uniq, sortBy } from 'lodash'; import { Application } from 'core/application'; @@ -412,7 +412,7 @@ export class ExecutionsTransformer { // Update the runningTimeInMs function to account for the group Object.defineProperties(groupedStage, { runningTime: { - get: () => duration(this.calculateRunningTime(groupedStage)()).humanize(), + get: () => duration(this.calculateRunningTime(groupedStage)()), configurable: true, }, runningTimeInMs: { diff --git a/app/scripts/modules/core/src/pipeline/triggers/NextRunTag.tsx b/app/scripts/modules/core/src/pipeline/triggers/NextRunTag.tsx index f79bfd2f1fc..eeb22234419 100644 --- a/app/scripts/modules/core/src/pipeline/triggers/NextRunTag.tsx +++ b/app/scripts/modules/core/src/pipeline/triggers/NextRunTag.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; -import * as moment from 'moment'; +import { DateTime } from 'luxon'; import { IPipeline, ICronTrigger } from 'core/domain'; import { Popover } from 'core/presentation/Popover'; import { SETTINGS } from 'core/config/settings'; import { later } from 'core/utils/later/later'; -import { timestamp } from 'core/utils/timeFormatters'; +import { timestamp, relativeTime } from 'core/utils/timeFormatters'; export interface INextRunTagProps { pipeline: IPipeline; @@ -30,13 +30,13 @@ export class NextRunTag extends React.Component { - const timezoneOffsetInMs = moment.tz.zone(SETTINGS.defaultTimeZone).offset(Date.now()) * 60 * 1000; + const timezoneOffsetInMs = DateTime.local().setZone(SETTINGS.defaultTimeZone).offset * 60 * 1000; const nextRun = later .schedule(later.parse.cron(cron.cronExpression, true)) - .next(1, new Date(Date.now() - timezoneOffsetInMs)); + .next(1, new Date(Date.now() + timezoneOffsetInMs)); if (nextRun) { - nextTimes.push(nextRun.getTime() + timezoneOffsetInMs); + nextTimes.push(nextRun.getTime() - timezoneOffsetInMs); } }); if (nextTimes.length) { @@ -57,7 +57,7 @@ export class NextRunTag extends React.Component { - const nextDuration = moment(this.state.nextScheduled).fromNow(); + const nextDuration = relativeTime(this.state.nextScheduled); const visible = !this.props.pipeline.disabled && this.state.hasNextScheduled; return ( diff --git a/app/scripts/modules/core/src/task/PlatformHealthOverrideMessage.tsx b/app/scripts/modules/core/src/task/PlatformHealthOverrideMessage.tsx index f2ea1a12ab7..635e08c34ff 100644 --- a/app/scripts/modules/core/src/task/PlatformHealthOverrideMessage.tsx +++ b/app/scripts/modules/core/src/task/PlatformHealthOverrideMessage.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { get } from 'lodash'; -import * as moment from 'moment'; import { Application } from 'core/application/application.model'; import { IInstanceCounts, IStage, ITask, ITaskStep, ITimedItem } from 'core/domain'; import { Tooltip } from 'core/presentation'; +import { Duration } from 'luxon'; export interface IPlatformHealthOverrideMessageProps { application: Application; @@ -61,7 +61,7 @@ export class PlatformHealthOverrideMessage extends React.Component< showMessage = isRelevantTask && props.step.name === 'waitForUpInstances' && - props.step.runningTimeInMs > moment.duration(5, 'minutes').asMilliseconds() && + props.step.runningTimeInMs > Duration.fromObject({ minutes: 5 }).as('milliseconds') && lastCapacity.unknown > 0 && lastCapacity.unknown === lastCapacityTotal && !get(props.application, 'attributes.platformHealthOnly'); diff --git a/app/scripts/modules/core/src/task/platformHealthOverrideMessage.component.ts b/app/scripts/modules/core/src/task/platformHealthOverrideMessage.component.ts index 9183d8c801c..c85103e1cd2 100644 --- a/app/scripts/modules/core/src/task/platformHealthOverrideMessage.component.ts +++ b/app/scripts/modules/core/src/task/platformHealthOverrideMessage.component.ts @@ -1,9 +1,9 @@ import { IController, IComponentOptions, module } from 'angular'; import { get } from 'lodash'; -import * as moment from 'moment'; import { Application } from 'core/application/application.model'; import { IInstanceCounts, IStage, ITask, ITaskStep, ITimedItem } from 'core/domain'; +import { Duration } from 'luxon'; class PlatformHealthOverrideMessageController implements IController { public showMessage: boolean; @@ -35,7 +35,7 @@ class PlatformHealthOverrideMessageController implements IController { this.showMessage = isRelevantTask && this.step.name === 'waitForUpInstances' && - this.step.runningTimeInMs > moment.duration(5, 'minutes').asMilliseconds() && + this.step.runningTimeInMs > Duration.fromObject({ minutes: 5 }).as('milliseconds') && lastCapacity.unknown > 0 && lastCapacity.unknown === lastCapacityTotal && !get(this.application, 'attributes.platformHealthOnly'); diff --git a/app/scripts/modules/core/src/task/task.read.service.spec.js b/app/scripts/modules/core/src/task/task.read.service.spec.js index adb58a13692..ed5785becc6 100644 --- a/app/scripts/modules/core/src/task/task.read.service.spec.js +++ b/app/scripts/modules/core/src/task/task.read.service.spec.js @@ -169,7 +169,7 @@ describe('Service: taskReader', function() { execute(); - expect(task.runningTime).toBe('a few seconds'); + expect(task.runningTime).toBe('less than 5 seconds'); }); it('uses start time to calculate running time if endTime is not present', function() { @@ -181,7 +181,7 @@ describe('Service: taskReader', function() { execute(); - expect(task.runningTime).toBe('a few seconds'); + expect(task.runningTime).toBe('less than 5 seconds'); }); it('calculates running time based on start and end times', function() { diff --git a/app/scripts/modules/core/src/utils/SystemTimezone.tsx b/app/scripts/modules/core/src/utils/SystemTimezone.tsx index a7cba475ae3..bdcf52a5a72 100644 --- a/app/scripts/modules/core/src/utils/SystemTimezone.tsx +++ b/app/scripts/modules/core/src/utils/SystemTimezone.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; -import * as moment from 'moment'; +import { DateTime } from 'luxon'; import { SETTINGS } from 'core/config/settings'; -export class SystemTimezone extends React.Component { - public render() { - const zone = SETTINGS.defaultTimeZone; - return {moment.tz(Date.now(), zone).zoneAbbr()}; - } -} +export const SystemTimezone = () => { + const zone = SETTINGS.defaultTimeZone; + const time = DateTime.local().setZone(zone); + return {time.offsetNameShort}; +}; diff --git a/app/scripts/modules/core/src/utils/timeFormatters.spec.ts b/app/scripts/modules/core/src/utils/timeFormatters.spec.ts index 1386076a263..5402f05689d 100644 --- a/app/scripts/modules/core/src/utils/timeFormatters.spec.ts +++ b/app/scripts/modules/core/src/utils/timeFormatters.spec.ts @@ -1,8 +1,8 @@ import { IFilterService, mock } from 'angular'; -import * as moment from 'moment'; import { SETTINGS } from 'core/config/settings'; import { duration } from './timeFormatters'; +import { Settings } from 'luxon'; describe('Filter: timeFormatters', function() { beforeEach(function() { @@ -59,14 +59,17 @@ describe('Filter: timeFormatters', function() { expect(filter('a')).toBe('-'); }); it('returns formatted date when valid value is provided', function() { - expect(filter(1445707299020)).toBe('2015-10-24 17:21:39 GMT'); + expect(filter(1445707299020)).toBe('2015-10-24 17:21:39 UTC'); }); it('returns formatted date in user local time when valid value is provided', function() { SETTINGS.feature.displayTimestampsInUserLocalTime = true; - spyOn(moment.tz, 'guess').and.callFake(function() { - return 'Asia/Tokyo'; // +09:00 - }); - expect(filter(1445707299020)).toBe('2015-10-25 02:21:39 JST'); + const baseZone = Settings.defaultZoneName; + // NOTE: this maybe breaks, depending on where the user running the test is. + // For example, the test originally set the timezone to "Asia/Tokyo", which + // should output "JST". However, in the US, Chrome output "GMT+9". :( + Settings.defaultZoneName = 'Atlantic/Reykjavik'; + expect(filter(1445707299020)).toBe('2015-10-24 17:21:39 GMT'); + Settings.defaultZoneName = baseZone; }); }); diff --git a/app/scripts/modules/core/src/utils/timeFormatters.ts b/app/scripts/modules/core/src/utils/timeFormatters.ts index 7105367f350..c72de9482b3 100644 --- a/app/scripts/modules/core/src/utils/timeFormatters.ts +++ b/app/scripts/modules/core/src/utils/timeFormatters.ts @@ -1,6 +1,7 @@ import { module } from 'angular'; -import * as moment from 'moment'; +import { DateTime, Duration } from 'luxon'; import { memoize, MemoizedFunction } from 'lodash'; +import * as distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; import { react2angular } from 'react2angular'; import { SETTINGS } from 'core/config/settings'; @@ -13,40 +14,48 @@ export function duration(input: any) { if (!isInputValid(input)) { return '-'; } - const thisMoment = moment.utc(parseInt(input, 10)); - const days = Math.floor(moment.duration(input, 'milliseconds').asDays()); - const format = days || thisMoment.hours() ? 'HH:mm:ss' : 'mm:ss'; + // formatting does not support optionally omitting fields so we have to get + // a little weird with the format strings and the durations we send into them + const baseDuration = Duration.fromMillis(parseInt(input, 10)); + const days = Math.floor(baseDuration.as('days')); + // remove any days - we will add them manually if needed + const thisDuration = baseDuration.minus({ days: Math.floor(baseDuration.as('days')) }); + const format = thisDuration.days || Math.floor(thisDuration.as('hours')) ? 'hh:mm:ss' : 'mm:ss'; let dayLabel = ''; - if (thisMoment.isValid()) { + if (thisDuration.isValid) { if (days > 0) { dayLabel = days + 'd'; } } - return thisMoment.isValid() ? dayLabel + thisMoment.format(format) : '-'; + return thisDuration.isValid ? dayLabel + thisDuration.toFormat(format) : '-'; } export function timestamp(input: any) { if (!isInputValid(input)) { return '-'; } - const tz = SETTINGS.feature.displayTimestampsInUserLocalTime ? moment.tz.guess() : SETTINGS.defaultTimeZone; - const thisMoment = moment.tz(parseInt(input, 10), tz); - return thisMoment.isValid() ? thisMoment.format('YYYY-MM-DD HH:mm:ss z') : '-'; + const tz = SETTINGS.feature.displayTimestampsInUserLocalTime ? undefined : SETTINGS.defaultTimeZone; + const thisMoment = DateTime.fromMillis(parseInt(input, 10), { zone: tz }); + return thisMoment.isValid ? thisMoment.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ') : '-'; } export function relativeTime(input: any) { if (!isInputValid(input)) { return '-'; } - const thisMoment = moment(parseInt(input, 10)); - return thisMoment.isValid() ? thisMoment.fromNow() : '-'; + const now = Date.now(); + const inputNumber = parseInt(input, 10); + const inFuture = inputNumber > now; + const thisMoment = DateTime.fromMillis(inputNumber); + const baseText = distanceInWordsToNow(thisMoment.toJSDate(), { includeSeconds: true }); + return thisMoment.isValid ? `${inFuture ? 'in ' : ''}${baseText}${inFuture ? '' : ' ago'}` : '-'; } export const fastPropertyTime: ((input: any) => string) & MemoizedFunction = memoize((input: any) => { if (input) { input = input.replace('[UTC]', ''); - const thisMoment = moment(input); - return thisMoment.isValid() ? thisMoment.format('YYYY-MM-DD HH:mm:ss z') : '-'; + const thisMoment = DateTime.fromMillis(input); + return thisMoment.isValid ? thisMoment.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ') : '-'; } else { return '--'; } @@ -55,8 +64,8 @@ export const fastPropertyTime: ((input: any) => string) & MemoizedFunction = mem export const fastPropertyTtl = (input: any, seconds: number) => { if (input) { input = input.replace('[UTC]', ''); - const thisMoment = moment(input); - return thisMoment.isValid() ? thisMoment.add(seconds, 'second').format('YYYY-MM-DD HH:mm:ss z') : '-'; + const thisMoment = DateTime.fromMillis(input + seconds * 1000); + return thisMoment.isValid ? thisMoment.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ') : '-'; } else { return '--'; } diff --git a/app/scripts/modules/google/src/serverGroup/configure/wizard/loadBalancers/loadBalancerSelector.directive.spec.js b/app/scripts/modules/google/src/serverGroup/configure/wizard/loadBalancers/loadBalancerSelector.directive.spec.js index 2f844fab481..ae82eaa68b4 100644 --- a/app/scripts/modules/google/src/serverGroup/configure/wizard/loadBalancers/loadBalancerSelector.directive.spec.js +++ b/app/scripts/modules/google/src/serverGroup/configure/wizard/loadBalancers/loadBalancerSelector.directive.spec.js @@ -1,9 +1,9 @@ 'use strict'; const angular = require('angular'); -import * as momentTimezone from 'moment-timezone'; import { InfrastructureCaches, SETTINGS, TIME_FORMATTERS } from '@spinnaker/core'; +import { DateTime } from 'luxon'; require('./loadBalancerSelector.directive.html'); @@ -28,8 +28,9 @@ describe('Directive: GCE Load Balancers Selector', function() { InfrastructureCaches.get('loadBalancers').getStats = function() { return { ageMax: lastRefreshed }; }; - const m = momentTimezone.tz(lastRefreshed, SETTINGS.defaultTimeZone); - expectedTime = m.format('YYYY-MM-DD HH:mm:ss z'); + expectedTime = DateTime.fromMillis(lastRefreshed, { zone: SETTINGS.defaultTimeZone }).toFormat( + 'yyyy-MM-dd HH:mm:ss ZZZZ', + ); selector = angular.element(''); }), diff --git a/app/scripts/modules/kubernetes/src/v2/manifest/status/ManifestCondition.tsx b/app/scripts/modules/kubernetes/src/v2/manifest/status/ManifestCondition.tsx index 94eda102a0b..9bcfae4d399 100644 --- a/app/scripts/modules/kubernetes/src/v2/manifest/status/ManifestCondition.tsx +++ b/app/scripts/modules/kubernetes/src/v2/manifest/status/ManifestCondition.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import * as moment from 'moment'; import { relativeTime } from '@spinnaker/core'; +import { DateTime } from 'luxon'; export interface IKubernetesManifestCondition { status: string; @@ -16,14 +16,14 @@ export interface IKubernetesManifestConditionProps { export class ManifestCondition extends React.Component { public render() { const { condition } = this.props; - const transitionTime = moment(condition.lastTransitionTime); + const transitionTime = DateTime.fromISO(condition.lastTransitionTime); return [ {condition.status === 'True' && } {condition.status === 'False' && } {condition.status === 'Unknown' && ?} {condition.type} - {transitionTime.isValid() && relativeTime(transitionTime.valueOf())} + {transitionTime.isValid && relativeTime(transitionTime.toMillis())} ,
{condition.message}
, ]; diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/react/ManifestEvents.tsx b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/react/ManifestEvents.tsx index adf5bb4220d..d4b3b13f8ed 100644 --- a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/react/ManifestEvents.tsx +++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/react/ManifestEvents.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { get } from 'lodash'; -import * as moment from 'moment'; +import { DateTime } from 'luxon'; import { IManifest, IManifestEvent, relativeTime } from '@spinnaker/core'; import { JobManifestPodLogs } from './JobManifestPodLogs'; @@ -33,10 +33,10 @@ export class ManifestEvents extends React.Component { let firstEpochMilliseconds = 0; let lastEpochMilliseconds = 0; if (firstTimestamp) { - firstEpochMilliseconds = moment(firstTimestamp).valueOf(); + firstEpochMilliseconds = DateTime.fromISO(firstTimestamp).toMillis(); } if (lastTimestamp) { - lastEpochMilliseconds = moment(lastTimestamp).valueOf(); + lastEpochMilliseconds = DateTime.fromISO(lastTimestamp).toMillis(); } return (
diff --git a/app/scripts/modules/kubernetes/src/v2/resources/ResourceDetails.tsx b/app/scripts/modules/kubernetes/src/v2/resources/ResourceDetails.tsx index fa498363151..d84e5893eb3 100644 --- a/app/scripts/modules/kubernetes/src/v2/resources/ResourceDetails.tsx +++ b/app/scripts/modules/kubernetes/src/v2/resources/ResourceDetails.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import * as moment from 'moment'; + import { get } from 'lodash'; import { UISref } from '@uirouter/react'; +import { DateTime } from 'luxon'; import { IManifest, Spinner, CloudProviderLogo, CollapsibleSection, AccountTag, timestamp } from '@spinnaker/core'; import { KubernetesManifestService } from '../manifest/manifest.service'; import { ManifestEvents } from 'kubernetes/v2/pipelines/stages/deployManifest/react/ManifestEvents'; @@ -56,7 +57,8 @@ export class KubernetesResourceDetails extends React.Component< public render() { const { manifest } = this.state; const metadata = get(manifest, ['manifest', 'metadata'], null); - const creationUnixMs = get(metadata, 'creationTimestamp') && moment(metadata.creationTimestamp).valueOf(); + const creationUnixMs = + get(metadata, 'creationTimestamp') && DateTime.fromISO(metadata.creationTimestamp).toMillis(); return (
diff --git a/package.json b/package.json index 131db38abaf..eec483d0075 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@fortawesome/fontawesome-free": "^5.4.1", "@spinnaker/kayenta": "0.0.72", "@spinnaker/styleguide": "^1.0.12", + "@types/date-fns": "^2.6.0", "@types/memoize-one": "^3.1.1", "@types/react-tether": "^0.5.3", "@uirouter/react-hybrid": "0.3.9", @@ -54,6 +55,7 @@ "create-react-context": "^0.2.3", "d3-scale": "^2.0.0", "d3-shape": "^1.2.0", + "date-fns": "^1.30.1", "diff-match-patch": "^1.0.0", "dompurify": "^1.0.4", "formik": "^1.3.1", @@ -66,8 +68,8 @@ "js-yaml": "^3.9.0", "lodash": "^4.16.1", "lodash-decorators": "4.5.0", + "luxon": "^1.11.3", "memoize-one": "^4.0.2", - "moment-timezone": "^0.5.23", "n3-charts": "^2.0.18", "ngimport": "^0.6.0", "prop-types": "15.6.1", @@ -108,9 +110,9 @@ "@types/jqueryui": "^1.11.32", "@types/js-yaml": "3.5.30", "@types/lodash": "^4.14.64", + "@types/luxon": "^1.11.1", "@types/marked": "^0.0.28", "@types/minimist": "^1.2.0", - "@types/moment-timezone": "^0.2.34", "@types/node": "7.0.5", "@types/prop-types": "^15.5.2", "@types/react": "^16.4.18", diff --git a/yarn.lock b/yarn.lock index 23b0b3fb9c7..14174ed2b4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -445,6 +445,13 @@ "@types/d3-voronoi" "*" "@types/d3-zoom" "*" +"@types/date-fns@^2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1" + integrity sha1-sGLKRlYgApCb4MY6ZGftFzE2rME= + dependencies: + date-fns "*" + "@types/dompurify@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-0.0.29.tgz#3a774b80a709848e560fbf9ea62d5f716e79b6f2" @@ -509,6 +516,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" integrity sha512-ufQcVg4daO8xQ5kopxRHanqFdL4AI7ondQkV+2f+7mz3gvp0LkBx2zBRC6hfs3T87mzQFmf5Fck7Fi145Ul6NQ== +"@types/luxon@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.11.1.tgz#819d730cf8ada52bec9c4a1d94347d1250f22c5d" + integrity sha512-XBHQ7rzpOHyJudEQcMyoT67Np61FTb6S2jWqWQER/U7H2NAS+dpC8wv5T+6ygV5g/yJQdaojQbsJQiweool0Aw== + "@types/marked@^0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" @@ -524,13 +536,6 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= -"@types/moment-timezone@^0.2.34": - version "0.2.34" - resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.2.34.tgz#948e0aff82742a31dd63714d1aac9616bc375053" - integrity sha1-lI4K/4J0KjHdY3FNGqyWFrw3UFM= - dependencies: - moment ">=2.14.0" - "@types/node@*", "@types/node@7.0.5": version "7.0.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.5.tgz#96a0f0a618b7b606f1ec547403c00650210bfbb7" @@ -4344,6 +4349,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@*, date-fns@^1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + date-format@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" @@ -9063,6 +9073,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +luxon@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.11.3.tgz#e1542026473f708b03b102d475be64b2a3055a6e" + integrity sha512-/0jMa+JfTRBx1ixsSBs5ZPAQ32H+TPeP9BvgRf0Gi4VxCqhUpRNWagwupy6wA8MckazneKWBLCcwwAH8hkQamg== + macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" @@ -9570,23 +9585,11 @@ moment-timezone@^0.4.0: dependencies: moment ">= 2.6.0" -moment-timezone@^0.5.23: - version "0.5.23" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463" - integrity sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.6.0", moment@>=2.14.0, moment@~2.18.0: +"moment@>= 2.6.0", moment@~2.18.0: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" integrity sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8= -"moment@>= 2.9.0": - version "2.23.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" - integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== - monotone-convex-hull-2d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz#47f5daeadf3c4afd37764baa1aa8787a40eee08c"