diff --git a/.changeset/nasty-crews-boil.md b/.changeset/nasty-crews-boil.md new file mode 100644 index 0000000000..5b4108c972 --- /dev/null +++ b/.changeset/nasty-crews-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Replaced moment and dayjs with Luxon diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 9350a73120..dcad0952f1 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -52,6 +52,7 @@ cookiecutter css Datadog dataflow +dayjs deadnaming debounce Debounce diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index d21159a845..97dd33e5ad 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -346,7 +346,7 @@ export type DateAggregation = { }; // @public (undocumented) -export const DEFAULT_DATE_FORMAT = "YYYY-MM-DD"; +export const DEFAULT_DATE_FORMAT = "yyyy-LL-dd"; // @public export enum Duration { diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 8f371a2390..1edd1377dc 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -41,9 +41,8 @@ "@types/react": "^16.9", "@types/recharts": "^1.8.14", "classnames": "^2.2.6", - "dayjs": "^1.9.4", "history": "^5.0.0", - "moment": "^2.27.0", + "luxon": "^1.26.0", "pluralize": "^8.0.0", "qs": "^6.9.4", "react": "^16.13.1", diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx index 7202c069f5..d0731326f2 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React, { useState } from 'react'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; +import { DateTime } from 'luxon'; import { useTheme, Box, @@ -54,8 +53,6 @@ import { formatPeriod } from '../../utils/formatters'; import { aggregationSum } from '../../utils/sum'; import { BarChartLegendOptions } from '../BarChart/BarChartLegend'; -dayjs.extend(utc); - export type CostOverviewBreakdownChartProps = { costBreakdown: Cost[]; }; @@ -184,7 +181,11 @@ export const CostOverviewBreakdownChart = ({ }) => { if (isInvalid({ label, payload })) return null; - const dateTitle = dayjs(label).utc().format(DEFAULT_DATE_FORMAT); + const date = + typeof label === 'number' + ? DateTime.fromMillis(label) + : DateTime.fromISO(label!); + const dateTitle = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); const items = payload.map(p => ({ label: p.dataKey as string, value: formatGraphValue(p.value as number), diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 4f806658be..e7b478ba07 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; +import { DateTime } from 'luxon'; import { useTheme, Box } from '@material-ui/core'; import { ComposedChart, @@ -52,8 +51,6 @@ import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; import { aggregationSort } from '../../utils/sort'; import { CostOverviewLegend } from './CostOverviewLegend'; -dayjs.extend(utc); - type CostOverviewChartProps = { metric: Maybe; metricData: Maybe; @@ -108,7 +105,11 @@ export const CostOverviewChart = ({ if (isInvalid({ label, payload })) return null; const dataKeys = [data.dailyCost.dataKey, data.metric.dataKey]; - const title = dayjs(label).utc().format(DEFAULT_DATE_FORMAT); + const date = + typeof label === 'number' + ? DateTime.fromMillis(label) + : DateTime.fromISO(label!); + const title = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); const items = payload .filter(p => dataKeys.includes(p.dataKey as string)) .map(p => ({ diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertChart.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertChart.tsx index bdfb27b8e8..74c0cea34b 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertChart.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertChart.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import moment from 'moment'; +import { DateTime } from 'luxon'; import { Box } from '@material-ui/core'; import { BarChart, BarChartLegend, BarChartLegendOptions } from '../BarChart'; import { LegendItem } from '../LegendItem'; @@ -38,8 +38,12 @@ export const ProjectGrowthAlertChart = ({ const resourceData = alert.products.map(resourceOf); const options: Partial = { - previousName: moment(alert.periodStart, 'YYYY-[Q]Q').format('[Q]Q YYYY'), - currentName: moment(alert.periodEnd, 'YYYY-[Q]Q').format('[Q]Q YYYY'), + previousName: DateTime.fromFormat(alert.periodStart, "yyyy-'Q'q").toFormat( + "'Q'q yyyy", + ), + currentName: DateTime.fromFormat(alert.periodEnd, "yyyy-'Q'q").toFormat( + "'Q'q yyyy", + ), }; return ( diff --git a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx index 89d2f28a45..35b00ce6f6 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import moment from 'moment'; +import { DateTime } from 'luxon'; import { Box, Typography } from '@material-ui/core'; import { AlertInstructionsLayout } from '../AlertInstructionsLayout'; import { ProductInsightsChart } from '../ProductInsightsCard'; @@ -30,7 +30,7 @@ import { import { ProjectGrowthAlert } from '../../alerts'; import { InfoCard } from '@backstage/core-components'; -const today = moment().format(DEFAULT_DATE_FORMAT); +const today = DateTime.now().toFormat(DEFAULT_DATE_FORMAT); export const ProjectGrowthInstructionsPage = () => { const alertData: ProjectGrowthData = { diff --git a/plugins/cost-insights/src/example/client.ts b/plugins/cost-insights/src/example/client.ts index dec3864ce6..a7a2a0e22f 100644 --- a/plugins/cost-insights/src/example/client.ts +++ b/plugins/cost-insights/src/example/client.ts @@ -15,7 +15,7 @@ */ /* eslint-disable no-restricted-imports */ -import dayjs from 'dayjs'; +import { DateTime } from 'luxon'; import { CostInsightsApi, ProductInsightsOptions } from '../api'; import { Alert, @@ -46,7 +46,7 @@ export class ExampleCostInsightsClient implements CostInsightsApi { getLastCompleteBillingDate(): Promise { return Promise.resolve( - dayjs().subtract(1, 'day').format(DEFAULT_DATE_FORMAT), + DateTime.now().minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT), ); } @@ -175,13 +175,13 @@ export class ExampleCostInsightsClient implements CostInsightsApi { ], }; - const today = dayjs(); + const today = DateTime.now(); const alerts: Alert[] = await this.request({ group }, [ new ProjectGrowthAlert(projectGrowthData), new UnlabeledDataflowAlert(unlabeledDataflowData), new KubernetesMigrationAlert(this, { - startDate: today.subtract(30, 'day').format(DEFAULT_DATE_FORMAT), - endDate: today.format(DEFAULT_DATE_FORMAT), + startDate: today.minus({ days: 30 }).toFormat(DEFAULT_DATE_FORMAT), + endDate: today.toFormat(DEFAULT_DATE_FORMAT), change: { ratio: 0, amount: 0, diff --git a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx index eb78327dc9..0dcc0a949e 100644 --- a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx +++ b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx @@ -21,7 +21,7 @@ import React, { forwardRef, FormEventHandler, } from 'react'; -import dayjs from 'dayjs'; +import { DateTime } from 'luxon'; import { Box, FormControl, @@ -57,7 +57,7 @@ export const AlertSnoozeForm = forwardRef< e.preventDefault(); if (duration) { const repeatInterval = 1; - const today = dayjs().format(DEFAULT_DATE_FORMAT); + const today = DateTime.now().toFormat(DEFAULT_DATE_FORMAT); onSubmit({ intervals: intervalsOf(duration, today, repeatInterval), }); diff --git a/plugins/cost-insights/src/testUtils/testUtils.ts b/plugins/cost-insights/src/testUtils/testUtils.ts index a2210b67cf..2c6f6b5ef2 100644 --- a/plugins/cost-insights/src/testUtils/testUtils.ts +++ b/plugins/cost-insights/src/testUtils/testUtils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import dayjs from 'dayjs'; +import { DateTime } from 'luxon'; import regression, { DataPoint } from 'regression'; import { ChangeStatistic, @@ -58,9 +58,9 @@ export function aggregationFor( ): DateAggregation[] { const { duration, endDate } = parseIntervals(intervals); const inclusiveEndDate = inclusiveEndDateOf(duration, endDate); - const days = dayjs(endDate).diff( - inclusiveStartDateOf(duration, inclusiveEndDate), - 'day', + const days = DateTime.fromISO(endDate).diff( + DateTime.fromISO(inclusiveStartDateOf(duration, inclusiveEndDate)), + 'days', ); function nextDelta(): number { @@ -74,9 +74,11 @@ export function aggregationFor( return [...Array(days).keys()].reduce( (values: DateAggregation[], i: number): DateAggregation[] => { const last = values.length ? values[values.length - 1].amount : baseline; - const date = dayjs(inclusiveStartDateOf(duration, inclusiveEndDate)) - .add(i, 'day') - .format(DEFAULT_DATE_FORMAT); + const date = DateTime.fromISO( + inclusiveStartDateOf(duration, inclusiveEndDate), + ) + .plus({ days: i }) + .toFormat(DEFAULT_DATE_FORMAT); const amount = Math.max(0, last + nextDelta()); values.push({ date: date, diff --git a/plugins/cost-insights/src/types/Duration.ts b/plugins/cost-insights/src/types/Duration.ts index dcc0d9390c..94297ac00a 100644 --- a/plugins/cost-insights/src/types/Duration.ts +++ b/plugins/cost-insights/src/types/Duration.ts @@ -27,4 +27,4 @@ export enum Duration { P3M = 'P3M', } -export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; +export const DEFAULT_DATE_FORMAT = 'yyyy-LL-dd'; diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index 8ab927ecf6..3a0d19b123 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -24,13 +24,10 @@ import { Duration, DateAggregation, } from '../types'; -import dayjs, { OpUnitType } from 'dayjs'; -import durationPlugin from 'dayjs/plugin/duration'; +import { DateTime, Duration as LuxonDuration } from 'luxon'; import { inclusiveStartDateOf } from './duration'; import { notEmpty } from './assert'; -dayjs.extend(durationPlugin); - // Used for displaying status colors export function growthOf(change: ChangeStatistic): GrowthType { const exceedsEngineerThreshold = Math.abs(change.amount) >= EngineerThreshold; @@ -85,16 +82,12 @@ export function getPreviousPeriodTotalCost( duration: Duration, inclusiveEndDate: string, ): number { - const dayjsDuration = dayjs.duration(duration); + const luxonDuration = LuxonDuration.fromISO(duration); const startDate = inclusiveStartDateOf(duration, inclusiveEndDate); - // dayjs doesn't allow adding an ISO 8601 period to dates. - const [amount, type]: [number, OpUnitType] = dayjsDuration.days() - ? [dayjsDuration.days(), 'day'] - : [dayjsDuration.months(), 'month']; - const nextPeriodStart = dayjs(startDate).add(amount, type); + const nextPeriodStart = DateTime.fromISO(startDate).plus(luxonDuration); // Add up costs that incurred before the start of the next period. return aggregation.reduce((acc, costByDate) => { - return dayjs(costByDate.date).isBefore(nextPeriodStart) + return DateTime.fromISO(costByDate.date) < nextPeriodStart ? acc + costByDate.amount : acc; }, 0); diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 17d0661c2b..6d7c8d4765 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import moment from 'moment'; +import { DateTime, Duration as LuxonDuration } from 'luxon'; import { Duration, DEFAULT_DATE_FORMAT } from '../types'; import { assertNever } from './assert'; @@ -34,14 +34,18 @@ export function inclusiveStartDateOf( case Duration.P7D: case Duration.P30D: case Duration.P90D: - return moment(inclusiveEndDate) - .subtract(moment.duration(duration).add(moment.duration(duration))) - .format(DEFAULT_DATE_FORMAT); + return DateTime.fromISO(inclusiveEndDate) + .minus( + LuxonDuration.fromISO(duration).plus(LuxonDuration.fromISO(duration)), + ) + .toFormat(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(inclusiveEndDate) + return DateTime.fromISO(inclusiveEndDate) .startOf('quarter') - .subtract(moment.duration(duration).add(moment.duration(duration))) - .format(DEFAULT_DATE_FORMAT); + .minus( + LuxonDuration.fromISO(duration).plus(LuxonDuration.fromISO(duration)), + ) + .toFormat(DEFAULT_DATE_FORMAT); default: return assertNever(duration); } @@ -55,11 +59,13 @@ export function exclusiveEndDateOf( case Duration.P7D: case Duration.P30D: case Duration.P90D: - return moment(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT); + return DateTime.fromISO(inclusiveEndDate) + .plus({ days: 1 }) + .toFormat(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(quarterEndDate(inclusiveEndDate)) - .add(1, 'day') - .format(DEFAULT_DATE_FORMAT); + return DateTime.fromISO(quarterEndDate(inclusiveEndDate)) + .plus({ days: 1 }) + .toFormat(DEFAULT_DATE_FORMAT); default: return assertNever(duration); } @@ -69,9 +75,9 @@ export function inclusiveEndDateOf( duration: Duration, inclusiveEndDate: string, ): string { - return moment(exclusiveEndDateOf(duration, inclusiveEndDate)) - .subtract(1, 'day') - .format(DEFAULT_DATE_FORMAT); + return DateTime.fromISO(exclusiveEndDateOf(duration, inclusiveEndDate)) + .minus({ days: 1 }) + .toFormat(DEFAULT_DATE_FORMAT); } // https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals @@ -87,13 +93,13 @@ export function intervalsOf( } export function quarterEndDate(inclusiveEndDate: string): string { - const endDate = moment(inclusiveEndDate); - const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT); + const endDate = DateTime.fromISO(inclusiveEndDate); + const endOfQuarter = endDate.endOf('quarter').toFormat(DEFAULT_DATE_FORMAT); if (endOfQuarter === inclusiveEndDate) { - return endDate.format(DEFAULT_DATE_FORMAT); + return endDate.toFormat(DEFAULT_DATE_FORMAT); } return endDate .startOf('quarter') - .subtract(1, 'day') - .format(DEFAULT_DATE_FORMAT); + .minus({ days: 1 }) + .toFormat(DEFAULT_DATE_FORMAT); } diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 7733046165..70782123cd 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import moment from 'moment'; +import { DateTime, Duration as LuxonDuration } from 'luxon'; import pluralize from 'pluralize'; import { ChangeStatistic, Duration } from '../types'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; @@ -67,9 +67,11 @@ export const monthOf = (date: string): string => { }; export const quarterOf = (date: string): string => { - // Supports formatting YYYY-MM-DD and YYYY-[Q]Q returned in alerts - const d = moment(date).isValid() ? moment(date) : moment(date, 'YYYY-[Q]Q'); - return d.format('[Q]Q YYYY'); + // Supports formatting yyyy-LL-dd and yyyy-'Q'q returned in alerts + const d = DateTime.fromISO(date).isValid + ? DateTime.fromISO(date) + : DateTime.fromFormat(date, "yyyy-'Q'q"); + return d.toFormat("'Q'q yyyy"); }; export function formatCurrency(amount: number, currency?: string): string { @@ -100,12 +102,12 @@ export function formatPercent(n: number): string { } export function formatLastTwoLookaheadQuarters(inclusiveEndDate: string) { - const start = moment( + const start = DateTime.fromISO( inclusiveStartDateOf(Duration.P3M, inclusiveEndDate), - ).format('[Q]Q YYYY'); - const end = moment(inclusiveEndDateOf(Duration.P3M, inclusiveEndDate)).format( - '[Q]Q YYYY', - ); + ).toFormat("'Q'q yyyy"); + const end = DateTime.fromISO( + inclusiveEndDateOf(Duration.P3M, inclusiveEndDate), + ).toFormat("'Q'q yyyy"); return `${start} vs ${end}`; } @@ -116,7 +118,7 @@ const formatRelativePeriod = ( ): string => { const periodStart = isEndDate ? inclusiveStartDateOf(duration, date) : date; const periodEnd = isEndDate ? date : inclusiveEndDateOf(duration, date); - const days = moment.duration(duration).asDays(); + const days = LuxonDuration.fromISO(duration).days; if (![periodStart, periodEnd].includes(date)) { throw new Error(`Invalid relative date ${date} for duration ${duration}`); }