diff --git a/frontend/app/mstore/types/search.ts b/frontend/app/mstore/types/search.ts index 287b330e8..ed24b1bfb 100644 --- a/frontend/app/mstore/types/search.ts +++ b/frontend/app/mstore/types/search.ts @@ -143,7 +143,7 @@ export default class Search { return new FilterItem(filter).toJson(); }); - const { startDate, endDate } = this.getDateRange(js.rangeValue, js.startDate, js.endDate); + const { startDate, endDate } = this.getDateRange(js.rangeValue, js.startDate, js.endDate, 15); js.startDate = startDate; js.endDate = endDate; @@ -152,7 +152,38 @@ export default class Search { return js; } - private getDateRange(rangeName: string, customStartDate: number, customEndDate: number): { + private roundToNextInterval(timestamp: number, intervalMinutes: number): number { + if (intervalMinutes <= 0) { + return timestamp; // No rounding if interval is invalid + } + + const date = new Date(timestamp); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + const milliseconds = date.getMilliseconds(); + + // Calculate minutes to add to reach next interval slot + const minutesToAdd = (intervalMinutes - (minutes % intervalMinutes)) % intervalMinutes; + + // If exactly at interval mark but has seconds/milliseconds, round up to next slot + const shouldAddExtra = (minutesToAdd === 0 && (seconds > 0 || milliseconds > 0)); + const adjustedMinutesToAdd = shouldAddExtra ? intervalMinutes : minutesToAdd; + + // Create a new date with added minutes and zeroed seconds/milliseconds + const roundedDate = new Date(timestamp); + roundedDate.setMinutes(minutes + adjustedMinutesToAdd); + roundedDate.setSeconds(0); + roundedDate.setMilliseconds(0); + + return roundedDate.getTime(); + } + + private getDateRange( + rangeName: string, + customStartDate: number, + customEndDate: number, + roundingOption: number | 'none' = 'none' + ): { startDate: number; endDate: number } { @@ -178,6 +209,29 @@ export default class Search { startDate = endDate - 24 * 60 * 60 * 1000; } + // Apply rounding if specified and it's a number + if (roundingOption !== 'none' && typeof roundingOption === 'number') { + const intervalMinutes = roundingOption; + + // For CUSTOM_RANGE, do not apply rounding + if (rangeName !== CUSTOM_RANGE) { + endDate = this.roundToNextInterval(endDate, intervalMinutes); + + // Recalculate the start date based on the rounded end date to maintain the correct time span + switch (rangeName) { + case LAST_7_DAYS: + startDate = endDate - 7 * 24 * 60 * 60 * 1000; + break; + case LAST_30_DAYS: + startDate = endDate - 30 * 24 * 60 * 60 * 1000; + break; + case LAST_24_HOURS: + default: + startDate = endDate - 24 * 60 * 60 * 1000; + } + } + } + return { startDate, endDate diff --git a/frontend/app/types/app/period.js b/frontend/app/types/app/period.js index 01c2e9d01..869e0720d 100644 --- a/frontend/app/types/app/period.js +++ b/frontend/app/types/app/period.js @@ -1,109 +1,146 @@ -import { DateTime, Interval, Settings } from "luxon"; -import Record from "Types/Record"; +import { DateTime, Interval, Settings } from 'luxon'; +import Record from 'Types/Record'; -export const LAST_30_MINUTES = "LAST_30_MINUTES"; -export const TODAY = "TODAY"; -export const LAST_24_HOURS = "LAST_24_HOURS"; -export const YESTERDAY = "YESTERDAY"; -export const LAST_7_DAYS = "LAST_7_DAYS"; -export const LAST_30_DAYS = "LAST_30_DAYS"; -export const THIS_MONTH = "THIS_MONTH"; -export const LAST_MONTH = "LAST_MONTH"; -export const THIS_YEAR = "THIS_YEAR"; -export const CUSTOM_RANGE = "CUSTOM_RANGE"; +export const LAST_30_MINUTES = 'LAST_30_MINUTES'; +export const TODAY = 'TODAY'; +export const LAST_24_HOURS = 'LAST_24_HOURS'; +export const YESTERDAY = 'YESTERDAY'; +export const LAST_7_DAYS = 'LAST_7_DAYS'; +export const LAST_30_DAYS = 'LAST_30_DAYS'; +export const THIS_MONTH = 'THIS_MONTH'; +export const LAST_MONTH = 'LAST_MONTH'; +export const THIS_YEAR = 'THIS_YEAR'; +export const CUSTOM_RANGE = 'CUSTOM_RANGE'; + +function roundToNext15Minutes(dateTime) { + const minutes = dateTime.minute; + const secondsAndMillis = dateTime.second + (dateTime.millisecond / 1000); + + // Calculate minutes to add to reach next 15-minute slot + const minutesToAdd = (15 - (minutes % 15)) % 15; + + // If exactly at 15-minute mark but has seconds/milliseconds, round up to next slot + const adjustedMinutesToAdd = (minutesToAdd === 0 && secondsAndMillis > 0) ? 15 : minutesToAdd; + + return dateTime + .plus({ minutes: adjustedMinutesToAdd }) + .set({ second: 0, millisecond: 0 }); +} + +// Helper function to get rounded now +function getRoundedNow(offset) { + const now = DateTime.now().setZone(offset); + return roundToNext15Minutes(now); +} function getRange(rangeName, offset) { - const now = DateTime.now().setZone(offset); - switch (rangeName) { - case TODAY: - return Interval.fromDateTimes(now.startOf("day"), now.endOf("day")); - case YESTERDAY: - const yesterday = now.minus({ days: 1 }); - return Interval.fromDateTimes( - yesterday.startOf("day"), - yesterday.endOf("day") - ); - case LAST_24_HOURS: - return Interval.fromDateTimes(now.minus({ hours: 24 }), now); - case LAST_30_MINUTES: - return Interval.fromDateTimes( - now.minus({ minutes: 30 }).startOf("minute"), - now.startOf("minute") - ); - case LAST_7_DAYS: - return Interval.fromDateTimes( - now.minus({ days: 7 }).startOf("day"), - now.endOf("day") - ); - case LAST_30_DAYS: - return Interval.fromDateTimes( - now.minus({ days: 30 }).startOf("day"), - now.endOf("day") - ); - case THIS_MONTH: - return Interval.fromDateTimes(now.startOf("month"), now.endOf("month")); - case LAST_MONTH: - const lastMonth = now.minus({ months: 1 }); - return Interval.fromDateTimes(lastMonth.startOf("month"), lastMonth.endOf("month")); - case THIS_YEAR: - return Interval.fromDateTimes(now.startOf("year"), now.endOf("year")); - default: - return Interval.fromDateTimes(now, now); - } + const now = getRoundedNow(offset); + + switch (rangeName) { + case TODAY: + return Interval.fromDateTimes(now.startOf('day'), now.endOf('day')); + case YESTERDAY: + const yesterday = now.minus({ days: 1 }); + return Interval.fromDateTimes( + yesterday.startOf('day'), + yesterday.endOf('day') + ); + case LAST_24_HOURS: + return Interval.fromDateTimes(now.minus({ hours: 24 }), now); + case LAST_30_MINUTES: + return Interval.fromDateTimes( + now.minus({ minutes: 30 }), + now + ); + case LAST_7_DAYS: + return Interval.fromDateTimes( + now.minus({ days: 7 }).startOf('day'), + now.endOf('day') + ); + case LAST_30_DAYS: + return Interval.fromDateTimes( + now.minus({ days: 30 }).startOf('day'), + now.endOf('day') + ); + case THIS_MONTH: + return Interval.fromDateTimes(now.startOf('month'), now.endOf('month')); + case LAST_MONTH: + const lastMonth = now.minus({ months: 1 }); + return Interval.fromDateTimes(lastMonth.startOf('month'), lastMonth.endOf('month')); + case THIS_YEAR: + return Interval.fromDateTimes(now.startOf('year'), now.endOf('year')); + default: + return Interval.fromDateTimes(now, now); + } } +// Get the current rounded time for default Record value +const defaultNow = getRoundedNow(); + export default Record( { - start: 0, - end: 0, - rangeName: CUSTOM_RANGE, - range: Interval.fromDateTimes(DateTime.now(), DateTime.now()), + start: 0, + end: 0, + rangeName: CUSTOM_RANGE, + range: Interval.fromDateTimes(defaultNow, defaultNow) }, { - fromJS: (period) => { - const offset = period.timezoneOffset || DateTime.now().offset; - if (!period.rangeName || period.rangeName === CUSTOM_RANGE) { - const isLuxon = DateTime.isDateTime(period.start); - const start = isLuxon - ? period.start : DateTime.fromMillis(period.start || 0, { zone: Settings.defaultZone }); - const end = isLuxon - ? period.end : DateTime.fromMillis(period.end || 0, { zone: Settings.defaultZone }); - const range = Interval.fromDateTimes(start, end); - return { - ...period, - range, - start: range.start.toMillis(), - end: range.end.toMillis(), - }; - } - const range = getRange(period.rangeName, offset); - return { - ...period, - range, - start: range.start.toMillis(), - end: range.end.toMillis(), - }; + fromJS: (period) => { + const offset = period.timezoneOffset || DateTime.now().offset; + + if (!period.rangeName || period.rangeName === CUSTOM_RANGE) { + const isLuxon = DateTime.isDateTime(period.start); + let start, end; + + if (isLuxon) { + start = roundToNext15Minutes(period.start); + end = roundToNext15Minutes(period.end); + } else { + start = roundToNext15Minutes( + DateTime.fromMillis(period.start || 0, { zone: Settings.defaultZone }) + ); + end = roundToNext15Minutes( + DateTime.fromMillis(period.end || 0, { zone: Settings.defaultZone }) + ); + } + + const range = Interval.fromDateTimes(start, end); + return { + ...period, + range, + start: range.start.toMillis(), + end: range.end.toMillis() + }; + } + + const range = getRange(period.rangeName, offset); + return { + ...period, + range, + start: range.start.toMillis(), + end: range.end.toMillis() + }; + }, + methods: { + toJSON() { + return { + startDate: this.start, + endDate: this.end, + rangeName: this.rangeName, + rangeValue: this.rangeName + }; }, - methods: { - toJSON() { - return { - startDate: this.start, - endDate: this.end, - rangeName: this.rangeName, - rangeValue: this.rangeName, - }; - }, - toTimestamps() { - return { - startTimestamp: this.start, - endTimestamp: this.end, - }; - }, - rangeFormatted(format = "MMM dd yyyy, HH:mm", tz) { - const start = this.range.start.setZone(tz); - const end = this.range.end.setZone(tz); - return `${start.toFormat(format)} - ${end.toFormat(format)}`; - }, + toTimestamps() { + return { + startTimestamp: this.start, + endTimestamp: this.end + }; }, + rangeFormatted(format = 'MMM dd yyyy, HH:mm', tz) { + const start = this.range.start.setZone(tz); + const end = this.range.end.setZone(tz); + return `${start.toFormat(format)} - ${end.toFormat(format)}`; + } + } } ); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f1b91f871..140989c55 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2952,67 +2952,61 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:^5.21.1": - version: 5.30.0 - resolution: "@sentry/browser@npm:5.30.0" +"@sentry-internal/browser-utils@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/browser-utils@npm:8.55.0" dependencies: - "@sentry/core": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/4787cc3ea90600b36b548a8403afb30f13e1e562dd426871879d824536c16005d0734b7406498f1a6dd4fa7e0a49808e17a1c2c24750430ba7f86f909a9eb95a + "@sentry/core": "npm:8.55.0" + checksum: 10c1/67fdc5ec9c8bc6c8eeda4598332a7937a8c7d6cc1cadb05a886323f3d13c25def7b9258ad4b834919dea5d612010de8900f5cf738e9a577a711c839f285557d7 languageName: node linkType: hard -"@sentry/core@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/core@npm:5.30.0" +"@sentry-internal/feedback@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/feedback@npm:8.55.0" dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/minimal": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/5c6dcdccc48a9d6957af7745226eacd3d4926574593e852ccbad0fbaa71355879b9c4707c194e3d9b1ef389d98171a3d85d2c75636a5c6d1cc3c9950cd06334a + "@sentry/core": "npm:8.55.0" + checksum: 10c1/3f6fd3b8c2305b457a5c729c92b2a2335200e5ee0d5a210b513246e00ecda6d2a28940871ed88eee7f7bd8465571388698a7b789c8e0f3d5832ff3a0b040b514 languageName: node linkType: hard -"@sentry/hub@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/hub@npm:5.30.0" +"@sentry-internal/replay-canvas@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/replay-canvas@npm:8.55.0" dependencies: - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/28b86742c72427b5831ee3077c377d1f305d2eb080f7dc977e81b8f29e8eb0dfa07f129c1f5cda29bda9238fe50e292ab719119c4c5a5b7ef580a24bcb6356a3 + "@sentry-internal/replay": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/48511881330193d754e01b842e3b2b931641d0954bac8a8f01503ff3d2aedc9f1779049be0a7a56ba35583769f0566381853c7656888c42f9f59224c6520e593 languageName: node linkType: hard -"@sentry/minimal@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/minimal@npm:5.30.0" +"@sentry-internal/replay@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/replay@npm:8.55.0" dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/d28ad14e43d3c5c06783288ace1fcf1474437070f04d1476b04d0288656351d9a6285cc66d346e8d84a3e73cf895944c06fa7c82bad93415831e4449e11f2d89 + "@sentry-internal/browser-utils": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/d60b4261df037b4c82dafc6b25695b2be32f95a45cd25fc43c659d65644325238f7152f6222cd5d4f3f52407c3f5ad67ea30b38fea27c9422536f8aaba6b0048 languageName: node linkType: hard -"@sentry/types@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/types@npm:5.30.0" - checksum: 10c1/07fe7f04f6aae13f037761fe56a20e06fa4a768bf024fb81970d3087ab9ab5b45bd85b9081945ef5019d93b7de742918374a0e7b70a992dbb29a5078982ddfd9 +"@sentry/browser@npm:^8.34.0": + version: 8.55.0 + resolution: "@sentry/browser@npm:8.55.0" + dependencies: + "@sentry-internal/browser-utils": "npm:8.55.0" + "@sentry-internal/feedback": "npm:8.55.0" + "@sentry-internal/replay": "npm:8.55.0" + "@sentry-internal/replay-canvas": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/3baf51a0b401bb63b345df480773d49b713dd557e15baf2c98f089612c9497aca6f2c7849b1c4d6ded6229d1de495e3305a0438145333de26c6ba190d261c039 languageName: node linkType: hard -"@sentry/utils@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/utils@npm:5.30.0" - dependencies: - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/311ad0be0e40af9f4ab7be2dfb8a782a779fa56700a0662f49ebcbff0dbbe4ea5dff690ad2c0ed4ecb6a6721a3066186b3c8f677fa302c5b606f86dfaa654de3 +"@sentry/core@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry/core@npm:8.55.0" + checksum: 10c1/fbb71058626214674c4b103160fea859ce1fcc83b26533920b2c4fc7d5169bde178b08cd46dad29fabaf616fa465db4274356c500a37f33888bdb8d10fda3d55 languageName: node linkType: hard @@ -11574,7 +11568,7 @@ __metadata: "@jest/globals": "npm:^29.7.0" "@medv/finder": "npm:^3.1.0" "@openreplay/sourcemap-uploader": "npm:^3.0.10" - "@sentry/browser": "npm:^5.21.1" + "@sentry/browser": "npm:^8.34.0" "@svg-maps/world": "npm:^1.0.1" "@tanstack/react-query": "npm:^5.56.2" "@trivago/prettier-plugin-sort-imports": "npm:^4.3.0" @@ -15762,7 +15756,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.3": +"tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10c1/24ee51ea8fb127ca8ad30a25fdac22c5bb11a2b043781757ddde0daf2e03126e1e13e88ab1748d9c50f786a648b5b038e70782063fd15c3ad07ebec039df8f6f