diff --git a/ui/package-lock.json b/ui/package-lock.json index 35580fc2a..5f0ee2b5e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8769,6 +8769,11 @@ } } }, + "date-fns": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz", + "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -15834,11 +15839,6 @@ "traverse": "^0.6.6" } }, - "moment": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz", - "integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==" - }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -19251,11 +19251,6 @@ "prop-types": "^15.5.10" } }, - "react-moment": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.7.tgz", - "integrity": "sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g==" - }, "react-popper": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.3.tgz", diff --git a/ui/package.json b/ui/package.json index 9bcca56e9..bb20a771d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,6 +21,7 @@ "bricks.js": "1.8.0", "copy-to-clipboard": "3.3.1", "csshake": "1.5.3", + "date-fns": "2.14.0", "favico.js": "0.3.10", "fontfaceobserver": "2.1.0", "lodash.debounce": "4.0.8", @@ -30,7 +31,6 @@ "mobx": "5.15.4", "mobx-react-lite": "2.0.7", "mobx-stored": "1.1.0", - "moment": "2.26.0", "promise-retry": "2.0.1", "prop-types": "15.7.2", "qs": "6.9.4", @@ -47,7 +47,6 @@ "react-json-pretty": "2.2.0", "react-linkify": "0.2.2", "react-media": "1.10.0", - "react-moment": "0.9.7", "react-popper": "2.2.3", "react-resize-detector": "5.0.6", "react-reveal": "1.2.2", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 398a92f4f..8116e3244 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -45,12 +45,6 @@ const FaviconBadge = React.lazy(() => })) ); -const MomentPolledTimer = React.lazy(() => - import("Components/MomentPolledTimer").then((module) => ({ - default: module.MomentPolledTimer, - })) -); - interface UIDefaults { Refresh: number; HideFiltersWhenIdle: boolean; @@ -162,7 +156,6 @@ const App: FunctionComponent = ({ defaultFilters, uiDefaults }) => { - )); diff --git a/ui/src/Components/AlertAck/index.js b/ui/src/Components/AlertAck/index.js index 0dc963e8f..324eec524 100644 --- a/ui/src/Components/AlertAck/index.js +++ b/ui/src/Components/AlertAck/index.js @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; import { toJS } from "mobx"; import { useObserver } from "mobx-react-lite"; -import moment from "moment"; +import addSeconds from "date-fns/addSeconds"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck"; @@ -62,17 +62,16 @@ const AlertAck = ({ alertStore, silenceFormStore, group }) => { const commentPrefix = toJS( alertStore.settings.values.alertAcknowledgement.commentPrefix ); + const now = new Date(); c.push({ payload: GenerateAlertmanagerSilenceData( - moment.utc(), - moment.utc().add(durationSeconds, "seconds"), + now, + addSeconds(now, durationSeconds), MatchersFromGroup(group, [], group.alerts, true), author, `${ commentPrefix ? commentPrefix + " " : "" - }This alert was acknowledged using karma on ${moment - .utc() - .toString()}` + }This alert was acknowledged using karma on ${now.toUTCString()}` ), clusterName: clusterName, members: clusterMembers, diff --git a/ui/src/Components/AlertAck/index.test.js b/ui/src/Components/AlertAck/index.test.js index 008392c12..10cb4ced2 100644 --- a/ui/src/Components/AlertAck/index.test.js +++ b/ui/src/Components/AlertAck/index.test.js @@ -250,7 +250,7 @@ describe("", () => { await MountAndClick(); expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({ comment: - "PREFIX This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000", + "PREFIX This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT", createdBy: "karma/ui", endsAt: "2000-02-01T00:02:03.000Z", matchers: [ @@ -268,7 +268,7 @@ describe("", () => { await MountAndClick(); expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({ comment: - "This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000", + "This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT", createdBy: "me", endsAt: "2000-02-01T00:03:57.000Z", matchers: [ @@ -288,7 +288,7 @@ describe("", () => { await MountAndClick(); expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({ comment: - "FOO: This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000", + "FOO: This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT", createdBy: "auth@example.com", endsAt: "2000-02-01T00:03:42.000Z", matchers: [ @@ -309,7 +309,7 @@ describe("", () => { await MountAndClick(); expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({ comment: - "FOO: This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000", + "FOO: This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT", createdBy: "bob@example.com", endsAt: "2000-02-01T00:03:42.000Z", matchers: [ @@ -328,7 +328,7 @@ describe("", () => { await MountAndClick(); expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({ comment: - "FOO: This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000", + "FOO: This alert was acknowledged using karma on Tue, 01 Feb 2000 00:00:00 GMT", createdBy: "me", endsAt: "2000-02-01T00:03:42.000Z", matchers: [ diff --git a/ui/src/Components/DateFromNow/index.test.js b/ui/src/Components/DateFromNow/index.test.js new file mode 100644 index 000000000..2e79bea15 --- /dev/null +++ b/ui/src/Components/DateFromNow/index.test.js @@ -0,0 +1,43 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import addSeconds from "date-fns/addSeconds"; +import subSeconds from "date-fns/subSeconds"; + +import { DateFromNow } from "."; + +describe("", () => { + it("renders 'just now' for now", () => { + const tree = mount(); + expect(tree.text()).toBe("just now"); + }); + + it("renders 'a few seconds ago' for 35 seconds old timestamp", () => { + const tree = mount( + + ); + expect(tree.text()).toBe("a few seconds ago"); + }); + + it("renders 'in a few seconds' for a timestamp 35 seconds away", () => { + const tree = mount( + + ); + expect(tree.text()).toBe("in a few seconds"); + }); + + it("renders '1 minute ago' for 65 seconds old timestamp", () => { + const tree = mount( + + ); + expect(tree.text()).toBe("1 minute ago"); + }); + + it("renders 'in 1 minute' for a timestamp 65 seconds away", () => { + const tree = mount( + + ); + expect(tree.text()).toBe("in 1 minute"); + }); +}); diff --git a/ui/src/Components/DateFromNow/index.tsx b/ui/src/Components/DateFromNow/index.tsx new file mode 100644 index 000000000..d67a58750 --- /dev/null +++ b/ui/src/Components/DateFromNow/index.tsx @@ -0,0 +1,32 @@ +import React, { FunctionComponent, useState, useEffect } from "react"; + +import parseISO from "date-fns/parseISO"; +import differenceInSeconds from "date-fns/differenceInSeconds"; +import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict"; + +const formatLabel = (timestamp: string) => { + const ts = parseISO(timestamp); + const diff = differenceInSeconds(new Date(), ts); + if (diff > 0 && diff < 60) return "a few seconds ago"; + if (diff < 0 && diff >= -60) return "in a few seconds"; + if (diff === 0) return "just now"; + return formatDistanceToNowStrict(ts, { + addSuffix: true, + }); +}; + +export const DateFromNow: FunctionComponent<{ timestamp: string }> = ({ + timestamp, +}) => { + const [label, setLabel] = useState(formatLabel(timestamp)); + + useEffect(() => { + const timer = setInterval( + () => setLabel(formatLabel(timestamp)), + 30 * 1000 + ); + return () => clearInterval(timer); + }, [timestamp]); + + return {label}; +}; diff --git a/ui/src/Components/Fetcher/index.js b/ui/src/Components/Fetcher/index.js index 570290157..4c297a6cd 100644 --- a/ui/src/Components/Fetcher/index.js +++ b/ui/src/Components/Fetcher/index.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { reaction } from "mobx"; -import moment from "moment"; +import addSeconds from "date-fns/addSeconds"; import { AlertStore, AlertStoreStatuses } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; @@ -50,12 +50,12 @@ const Fetcher = ({ alertStore, settingsStore }) => { }; const fetchIfIdle = () => { - const nextTick = moment(alertStore.status.lastUpdateAt).add( - settingsStore.fetchConfig.config.interval, - "seconds" + const nextTick = addSeconds( + alertStore.status.lastUpdateAt, + settingsStore.fetchConfig.config.interval ); - const pastDeadline = moment().isSameOrAfter(nextTick); + const pastDeadline = new Date() >= nextTick; const status = alertStore.status.value.toString(); const updateInProgress = diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js index 733192083..6e9725fa9 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js @@ -5,8 +5,6 @@ import { useObserver } from "mobx-react-lite"; import { Manager, Reference, Popper } from "react-popper"; -import Moment from "react-moment"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown"; import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; @@ -17,6 +15,7 @@ import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore, SilenceTabNames } from "Stores/SilenceFormStore"; import { FetchPauser } from "Components/FetchPauser"; import { DropdownSlide } from "Components/Animations/DropdownSlide"; +import { DateFromNow } from "Components/DateFromNow"; import { useOnClickOutside } from "Hooks/useOnClickOutside"; const onSilenceClick = (alertStore, silenceFormStore, group, alert) => { @@ -130,7 +129,7 @@ const AlertMenu = ({ style={{ width: "0.8rem" }} icon={faCaretDown} /> - {alert.startsAt} + )} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap index 78cdf0f09..3c344eb53 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap @@ -75,9 +75,7 @@ exports[` matches snapshot when inhibited 1`] = ` > - + 1 day ago
matches snapshot with showAlertmanagers=false showReceiver=fa > - + 1 day ago
", () => { it("alert timestamp is updated every minute", () => { jest.useFakeTimers(); - Moment.startPooledTimer(); advanceTo(new Date(Date.UTC(2018, 7, 14, 17, 36, 41))); const alert = MockedAlert(); const group = MockAlertGroup({}, [alert], [], {}, {}); const tree = MountedAlert(alert, group, false, false); - expect(tree.find("time").text()).toBe("a few seconds ago"); + expect( + tree + .find("span.components-label.badge.badge-secondary.cursor-pointer") + .at(0) + .text() + ).toBe("just now"); + + advanceTo(new Date(Date.UTC(2018, 7, 14, 17, 36, 42))); + act(() => jest.advanceTimersByTime(31 * 1000)); + expect( + tree + .find("span.components-label.badge.badge-secondary.cursor-pointer") + .at(0) + .text() + ).toBe("a few seconds ago"); advanceTo(new Date(Date.UTC(2018, 7, 14, 17, 37, 41))); - act(() => jest.advanceTimersByTime(61 * 1000)); - expect(tree.find("time").text()).toBe("a minute ago"); + act(() => jest.advanceTimersByTime(31 * 1000)); + expect( + tree + .find("span.components-label.badge.badge-secondary.cursor-pointer") + .at(0) + .text() + ).toBe("1 minute ago"); advanceTo(new Date(Date.UTC(2018, 7, 14, 18, 36, 41))); - jest.advanceTimersByTime(61 * 1000); - expect(tree.find("time").text()).toBe("an hour ago"); + act(() => jest.advanceTimersByTime(31 * 1000)); + expect( + tree + .find("span.components-label.badge.badge-secondary.cursor-pointer") + .at(0) + .text() + ).toBe("1 hour ago"); advanceTo(new Date(Date.UTC(2018, 7, 14, 19, 36, 41))); - jest.advanceTimersByTime(61 * 1000); - expect(tree.find("time").text()).toBe("2 hours ago"); + act(() => jest.advanceTimersByTime(31 * 1000)); + expect( + tree + .find("span.components-label.badge.badge-secondary.cursor-pointer") + .at(0) + .text() + ).toBe("2 hours ago"); + + advanceTo(new Date(Date.UTC(2018, 7, 16, 19, 36, 41))); + act(() => jest.advanceTimersByTime(31 * 1000)); + expect( + tree + .find("span.components-label.badge.badge-secondary.cursor-pointer") + .at(0) + .text() + ).toBe("2 days ago"); }); }); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap index 539472359..f17d4404e 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap @@ -285,10 +285,7 @@ exports[` mathes snapshot when silence is rendered 1`] = ` — me@example.com - Expired - + Expired 14 hours ago
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js index d298d3966..025b3e82a 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js @@ -4,7 +4,6 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; -import moment from "moment"; import { advanceTo, clear } from "jest-date-mock"; import { @@ -48,7 +47,7 @@ beforeEach(() => { alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); group = MockGroup(); - advanceTo(moment.utc([2000, 0, 1, 15, 0, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 15, 0, 0))); alertStore.data.receivers = ["by-cluster-service", "by-name"]; }); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js index 6aaf803c0..5b9b9259c 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js @@ -3,8 +3,6 @@ import { act } from "react-dom/test-utils"; import { mount } from "enzyme"; -import moment from "moment"; - import { MockAlert, MockAlertGroup } from "__mocks__/Alerts"; import { MockThemeContext } from "__mocks__/Theme"; import { AlertStore } from "Stores/AlertStore"; @@ -52,9 +50,9 @@ afterEach(() => { const MockAlerts = (alertCount) => { for (let i = 1; i <= alertCount; i++) { let alert = MockAlert([], { instance: `instance${i}` }, "active"); - const startsAt = moment().toISOString(); - alert.startsAt = startsAt; - alert.alertmanager[0].startsAt = startsAt; + const startsAt = new Date(); + alert.startsAt = startsAt.toISOString(); + alert.alertmanager[0].startsAt = startsAt.toISOString(); group.alerts.push(alert); } }; diff --git a/ui/src/Components/ManagedSilence/DeleteSilence.test.js b/ui/src/Components/ManagedSilence/DeleteSilence.test.js index fe6a94e18..32d078eb1 100644 --- a/ui/src/Components/ManagedSilence/DeleteSilence.test.js +++ b/ui/src/Components/ManagedSilence/DeleteSilence.test.js @@ -5,8 +5,6 @@ import { mount } from "enzyme"; import { advanceTo, clear } from "jest-date-mock"; -import moment from "moment"; - import { EmptyAPIResponse } from "__mocks__/Fetch"; import { MockSilence } from "__mocks__/Alerts"; import { PressKey } from "__mocks__/PressKey"; @@ -22,7 +20,7 @@ let cluster; let silence; beforeEach(() => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 0))); jest.useFakeTimers(); alertStore = new AlertStore([]); @@ -297,7 +295,7 @@ describe("", () => { tree.find(".btn-danger").simulate("click"); expect(useFetchDelete).toHaveBeenCalledTimes(1); - advanceTo(moment.utc([2000, 0, 1, 0, 30, 1])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 1))); tree.find(".btn-danger").simulate("click"); expect(useFetchDelete).toHaveBeenCalledTimes(2); }); diff --git a/ui/src/Components/ManagedSilence/SilenceDetails.js b/ui/src/Components/ManagedSilence/SilenceDetails.js index b790c79f8..1f89c72a6 100644 --- a/ui/src/Components/ManagedSilence/SilenceDetails.js +++ b/ui/src/Components/ManagedSilence/SilenceDetails.js @@ -1,8 +1,7 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import moment from "moment"; -import Moment from "react-moment"; +import parseISO from "date-fns/parseISO"; import copy from "copy-to-clipboard"; @@ -22,6 +21,7 @@ import { SilenceFormStore } from "Stores/SilenceFormStore"; import { QueryOperators } from "Common/Query"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { RenderLinkAnnotation } from "Components/Grid/AlertGrid/AlertGroup/Annotation"; +import { DateFromNow } from "Components/DateFromNow"; import { DeleteSilence } from "./DeleteSilence"; const SilenceIDCopyButton = ({ id }) => { @@ -51,7 +51,7 @@ const SilenceDetails = ({ onEditSilence, isUpper, }) => { - let isExpired = moment(silence.endsAt) < moment(); + const isExpired = parseISO(silence.endsAt) < new Date(); let expiresClass = ""; let expiresLabel = "Expires"; if (isExpired) { @@ -78,7 +78,7 @@ const SilenceDetails = ({ icon={faCalendarCheck} fixedWidth /> - Started {silence.startsAt} + Started - {expiresLabel} {silence.endsAt} + {expiresLabel}
diff --git a/ui/src/Components/ManagedSilence/SilenceDetails.test.js b/ui/src/Components/ManagedSilence/SilenceDetails.test.js index b4d3cbdd2..40b613630 100644 --- a/ui/src/Components/ManagedSilence/SilenceDetails.test.js +++ b/ui/src/Components/ManagedSilence/SilenceDetails.test.js @@ -6,7 +6,6 @@ import toDiffableHtml from "diffable-html"; import copy from "copy-to-clipboard"; -import moment from "moment"; import { advanceTo, clear } from "jest-date-mock"; import { MockSilence } from "__mocks__/Alerts"; @@ -67,14 +66,14 @@ const MountedSilenceDetails = () => { describe("", () => { it("unexpired silence endsAt label doesn't use 'danger' class", () => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 0))); const tree = MountedSilenceDetails(); const endsAt = tree.find("span.badge").at(1); expect(toDiffableHtml(endsAt.html())).not.toMatch(/text-danger/); }); it("expired silence endsAt label uses 'danger' class", () => { - advanceTo(moment.utc([2000, 0, 1, 23, 0, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 23, 0, 0))); const tree = MountedSilenceDetails(); const endsAt = tree.find("span.badge").at(1); expect(toDiffableHtml(endsAt.html())).toMatch(/text-danger/); diff --git a/ui/src/Components/ManagedSilence/SilenceProgress.js b/ui/src/Components/ManagedSilence/SilenceProgress.js index 66a4d0611..4aea0af90 100644 --- a/ui/src/Components/ManagedSilence/SilenceProgress.js +++ b/ui/src/Components/ManagedSilence/SilenceProgress.js @@ -2,16 +2,19 @@ import React, { useEffect, useState } from "react"; import { useObserver } from "mobx-react-lite"; -import moment from "moment"; -import Moment from "react-moment"; +import parseISO from "date-fns/parseISO"; +import getUnixTime from "date-fns/getUnixTime"; import { APISilence } from "Models/API"; +import { DateFromNow } from "Components/DateFromNow"; import "./SilenceProgress.scss"; const calculatePercent = (startsAt, endsAt) => { - const durationDone = moment().unix() - moment(startsAt).unix(); - const durationTotal = moment(endsAt).unix() - moment(startsAt).unix(); + const durationDone = + getUnixTime(new Date()) - getUnixTime(parseISO(startsAt)); + const durationTotal = + getUnixTime(parseISO(endsAt)) - getUnixTime(parseISO(startsAt)); return Math.floor((durationDone / durationTotal) * 100); }; @@ -28,13 +31,13 @@ const SilenceProgress = ({ silence }) => { }, [silence.startsAt, silence.endsAt]); return useObserver(() => - moment(silence.endsAt) < moment() ? ( + parseISO(silence.endsAt) < new Date() ? ( - Expired {silence.endsAt} + Expired ) : ( - Expires {silence.endsAt} + Expires
{ describe("", () => { it("renders with class 'danger' and no progressbar when expired", () => { - advanceTo(moment.utc([2001, 0, 1, 23, 0, 0])); + advanceTo(new Date(Date.UTC(2001, 0, 1, 23, 0, 0))); const tree = MountedSilenceProgress(); expect(toDiffableHtml(tree.html())).toMatch(/badge-danger/); - expect(tree.text()).toMatch(/Expired a year ago/); + expect(tree.text()).toMatch(/Expired 1 year ago/); }); it("progressbar uses class 'danger' when > 90%", () => { - advanceTo(moment.utc([2000, 0, 1, 0, 55, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 55, 0))); const tree = MountedSilenceProgress(); expect(toDiffableHtml(tree.html())).toMatch(/progress-bar bg-danger/); }); it("progressbar uses class 'danger' when > 75%", () => { - advanceTo(moment.utc([2000, 0, 1, 0, 50, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 50, 0))); const tree = MountedSilenceProgress(); expect(toDiffableHtml(tree.html())).toMatch(/progress-bar bg-warning/); }); it("progressbar uses class 'success' when <= 75%", () => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 0))); const tree = MountedSilenceProgress(); expect(toDiffableHtml(tree.html())).toMatch(/progress-bar bg-success/); }); it("progressbar is updated every 30 seconds", () => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 0))); const tree = MountedSilenceProgress(); expect(toDiffableHtml(tree.html())).toMatch(/progress-bar bg-success/); - advanceTo(moment.utc([2000, 0, 1, 0, 50, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 50, 0))); act(() => jest.runOnlyPendingTimers()); expect(toDiffableHtml(tree.html())).toMatch(/progress-bar bg-warning/); - advanceTo(moment.utc([2000, 0, 1, 0, 55, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 55, 0))); act(() => jest.runOnlyPendingTimers()); expect(toDiffableHtml(tree.html())).toMatch(/progress-bar bg-danger/); }); diff --git a/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap b/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap index 1f04e3c4f..ff87e4d06 100644 --- a/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap +++ b/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap @@ -28,10 +28,7 @@ exports[` Matches snapshot when collapsed 1`] = ` — me@example.com - Expired - + Expired 20 years ago
@@ -98,10 +95,7 @@ exports[` Matches snapshot when collapsed and multiple cluster ha
- Expired - + Expired 20 years ago
diff --git a/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap b/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap index a1747014b..c5a7e39c7 100644 --- a/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap +++ b/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap @@ -32,10 +32,7 @@ exports[` matches snapshot when collapsed 1`] = ` — me@example.com - Expires - + Expires in 30 minutes
matches snapshot with expaned details 1`] = ` > - Started - + Started 30 minutes ago matches snapshot with expaned details 1`] = ` > - Expires - + Expires in 30 minutes
diff --git a/ui/src/Components/ManagedSilence/index.test.js b/ui/src/Components/ManagedSilence/index.test.js index 1a86ff9b7..656e1ac0d 100644 --- a/ui/src/Components/ManagedSilence/index.test.js +++ b/ui/src/Components/ManagedSilence/index.test.js @@ -4,7 +4,6 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; -import moment from "moment"; import { advanceTo, clear } from "jest-date-mock"; import { MockSilence } from "__mocks__/Alerts"; @@ -20,7 +19,7 @@ let cluster; let silence; beforeEach(() => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 0))); alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); @@ -169,7 +168,7 @@ describe("", () => { }); it("shows Recreate button on expired silence", () => { - advanceTo(moment.utc([2000, 0, 1, 23, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 23, 30, 0))); const tree = MountedManagedSilence(); tree.find("svg.text-muted.cursor-pointer").simulate("click"); diff --git a/ui/src/Components/MomentPolledTimer/index.js b/ui/src/Components/MomentPolledTimer/index.js deleted file mode 100644 index 9b646ffbc..000000000 --- a/ui/src/Components/MomentPolledTimer/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { useEffect } from "react"; - -import Moment from "react-moment"; - -export function MomentPolledTimer() { - useEffect(() => { - Moment.startPooledTimer(); - return () => Moment.clearPooledTimer(); - }, []); - - return ; -} diff --git a/ui/src/Components/NavBar/index.test.js b/ui/src/Components/NavBar/index.test.js index c0061a31e..370eb8d3a 100644 --- a/ui/src/Components/NavBar/index.test.js +++ b/ui/src/Components/NavBar/index.test.js @@ -3,8 +3,6 @@ import { act } from "react-dom/test-utils"; import { mount } from "enzyme"; -import moment from "moment"; - import { MockThemeContext } from "__mocks__/Theme"; import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; @@ -26,8 +24,8 @@ beforeEach(() => { silenceFormStore = new SilenceFormStore(); settingsStore.filterBarConfig.config.autohide = true; // fix startsAt & endsAt dates so they don't change between tests - silenceFormStore.data.startsAt = moment([2018, 1, 30, 10, 25, 50]).utc(); - silenceFormStore.data.endsAt = moment([2018, 1, 30, 11, 25, 50]).utc(); + silenceFormStore.data.startsAt = new Date(Date.UTC(2018, 1, 30, 10, 25, 50)); + silenceFormStore.data.endsAt = new Date(Date.UTC(2018, 1, 30, 11, 25, 50)); }); afterEach(() => { diff --git a/ui/src/Components/SilenceModal/Browser/index.test.js b/ui/src/Components/SilenceModal/Browser/index.test.js index b07f610dd..4e6f7ae44 100644 --- a/ui/src/Components/SilenceModal/Browser/index.test.js +++ b/ui/src/Components/SilenceModal/Browser/index.test.js @@ -5,7 +5,6 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; -import moment from "moment"; import { advanceTo, clear } from "jest-date-mock"; import { MockSilence } from "__mocks__/Alerts"; @@ -25,7 +24,7 @@ let cluster; let silence; beforeEach(() => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 0])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 0))); jest.useFakeTimers(); alertStore = new AlertStore([]); @@ -108,13 +107,13 @@ describe("", () => { settingsStore.fetchConfig.config.interval = 1; MountedBrowser(); - advanceTo(moment.utc([2000, 0, 1, 0, 30, 2])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 2))); act(() => jest.runOnlyPendingTimers()); - advanceTo(moment.utc([2000, 0, 1, 0, 30, 4])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 4))); act(() => jest.runOnlyPendingTimers()); - advanceTo(moment.utc([2000, 0, 1, 0, 30, 6])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 6))); act(() => jest.runOnlyPendingTimers()); expect(useFetchGet.fetch.calls).toHaveLength(4); @@ -364,7 +363,7 @@ describe("", () => { tree.unmount(); act(() => { - advanceTo(moment.utc([2000, 0, 1, 0, 30, 59])); + advanceTo(new Date(Date.UTC(2000, 0, 1, 0, 30, 59))); jest.runOnlyPendingTimers(); }); diff --git a/ui/src/Components/SilenceModal/DateTimeSelect/HourMinute.js b/ui/src/Components/SilenceModal/DateTimeSelect/HourMinute.js index 158851c65..79d167f20 100644 --- a/ui/src/Components/SilenceModal/DateTimeSelect/HourMinute.js +++ b/ui/src/Components/SilenceModal/DateTimeSelect/HourMinute.js @@ -3,8 +3,6 @@ import PropTypes from "prop-types"; import { observer } from "mobx-react-lite"; -import moment from "moment"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleUp } from "@fortawesome/free-solid-svg-icons/faAngleUp"; import { faAngleDown } from "@fortawesome/free-solid-svg-icons/faAngleDown"; @@ -59,8 +57,8 @@ const HourMinute = observer( } }; - const hour = dateValue.hour(); - const minute = dateValue.minute(); + const hour = dateValue.getHours(); + const minute = dateValue.getMinutes(); return (
{ + const now = new Date(); + now.setSeconds(0); + return now; +}; + const OffsetBadge = ({ startDate, endDate, prefixLabel }) => { - const days = endDate.diff(startDate, "days"); - const hours = endDate.diff(startDate, "hours") % 24; - const minutes = endDate.diff(startDate, "minutes") % 60; + const days = differenceInDays(endDate, startDate); + const hours = differenceInHours(endDate, startDate) % 24; + const minutes = differenceInMinutes(endDate, startDate) % 60; return ( @@ -27,8 +36,8 @@ const OffsetBadge = ({ startDate, endDate, prefixLabel }) => { ); }; OffsetBadge.propTypes = { - startDate: PropTypes.instanceOf(moment).isRequired, - endDate: PropTypes.instanceOf(moment).isRequired, + startDate: PropTypes.instanceOf(Date).isRequired, + endDate: PropTypes.instanceOf(Date).isRequired, prefixLabel: PropTypes.string.isRequired, }; @@ -60,28 +69,26 @@ const TabContentStart = ({ silenceFormStore }) => {
{ - const startsAt = moment(val); - startsAt.set({ - hour: silenceFormStore.data.startsAt.hour(), - minute: silenceFormStore.data.startsAt.minute(), - second: 0, - }); + const startsAt = new Date(val); + startsAt.setHours(silenceFormStore.data.startsAt.getHours()); + startsAt.setMinutes(silenceFormStore.data.startsAt.getMinutes()); + startsAt.setSeconds(0); silenceFormStore.data.startsAt = startsAt; silenceFormStore.data.verifyStarEnd(); }} selectedDays={{ - from: silenceFormStore.data.startsAt.toDate(), - to: silenceFormStore.data.endsAt.toDate(), + from: silenceFormStore.data.startsAt, + to: silenceFormStore.data.endsAt, }} modifiers={{ - start: silenceFormStore.data.startsAt.toDate(), - end: silenceFormStore.data.endsAt.toDate(), + start: silenceFormStore.data.startsAt, + end: silenceFormStore.data.endsAt, }} />
@@ -102,28 +109,26 @@ const TabContentEnd = ({ silenceFormStore }) => {
{ - const endsAt = moment(val); - endsAt.set({ - hour: silenceFormStore.data.endsAt.hour(), - minute: silenceFormStore.data.endsAt.minute(), - second: 0, - }); + const endsAt = new Date(val); + endsAt.setHours(silenceFormStore.data.endsAt.getHours()); + endsAt.setMinutes(silenceFormStore.data.endsAt.getMinutes()); + endsAt.setSeconds(0); silenceFormStore.data.endsAt = endsAt; silenceFormStore.data.verifyStarEnd(); }} selectedDays={{ - from: silenceFormStore.data.startsAt.toDate(), - to: silenceFormStore.data.endsAt.toDate(), + from: silenceFormStore.data.startsAt, + to: silenceFormStore.data.endsAt, }} modifiers={{ - start: silenceFormStore.data.startsAt.toDate(), - end: silenceFormStore.data.endsAt.toDate(), + start: silenceFormStore.data.startsAt, + end: silenceFormStore.data.endsAt, }} />
@@ -199,10 +204,10 @@ TabContentDuration.propTypes = { const DateTimeSelect = ({ silenceFormStore, openTab }) => { const [currentTab, setCurrentTab] = useState(openTab); - const [timeNow, setTimeNow] = useState(moment().seconds(0)); + const [timeNow, setTimeNow] = useState(nowZeroSeconds()); const updateTimeNow = useCallback(() => { - setTimeNow(moment().seconds(0)); + setTimeNow(nowZeroSeconds()); }, []); useEffect(() => { diff --git a/ui/src/Components/SilenceModal/DateTimeSelect/index.test.js b/ui/src/Components/SilenceModal/DateTimeSelect/index.test.js index e1a0e1b71..c3fda33f7 100644 --- a/ui/src/Components/SilenceModal/DateTimeSelect/index.test.js +++ b/ui/src/Components/SilenceModal/DateTimeSelect/index.test.js @@ -7,7 +7,9 @@ import { advanceTo, clear } from "jest-date-mock"; import toDiffableHtml from "diffable-html"; -import moment from "moment"; +import addMinutes from "date-fns/addMinutes"; +import addHours from "date-fns/addHours"; +import differenceInMilliseconds from "date-fns/differenceInMilliseconds"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { @@ -21,8 +23,8 @@ let silenceFormStore; beforeEach(() => { silenceFormStore = new SilenceFormStore(); - silenceFormStore.data.startsAt = moment([2060, 1, 1, 0, 0, 0]); - silenceFormStore.data.endsAt = moment([2061, 1, 1, 0, 0, 0]); + silenceFormStore.data.startsAt = new Date(2060, 1, 1, 0, 0, 0); + silenceFormStore.data.endsAt = new Date(2061, 1, 1, 0, 0, 0); }); afterEach(() => { @@ -121,8 +123,8 @@ describe("", () => { it("'Ends' tab offset badge is updated after 1 minute", () => { jest.useFakeTimers(); advanceTo(new Date(2060, 1, 1, 12, 0, 0)); - silenceFormStore.data.startsAt = moment([2060, 1, 1, 12, 0, 0]); - silenceFormStore.data.endsAt = moment([2060, 1, 1, 13, 0, 0]); + silenceFormStore.data.startsAt = new Date(2060, 1, 1, 12, 0, 0); + silenceFormStore.data.endsAt = new Date(2060, 1, 1, 13, 0, 0); const tree = MountedDateTimeSelect(); expect(tree.find(".nav-link").at(1).text()).toBe("Endsin 1h "); @@ -149,19 +151,22 @@ const ValidateTimeButton = ( const button = tab.find("td > span").at(elemIndex); expect(button.html()).toMatch(iconMatch); - const oldTimeValue = moment(silenceFormStore.data[storeKey]); + const oldTimeValue = new Date(silenceFormStore.data[storeKey]); button.simulate("click"); expect(silenceFormStore.data[storeKey].toISOString()).not.toBe( oldTimeValue.toISOString() ); - const diffMS = silenceFormStore.data[storeKey].diff(oldTimeValue); + const diffMS = differenceInMilliseconds( + silenceFormStore.data[storeKey], + oldTimeValue + ); expect(diffMS).toBe(expectedDiff); }; const ValidateTimeWheel = (tab, storeKey, className, deltaY, expectedDiff) => { const elem = tab.find(className); - const oldTimeValue = moment(silenceFormStore.data[storeKey]); + const oldTimeValue = new Date(silenceFormStore.data[storeKey]); elem.simulate("wheel", { deltaY: deltaY }); // fire real event so cancel listener will trigger @@ -175,7 +180,10 @@ const ValidateTimeWheel = (tab, storeKey, className, deltaY, expectedDiff) => { expect(silenceFormStore.data[storeKey].toISOString()).not.toBe( oldTimeValue.toISOString() ); - const diffMS = silenceFormStore.data[storeKey].diff(oldTimeValue); + const diffMS = differenceInMilliseconds( + silenceFormStore.data[storeKey], + oldTimeValue + ); expect(diffMS).toBe(expectedDiff); }; @@ -187,11 +195,11 @@ describe("", () => { it("selecting date on DayPicker updates startsAt", () => { const tree = MountedTabContentStart(); expect(silenceFormStore.data.startsAt.toISOString()).toBe( - moment([2060, 1, 1, 0, 0, 0]).toISOString() + new Date(2060, 1, 1, 0, 0, 0).toISOString() ); tree.find('div[aria-label="Wed Feb 18 2060"]').simulate("click"); expect(silenceFormStore.data.startsAt.toISOString()).toBe( - moment([2060, 1, 18, 0, 0, 0]).toISOString() + new Date(2060, 1, 18, 0, 0, 0).toISOString() ); }); @@ -300,11 +308,11 @@ describe("", () => { it("Selecting date on DayPicker updates endsAt", () => { const tree = MountedTabContentEnd(); expect(silenceFormStore.data.endsAt.toISOString()).toBe( - moment([2061, 1, 1, 0, 0, 0]).toISOString() + new Date(2061, 1, 1, 0, 0, 0).toISOString() ); tree.find('div[aria-label="Thu Feb 24 2061"]').simulate("click"); expect(silenceFormStore.data.endsAt.toISOString()).toBe( - moment([2061, 1, 24, 0, 0, 0]).toISOString() + new Date(2061, 1, 24, 0, 0, 0).toISOString() ); }); @@ -406,12 +414,15 @@ const ValidateDurationButton = (elemIndex, iconMatch, expectedDiff) => { const button = tree.find("td > span").at(elemIndex); expect(button.html()).toMatch(iconMatch); - const oldEndsAt = moment(silenceFormStore.data.endsAt); + const oldEndsAt = new Date(silenceFormStore.data.endsAt); button.simulate("click"); expect(silenceFormStore.data.endsAt.toISOString()).not.toBe( oldEndsAt.toISOString() ); - const diffMS = silenceFormStore.data.endsAt.diff(oldEndsAt); + const diffMS = differenceInMilliseconds( + silenceFormStore.data.endsAt, + oldEndsAt + ); expect(diffMS).toBe(expectedDiff); }; @@ -421,7 +432,7 @@ const ValidateDurationWheel = (elemIndex, deltaY, expectedDiff) => { ); const elem = tree.find(".components-duration").at(elemIndex); - const oldEndsAt = moment(silenceFormStore.data.endsAt); + const oldEndsAt = new Date(silenceFormStore.data.endsAt); elem.simulate("wheel", { deltaY: deltaY }); // fire real event so cancel listener will trigger @@ -431,7 +442,10 @@ const ValidateDurationWheel = (elemIndex, deltaY, expectedDiff) => { expect(silenceFormStore.data.endsAt.toISOString()).not.toBe( oldEndsAt.toISOString() ); - const diffMS = silenceFormStore.data.endsAt.diff(oldEndsAt); + const diffMS = differenceInMilliseconds( + silenceFormStore.data.endsAt, + oldEndsAt + ); expect(diffMS).toBe(expectedDiff); }; @@ -486,8 +500,8 @@ describe("", () => { }); const SetDurationTo = (hours, minutes) => { - const startsAt = moment([2060, 1, 1, 0, 0, 0]); - const endsAt = moment(startsAt).add(hours, "hours").add(minutes, "minutes"); + const startsAt = new Date(2060, 1, 1, 0, 0, 0); + const endsAt = addMinutes(addHours(startsAt, hours), minutes); silenceFormStore.data.startsAt = startsAt; silenceFormStore.data.endsAt = endsAt; }; diff --git a/ui/src/Components/SilenceModal/PayloadPreview/index.test.js b/ui/src/Components/SilenceModal/PayloadPreview/index.test.js index 4bef2df7d..26113d440 100644 --- a/ui/src/Components/SilenceModal/PayloadPreview/index.test.js +++ b/ui/src/Components/SilenceModal/PayloadPreview/index.test.js @@ -4,16 +4,14 @@ import { render } from "enzyme"; import toDiffableHtml from "diffable-html"; -import moment from "moment"; - import { SilenceFormStore } from "Stores/SilenceFormStore"; import { PayloadPreview } from "."; describe("", () => { it("matches snapshot", () => { const silenceFormStore = new SilenceFormStore(); - silenceFormStore.data.startsAt = moment.utc([2000, 1, 1, 0, 0, 0]); - silenceFormStore.data.endsAt = moment.utc([2000, 1, 1, 1, 0, 0]); + silenceFormStore.data.startsAt = new Date(Date.UTC(2000, 1, 1, 0, 0, 0)); + silenceFormStore.data.endsAt = new Date(Date.UTC(2000, 1, 1, 1, 0, 0)); silenceFormStore.data.createdBy = "me@example.com"; silenceFormStore.data.comment = "PayloadPreview test"; diff --git a/ui/src/Components/SilenceModal/index.stories.js b/ui/src/Components/SilenceModal/index.stories.js index d8486329f..1b590ce2d 100644 --- a/ui/src/Components/SilenceModal/index.stories.js +++ b/ui/src/Components/SilenceModal/index.stories.js @@ -4,7 +4,8 @@ import fetchMock from "fetch-mock"; import { storiesOf } from "@storybook/react"; -import moment from "moment"; +import addHours from "date-fns/addHours"; +import addDays from "date-fns/addDays"; import { MockSilence } from "__mocks__/Alerts"; import { AlertStore } from "Stores/AlertStore"; @@ -105,8 +106,8 @@ storiesOf("SilenceModal", module) silenceFormStore.data.comment = "fake silence"; silenceFormStore.data.silenceID = "1234567890"; - silenceFormStore.data.startsAt = moment(); - silenceFormStore.data.endsAt = moment().add(2, "hour").add(10, "day"); + silenceFormStore.data.startsAt = new Date(); + silenceFormStore.data.endsAt = addDays(addHours(new Date(), 2), 10); silenceFormStore.tab.current = SilenceTabNames.Editor; diff --git a/ui/src/Stores/SilenceFormStore.js b/ui/src/Stores/SilenceFormStore.js index af9f7d9b2..94754ec3f 100644 --- a/ui/src/Stores/SilenceFormStore.js +++ b/ui/src/Stores/SilenceFormStore.js @@ -2,7 +2,13 @@ import { observable, action, computed } from "mobx"; import uniqueId from "lodash.uniqueid"; -import moment from "moment"; +import parseISO from "date-fns/parseISO"; +import addHours from "date-fns/addHours"; +import addMinutes from "date-fns/addMinutes"; +import subMinutes from "date-fns/subMinutes"; +import differenceInDays from "date-fns/differenceInDays"; +import differenceInHours from "date-fns/differenceInHours"; +import differenceInMinutes from "date-fns/differenceInMinutes"; const NewEmptyMatcher = () => { return { @@ -178,8 +184,8 @@ class SilenceFormStore { silenceID: null, alertmanagers: [], matchers: [], - startsAt: moment(), - endsAt: moment().add(1, "hour"), + startsAt: new Date(), + endsAt: addHours(new Date(), 1), comment: "", author: "", @@ -201,8 +207,8 @@ class SilenceFormStore { }, resetStartEnd() { - this.startsAt = moment(); - this.endsAt = moment().add(1, "hour"); + this.startsAt = new Date(); + this.endsAt = addHours(new Date(), 1); }, resetProgress() { @@ -260,44 +266,51 @@ class SilenceFormStore { } this.matchers = matchers; - this.startsAt = moment(silence.startsAt); - this.endsAt = moment(silence.endsAt); + this.startsAt = parseISO(silence.startsAt); + this.endsAt = parseISO(silence.endsAt); this.comment = silence.comment; this.author = silence.createdBy; }, verifyStarEnd() { - const now = moment().second(0); - if (this.startsAt.isBefore(now)) { + const now = new Date(); + now.setSeconds(0); + if (this.startsAt < now) { this.startsAt = now; } - if (this.endsAt.isSameOrBefore(this.startsAt)) { - this.endsAt = moment(this.startsAt).add(1, "minutes"); + if (this.endsAt <= this.startsAt) { + this.endsAt = addMinutes(this.startsAt, 1); } }, incStart(minutes) { - this.startsAt = moment(this.startsAt).add(minutes, "minutes"); + this.startsAt = addMinutes(this.startsAt, minutes); this.verifyStarEnd(); }, decStart(minutes) { - this.startsAt = moment(this.startsAt).subtract(minutes, "minutes"); + this.startsAt = subMinutes(this.startsAt, minutes); this.verifyStarEnd(); }, incEnd(minutes) { - this.endsAt = moment(this.endsAt).add(minutes, "minutes"); + this.endsAt = addMinutes(this.endsAt, minutes); this.verifyStarEnd(); }, decEnd(minutes) { - this.endsAt = moment(this.endsAt).subtract(minutes, "minutes"); + this.endsAt = subMinutes(this.endsAt, minutes); this.verifyStarEnd(); }, get toAlertmanagerPayload() { + const startsAt = new Date(this.startsAt); + startsAt.setSeconds(0); + startsAt.setMilliseconds(0); + const endsAt = new Date(this.endsAt); + endsAt.setSeconds(0); + endsAt.setMilliseconds(0); return GenerateAlertmanagerSilenceData( - this.startsAt.second(0).millisecond(0), - this.endsAt.second(0).millisecond(0), + startsAt, + endsAt, this.matchers, this.author, this.comment, @@ -307,9 +320,9 @@ class SilenceFormStore { get toDuration() { const data = { - days: this.endsAt.diff(this.startsAt, "days"), - hours: this.endsAt.diff(this.startsAt, "hours") % 24, - minutes: this.endsAt.diff(this.startsAt, "minutes") % 60, + days: differenceInDays(this.endsAt, this.startsAt), + hours: differenceInHours(this.endsAt, this.startsAt) % 24, + minutes: differenceInMinutes(this.endsAt, this.startsAt) % 60, }; return data; }, diff --git a/ui/src/Stores/SilenceFormStore.test.js b/ui/src/Stores/SilenceFormStore.test.js index 34555486a..5064b1f45 100644 --- a/ui/src/Stores/SilenceFormStore.test.js +++ b/ui/src/Stores/SilenceFormStore.test.js @@ -1,4 +1,5 @@ -import moment from "moment"; +import differenceInMilliseconds from "date-fns/differenceInMilliseconds"; +import isSameDay from "date-fns/isSameDay"; import { MockAlert, @@ -74,13 +75,13 @@ describe("SilenceFormStore.toggle", () => { describe("SilenceFormStore.data", () => { it("resetStartEnd() sets startsAt and endsAt to defaults", () => { - store.data.startsAt = moment([2000, 1, 1, 0, 1, 0]); - store.data.endsAt = moment([2000, 1, 1, 1, 2, 0]); - expect(store.data.startsAt.isSame([2000, 1, 1], "day")).toBe(true); - expect(store.data.endsAt.isSame([2000, 1, 1], "day")).toBe(true); + store.data.startsAt = new Date(2000, 1, 1, 0, 1, 0); + store.data.endsAt = new Date(2000, 1, 1, 1, 2, 0); + expect(isSameDay(store.data.startsAt, new Date(2000, 1, 1))).toBe(true); + expect(isSameDay(store.data.endsAt, new Date(2000, 1, 1))).toBe(true); store.data.resetStartEnd(); - expect(store.data.startsAt.isSame([2000, 1, 1], "day")).toBe(false); - expect(store.data.endsAt.isSame([2000, 1, 1], "day")).toBe(false); + expect(isSameDay(store.data.startsAt, new Date(2000, 1, 1))).toBe(false); + expect(isSameDay(store.data.endsAt, new Date(2000, 1, 1))).toBe(false); }); it("resetProgress() sets currentStage to UserInput", () => { @@ -303,11 +304,11 @@ describe("SilenceFormStore.data", () => { }) ); - expect(store.data.startsAt.utc().toISOString()).toBe( - moment.utc([2000, 0, 1, 0, 0, 0]).toISOString() + expect(store.data.startsAt.toISOString()).toBe( + new Date(Date.UTC(2000, 0, 1, 0, 0, 0)).toISOString() ); - expect(store.data.endsAt.utc().toISOString()).toBe( - moment.utc([2000, 0, 1, 1, 0, 0]).toISOString() + expect(store.data.endsAt.toISOString()).toBe( + new Date(Date.UTC(2000, 0, 1, 1, 0, 0)).toISOString() ); expect(store.data.author).toBe("me@example.com"); @@ -331,8 +332,8 @@ describe("SilenceFormStore.data", () => { store.data.fillMatchersFromGroup(group, []); // add empty matcher so we test empty string rendering store.data.addEmptyMatcher(); - store.data.startsAt = moment.utc([2000, 1, 1, 0, 0, 0]); - store.data.endsAt = moment.utc([2000, 1, 1, 1, 0, 0]); + store.data.startsAt = new Date(Date.UTC(2000, 1, 1, 0, 0, 0)); + store.data.endsAt = new Date(Date.UTC(2000, 1, 1, 1, 0, 0)); store.data.author = "me@example.com"; store.data.comment = "toAlertmanagerPayload test"; expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); @@ -406,8 +407,8 @@ describe("SilenceFormStore.data.isValid", () => { describe("SilenceFormStore.data startsAt & endsAt validation", () => { it("toDuration returns correct duration for 5d 0h 1m", () => { - store.data.startsAt = moment([2000, 1, 1, 0, 0, 0]); - store.data.endsAt = moment([2000, 1, 6, 0, 1, 15]); + store.data.startsAt = new Date(2000, 1, 1, 0, 0, 0); + store.data.endsAt = new Date(2000, 1, 6, 0, 1, 15); expect(store.data.toDuration).toMatchObject({ days: 5, hours: 0, @@ -416,8 +417,8 @@ describe("SilenceFormStore.data startsAt & endsAt validation", () => { }); it("toDuration returns correct duration for 2h 15m", () => { - store.data.startsAt = moment([2000, 1, 1, 0, 0, 0]); - store.data.endsAt = moment([2000, 1, 1, 2, 15, 0]); + store.data.startsAt = new Date(2000, 1, 1, 0, 0, 0); + store.data.endsAt = new Date(2000, 1, 1, 2, 15, 0); expect(store.data.toDuration).toMatchObject({ days: 0, hours: 2, @@ -426,8 +427,8 @@ describe("SilenceFormStore.data startsAt & endsAt validation", () => { }); it("toDuration returns correct duration for 59m", () => { - store.data.startsAt = moment([2000, 1, 1, 0, 10, 0]); - store.data.endsAt = moment([2000, 1, 1, 1, 9, 0]); + store.data.startsAt = new Date(2000, 1, 1, 0, 10, 0); + store.data.endsAt = new Date(2000, 1, 1, 1, 9, 0); expect(store.data.toDuration).toMatchObject({ days: 0, hours: 0, @@ -436,8 +437,8 @@ describe("SilenceFormStore.data startsAt & endsAt validation", () => { }); it("verifyStarEnd() doesn't do anything if endsAt if after startsAt", () => { - const startsAt = moment([2063, 1, 1, 0, 0, 0]); - const endsAt = moment([2063, 1, 1, 1, 1, 0]); + const startsAt = new Date(2063, 1, 1, 0, 0, 0); + const endsAt = new Date(2063, 1, 1, 1, 1, 0); store.data.startsAt = startsAt; store.data.endsAt = endsAt; store.data.verifyStarEnd(); @@ -446,57 +447,58 @@ describe("SilenceFormStore.data startsAt & endsAt validation", () => { }); it("verifyStarEnd() updates startsAt if it's before now()", () => { - const now = moment().second(0); - const startsAt = moment([2000, 1, 1, 0, 0, 1]); - const endsAt = moment([2063, 1, 1, 0, 0, 0]); + const now = new Date(); + now.setSeconds(0); + const startsAt = new Date(2000, 1, 1, 0, 0, 1); + const endsAt = new Date(2063, 1, 1, 0, 0, 0); store.data.startsAt = startsAt; store.data.endsAt = endsAt; store.data.verifyStarEnd(); - expect(store.data.startsAt.isSameOrAfter(now)).toBeTruthy(); + expect(store.data.startsAt >= now).toBeTruthy(); expect(store.data.endsAt.toISOString()).toBe(endsAt.toISOString()); }); it("verifyStarEnd() updates endsAt if it's before startsAt", () => { - const startsAt = moment([2063, 1, 1, 0, 0, 1]); - const endsAt = moment([2063, 1, 1, 0, 0, 0]); + const startsAt = new Date(2063, 1, 1, 0, 0, 1); + const endsAt = new Date(2063, 1, 1, 0, 0, 0); store.data.startsAt = startsAt; store.data.endsAt = endsAt; store.data.verifyStarEnd(); expect(store.data.startsAt.toISOString()).toBe(startsAt.toISOString()); expect(store.data.endsAt.toISOString()).toBe( - moment([2063, 1, 1, 0, 1, 1]).toISOString() + new Date(2063, 1, 1, 0, 1, 1).toISOString() ); }); it("incStart(7) adds 7 minutes to startsAt", () => { - const startsAt = moment([2063, 1, 1, 0, 0, 1]); + const startsAt = new Date(2063, 1, 1, 0, 0, 1); store.data.startsAt = startsAt; store.data.incStart(7); - const diffMS = store.data.startsAt.diff(startsAt); + const diffMS = differenceInMilliseconds(store.data.startsAt, startsAt); expect(diffMS).toBe(7 * 60 * 1000); }); it("decStart(14) subtracts 14 minutes from startsAt", () => { - const startsAt = moment([2063, 1, 1, 0, 0, 1]); + const startsAt = new Date(2063, 1, 1, 0, 0, 1); store.data.startsAt = startsAt; store.data.decStart(14); - const diffMS = store.data.startsAt.diff(startsAt); + const diffMS = differenceInMilliseconds(store.data.startsAt, startsAt); expect(diffMS).toBe(-14 * 60 * 1000); }); it("incEnd(120) adds 120 minutes to endsAt", () => { - const endsAt = moment([2063, 1, 1, 0, 0, 1]); + const endsAt = new Date(2063, 1, 1, 0, 0, 1); store.data.endsAt = endsAt; store.data.incEnd(120); - const diffMS = store.data.endsAt.diff(endsAt); + const diffMS = differenceInMilliseconds(store.data.endsAt, endsAt); expect(diffMS).toBe(120 * 60 * 1000); }); it("decEnd(1) subtracts 1 minute from endsAt", () => { - const endsAt = moment([2063, 1, 1, 0, 0, 1]); + const endsAt = new Date(2063, 1, 1, 0, 0, 1); store.data.endsAt = endsAt; store.data.decEnd(1); - const diffMS = store.data.endsAt.diff(endsAt); + const diffMS = differenceInMilliseconds(store.data.endsAt, endsAt); expect(diffMS).toBe(-1 * 60 * 1000); }); }); diff --git a/ui/src/__mocks__/Fetch.js b/ui/src/__mocks__/Fetch.js index e6bf93593..c4f7f23a2 100644 --- a/ui/src/__mocks__/Fetch.js +++ b/ui/src/__mocks__/Fetch.js @@ -1,10 +1,8 @@ -import moment from "moment"; - import { MockAlert, MockAlertGroup, MockSilence } from "./Alerts"; const EmptyAPIResponse = () => ({ status: "success", - timestamp: moment().toISOString(), + timestamp: new Date().toISOString(), version: "fakeVersion", upstreams: { counters: { total: 1, healthy: 1, failed: 0 }, diff --git a/ui/src/__mocks__/Stories.js b/ui/src/__mocks__/Stories.js index 7b462fc52..5822aabf3 100644 --- a/ui/src/__mocks__/Stories.js +++ b/ui/src/__mocks__/Stories.js @@ -1,4 +1,4 @@ -import moment from "moment"; +import subMinutes from "date-fns/subMinutes"; import { MockAlert, MockAlertGroup, MockSilence } from "./Alerts"; @@ -49,7 +49,7 @@ const MockGroup = (groupName, alertCount, active, suppressed, unprocessed) => { { instance: `instance${i}` }, state ); - alert.startsAt = moment().subtract(alertCount, "minutes").toISOString(); + alert.startsAt = subMinutes(new Date(), alertCount).toISOString(); alerts.push(alert); } const group = MockAlertGroup(