@kelvininc/ui-components
Version:
Kelvin UI Components
349 lines (348 loc) • 15.5 kB
JavaScript
import { ERelativeTimeComparisonConfig } from "../../types";
import { calculateOffsetDate, isDateTimeBefore, newDate, newTimezoneDate, newTimezoneDateFromFormat } from "../date/date.helper";
import { RELATIVE_TIME_OFFSETS } from "./relative-time.config";
import { ERelativeTimeRangeKey, EUnitReference } from "./relative-time.types";
/** Date range builders */
export const buildOptionRange = (option, timeZone) => {
switch (option.comparisonConfig) {
case ERelativeTimeComparisonConfig.StartDateEndDate:
return buildStartDateEndDateConfigRange(option, timeZone);
case ERelativeTimeComparisonConfig.StartDate || ERelativeTimeComparisonConfig.EndDate:
return buildSingleDateConfigRange(option, timeZone);
case ERelativeTimeComparisonConfig.AbsoluteAmountOfUnits:
return buildAbsoluteAmountOfUnitsConfigRange(option, timeZone);
default:
return buildRelativeAmountOfUnitsConfigRange(option, timeZone);
}
};
// Build date from config with start and end date
export const buildStartDateEndDateConfigRange = (option, timeZone) => {
const startDateTime = newTimezoneDateFromFormat(timeZone, option.startDate.dateFormat, option.startDate.date);
const endDateTime = newTimezoneDateFromFormat(timeZone, option.endDate.dateFormat, option.endDate.date);
return buildDayjsRange(startDateTime, endDateTime);
};
// Build date from config with start or end date
export const buildSingleDateConfigRange = (option, timeZone) => {
const nowDateTime = newTimezoneDate(timeZone);
const calculatedDate = newTimezoneDateFromFormat(timeZone, option.startDate.dateFormat, option.startDate.date);
return buildDayjsRange(nowDateTime, calculatedDate);
};
// Build date with both start and end date relative to now timestamp
export const buildAbsoluteAmountOfUnitsConfigRange = (option, timeZone) => {
const nowDateTime = newTimezoneDate(timeZone);
const { unit: startDateUnit, amount: startDateAmount, unitReference: startUnitReference } = option.startDate;
const { unit: endDateUnit, amount: endDateAmount, unitReference: endUnitReference } = option.endDate;
const startDateTime = calculateOffsetDateWithUnitReference(calculateOffsetDate(nowDateTime, startDateAmount, startDateUnit), startDateUnit, startUnitReference);
const endDateTime = calculateOffsetDateWithUnitReference(calculateOffsetDate(nowDateTime, endDateAmount, endDateUnit), endDateUnit, endUnitReference);
return buildDayjsRange(startDateTime, endDateTime);
};
// Build date with start date relative to now timestamp
export const buildRelativeAmountOfUnitsConfigRange = (option, timeZone) => {
const nowDateTime = newTimezoneDate(timeZone);
const { amount, unit, unitReference } = option.startDate;
const calculatedDate = calculateOffsetDateWithUnitReference(calculateOffsetDate(nowDateTime, amount, unit), unit, unitReference);
return buildDayjsRange(nowDateTime, calculatedDate);
};
const calculateOffsetDateWithUnitReference = (date, unit, unitReference) => {
if (!unitReference) {
return date;
}
return unitReference === EUnitReference.StartOfUnit ? date.startOf(unit) : date.endOf(unit);
};
/**
* Builds the range of the start and end date and returns the range array, ordering the dates if needed
* @param dateA date (start or end)
* @param dateB date (start or end)
* @returns range containing start date and end date ordered
*/
export const buildDayjsRange = (dateA, dateB) => {
return isDateTimeBefore(dateA, dateB) ? [dateA, dateB] : [dateB, dateA];
};
export const buildTimestampRange = ([startDate, endDate]) => [startDate.valueOf(), endDate.valueOf()];
/**
* Finds a relative time option by its key from the provided options groups.
*
* @param key - The option key to search for
* @param options - Optional: The option groups to search in (defaults to DEFAULT_RELATIVE_TIME_OPTIONS_GROUPS)
* @returns The matching option or undefined
*/
export const getRelativeTimeOption = (key, options) => (options !== null && options !== void 0 ? options : DEFAULT_RELATIVE_TIME_OPTIONS_GROUPS).flat().find(option => option.value === key);
/**
* Derives DateTimeCalculation configuration from a relative time range key.
* This eliminates manual duplication of offset data.
*
* @param key - The relative time range key
* @returns DateTimeCalculation object ready for use in IRelativeTimePickerOption
*
* @example
* ```typescript
* // Simple offset
* getRelativeTimeCalculation(ERelativeTimeRangeKey.Last_5_m)
* // Returns: { amount: -5, unit: 'minutes' }
* ```
*
* @example
* ```typescript
* // Unit reference offset
* getRelativeTimeCalculation(ERelativeTimeRangeKey.Today)
* // Returns: { amount: 0, unit: 'day', unitReference: EUnitReference.StartOfUnit }
* ```
*
* @throws {Error} If no configuration is found for the provided key
*/
export const getRelativeTimeCalculation = (key) => {
const config = RELATIVE_TIME_OFFSETS[key];
if (config === undefined) {
throw new Error(`No offset configuration found for key: ${key}`);
}
return Object.assign({ amount: config.offset, unit: config.unit }, (config.unitReference !== undefined && { unitReference: config.unitReference }));
};
/**
* Builder function to create IRelativeTimePickerOption with automatic
* startDate/endDate derivation from RELATIVE_TIME_OFFSETS.
*
* @param label - Display label for the option
* @param value - Relative time range key
* @param comparisonConfig - How the time calculation should be performed
* @param labelRangeFormatter - Optional formatter for date range display
* @param endDateKey - Optional: If provided, derives endDate from this key
* @returns Complete IRelativeTimePickerOption
*
* @example
* ```typescript
* // Simple option
* buildRelativeTimeOption(
* 'Last 5 minutes',
* ERelativeTimeRangeKey.Last_5_m,
* ERelativeTimeComparisonConfig.RelativeAmountOfUnits,
* {
* startDateFormatter: 'HH:mm',
* separator: 'to',
* endDateFormatter: 'HH:mm'
* }
* )
* ```
*
* @example
* ```typescript
* // Option with endDate
* buildRelativeTimeOption(
* 'Yesterday',
* ERelativeTimeRangeKey.Yesterday,
* ERelativeTimeComparisonConfig.AbsoluteAmountOfUnits,
* { startDateFormatter: 'D MMM' },
* ERelativeTimeRangeKey.Yesterday_End
* )
* ```
*/
export const buildRelativeTimeOption = (label, value, comparisonConfig, labelRangeFormatter, endDateKey) => {
const option = Object.assign({ label,
value,
comparisonConfig, startDate: getRelativeTimeCalculation(value) }, (labelRangeFormatter !== undefined && { labelRangeFormatter }));
if (endDateKey !== undefined) {
option.endDate = getRelativeTimeCalculation(endDateKey);
}
return option;
};
/**
* Validates that all keys in RELATIVE_TIME_OFFSETS are valid enum values.
* Useful for development/testing to ensure consistency.
*
* @returns true if all keys are valid
* @throws {Error} If any invalid keys are found
*
* @example
* ```typescript
* // In tests or development
* validateRelativeTimeOffsets(); // throws if config is invalid
* ```
*/
export const validateRelativeTimeOffsets = () => {
const enumValues = Object.values(ERelativeTimeRangeKey);
const offsetKeys = Object.keys(RELATIVE_TIME_OFFSETS);
const invalidKeys = offsetKeys.filter(key => !enumValues.includes(key));
if (invalidKeys.length > 0) {
throw new Error(`Invalid keys in RELATIVE_TIME_OFFSETS: ${invalidKeys.join(', ')}`);
}
return true;
};
/**
* Default relative time option groups for the time picker component.
*
* @remarks
* This configuration is built using the `buildRelativeTimeOption` helper to ensure
* consistency with `RELATIVE_TIME_OFFSETS` and avoid data duplication.
*
* Organized into 5 groups:
* 1. Today/Yesterday
* 2. Day/Month ranges (24h, 48h, 7d, 30d, 90d, 6M)
* 3. Minute/Hour ranges (5m, 15m, 30m, 1h, 3h, 6h, 12h)
* 4. Long-term ranges (365d, 2y)
* 5. "So far" ranges (week, month, quarter, year)
*/
export const DEFAULT_RELATIVE_TIME_OPTIONS_GROUPS = [
[
buildRelativeTimeOption('Today', ERelativeTimeRangeKey.Today, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM',
separator: 'until',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Yesterday', ERelativeTimeRangeKey.Yesterday, ERelativeTimeComparisonConfig.AbsoluteAmountOfUnits, {
startDateFormatter: 'D MMM'
}, ERelativeTimeRangeKey.Yesterday_End)
],
[
buildRelativeTimeOption('Last 24 hours', ERelativeTimeRangeKey.Last_24_H, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM HH:mm',
separator: 'to',
endDateFormatter: 'D MMM HH:mm'
}),
buildRelativeTimeOption('Last 48 hours', ERelativeTimeRangeKey.Last_48_H, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM HH:mm',
separator: 'to',
endDateFormatter: 'D MMM HH:mm'
}),
buildRelativeTimeOption('Last 7 days', ERelativeTimeRangeKey.Last_7_D, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM',
separator: 'to',
endDateFormatter: 'D MMM'
}),
buildRelativeTimeOption('Last 30 days', ERelativeTimeRangeKey.Last_30_D, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM',
separator: 'to',
endDateFormatter: 'D MMM'
}),
buildRelativeTimeOption('Last 90 days', ERelativeTimeRangeKey.Last_90_D, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM',
separator: 'to',
endDateFormatter: 'D MMM'
}),
buildRelativeTimeOption('Last 6 months', ERelativeTimeRangeKey.Last_6_M, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM',
separator: 'to',
endDateFormatter: 'D MMM'
})
],
[
buildRelativeTimeOption('Last 5 minutes', ERelativeTimeRangeKey.Last_5_m, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Last 15 minutes', ERelativeTimeRangeKey.Last_15_m, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Last 30 minutes', ERelativeTimeRangeKey.Last_30_m, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Last 1 hour', ERelativeTimeRangeKey.Last_1_H, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Last 3 hours', ERelativeTimeRangeKey.Last_3_H, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Last 6 hours', ERelativeTimeRangeKey.Last_6_H, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
}),
buildRelativeTimeOption('Last 12 hours', ERelativeTimeRangeKey.Last_12_H, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'HH:mm',
separator: 'to',
endDateFormatter: 'HH:mm'
})
],
[
buildRelativeTimeOption('Last 365 days', ERelativeTimeRangeKey.Last_365_D, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM YYYY',
separator: 'to',
endDateFormatter: 'D MMM YYYY'
}),
buildRelativeTimeOption('Last 2 years', ERelativeTimeRangeKey.Last_2_Y, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM YYYY',
separator: 'to',
endDateFormatter: 'D MMM YYYY'
})
],
[
buildRelativeTimeOption('This week so far', ERelativeTimeRangeKey.This_Week_So_Far, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM HH:mm',
separator: 'to',
endDateFormatter: 'D MMM HH:mm'
}),
buildRelativeTimeOption('This month so far', ERelativeTimeRangeKey.This_Month_So_Far, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM HH:mm',
separator: 'to',
endDateFormatter: 'D MMM HH:mm'
}),
buildRelativeTimeOption('This quarter so far', ERelativeTimeRangeKey.This_Quarter_So_Far, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM HH:mm',
separator: 'to',
endDateFormatter: 'D MMM HH:mm'
}),
buildRelativeTimeOption('This year so far', ERelativeTimeRangeKey.This_Year_So_Far, ERelativeTimeComparisonConfig.RelativeAmountOfUnits, {
startDateFormatter: 'D MMM HH:mm',
separator: 'to',
endDateFormatter: 'D MMM HH:mm'
})
]
];
/**
* Base helper function to calculate relative time range.
* @param key the relative time key
* @param cursor the relative timestamp cursor (as dayjs instance)
* @returns DayjsTimeRange with start and end dates
* @private
*/
const getRelativeTimeRange = (key, cursor) => {
const { offset, unit } = RELATIVE_TIME_OFFSETS[key];
let startDate;
let endDate;
if (offset < 0) {
startDate = calculateOffsetDate(cursor, offset, unit);
endDate = cursor;
}
else {
endDate = calculateOffsetDate(cursor, offset, unit);
startDate = cursor;
}
return [startDate, endDate];
};
/**
* Returns a start and end time according to a relative time range in utc.
* @param key the relative time key
* @param cursor the relative timestamp cursor
* @returns an array with datetime strings
*/
export const getRelativeTimeRangeISO = (key, cursor = newDate().toISOString()) => {
const cursorDate = newDate(cursor);
const [startDate, endDate] = getRelativeTimeRange(key, cursorDate);
return [startDate.toISOString(), endDate.toISOString()];
};
/**
* Returns a start and end time according to a relative time range as timestamps.
* @param key the relative time key
* @param cursor the relative timestamp cursor
* @returns an array with timestamps
*/
export const getRelativeTimeRangeTimestamp = (key, cursor = newDate()) => {
const [startDate, endDate] = getRelativeTimeRange(key, cursor);
return [startDate.valueOf(), endDate.valueOf()];
};
/**
* Returns the provided range in utc.
* @range the absolute time range
* @offset the timezone offset
* @returns an array with datetime strings
*/
export const getAbsoluteTimeRange = (range, offset = 0) => {
return range.map(date => calculateOffsetDate(date, offset, 'minutes').toISOString());
};