Utilities
Helper functions exported by @tetherto/mdk-react-devkit/core (formatting, validation, conversions), @tetherto/mdk-react-devkit/foundation (settings persistence), and @tetherto/mdk-ui-core (alert query builders and time-range chunking)
This page documents helper functions exported by the MDK packages.
- Core utilities ships 15 utility modules with functions for formatting, dates, validation, conversions, class-name merging, and more
- Foundation utilities currently ships a single public utility module (
settings-utils) for parsing, validating, and exporting settings JSON - UI core alert utilities ships query-parameter builders and time-range chunking helpers for the Alerts feature
Core utilities
Helper functions exported by @tetherto/mdk-react-devkit/core for formatting, dates, validation, conversions, and class-name merging.
Prerequisites
Import
@tetherto/mdk-react-devkit/core
import {
formatNumber,
formatHashrate,
formatDate,
formatRelativeTime,
cn,
isEmpty,
isValidEmail,
} from '@tetherto/mdk-react-devkit/core'Formatting utilities
formatNumber
@tetherto/mdk-react-devkit/core
Format numbers with locale formatting and configurable options.
import { formatNumber, FALLBACK } from '@tetherto/mdk-react-devkit/core'
formatNumber(1234.567) // "1,234.57"
formatNumber(null) // "-"
formatNumber(1234, { minimumFractionDigits: 2 }) // "1,234.00"
formatNumber(undefined, {}, 'N/A') // "N/A"formatHashrate
@tetherto/mdk-react-devkit/core
Format hashrate values with rounding.
import { formatHashrate } from '@tetherto/mdk-react-devkit/core'
formatHashrate(150.456) // "150.46"
formatHashrate(null) // "-"formatCurrency
@tetherto/mdk-react-devkit/core
Format currency values.
import { formatCurrency } from '@tetherto/mdk-react-devkit/core'
formatCurrency(1234.56, 'USD') // "$1,234.56"
formatCurrency(0.00012345, 'BTC') // "₿0.00012345"getPercentFormattedNumber
@tetherto/mdk-react-devkit/core
Format numbers as percentages.
import { getPercentFormattedNumber } from '@tetherto/mdk-react-devkit/core'
getPercentFormattedNumber(0.75) // "75%"
getPercentFormattedNumber(0.1234, 1) // "12.3%"formatValueUnit
@tetherto/mdk-react-devkit/core
Format value-unit objects.
import { formatValueUnit } from '@tetherto/mdk-react-devkit/core'
formatValueUnit(150, 'TH/s') // "150 TH/s"Date utilities
formatDate
@tetherto/mdk-react-devkit/core
Format dates with customizable patterns.
import { formatDate } from '@tetherto/mdk-react-devkit/core'
formatDate(new Date()) // "Jan 15, 2025"
formatDate(1705334400000, { format: 'yyyy-MM-dd' }) // "2025-01-15"formatRelativeTime
@tetherto/mdk-react-devkit/core
Format dates as relative time strings.
import { formatRelativeTime } from '@tetherto/mdk-react-devkit/core'
formatRelativeTime(new Date(Date.now() - 3600000)) // "1h ago"
formatRelativeTime(new Date(Date.now() - 86400000)) // "1d ago"formatChartDate
@tetherto/mdk-react-devkit/core
Format timestamps for chart display.
import { formatChartDate } from '@tetherto/mdk-react-devkit/core'
formatChartDate(1705334400) // "Jan 15"
formatChartDate(1705334400, true) // "Jan 15, 2025"isValidTimestamp
@tetherto/mdk-react-devkit/core
Check if a timestamp is valid.
import { isValidTimestamp } from '@tetherto/mdk-react-devkit/core'
isValidTimestamp(1705334400000) // true
isValidTimestamp('invalid') // falseparseMonthLabelToDate
@tetherto/mdk-react-devkit/core
Parse month labels to Date objects.
import { parseMonthLabelToDate } from '@tetherto/mdk-react-devkit/core'
parseMonthLabelToDate('01-26') // Date(2026, 0, 1)
parseMonthLabelToDate('03-2025') // Date(2025, 2, 1)getPastDateFromDate
@tetherto/mdk-react-devkit/core
Get a date in the past.
import { getPastDateFromDate } from '@tetherto/mdk-react-devkit/core'
getPastDateFromDate({ dateTs: Date.now(), days: 7 }) // 7 days agoValidation utilities
isEmpty
@tetherto/mdk-react-devkit/core
Check if a value is empty.
import { isEmpty } from '@tetherto/mdk-react-devkit/core'
isEmpty(null) // true
isEmpty('') // true
isEmpty([]) // true
isEmpty({}) // true
isEmpty('hello') // false
isEmpty([1, 2, 3]) // falseisValidEmail
@tetherto/mdk-react-devkit/core
Validate email addresses.
import { isValidEmail } from '@tetherto/mdk-react-devkit/core'
isValidEmail('user@example.com') // true
isValidEmail('invalid') // falseisValidUrl
@tetherto/mdk-react-devkit/core
Validate URLs.
import { isValidUrl } from '@tetherto/mdk-react-devkit/core'
isValidUrl('https://example.com') // true
isValidUrl('not-a-url') // falseisNil
@tetherto/mdk-react-devkit/core
Check if value is null or undefined.
import { isNil } from '@tetherto/mdk-react-devkit/core'
isNil(null) // true
isNil(undefined) // true
isNil(0) // false
isNil('') // falseisPlainObject
@tetherto/mdk-react-devkit/core
Check if value is a plain object.
import { isPlainObject } from '@tetherto/mdk-react-devkit/core'
isPlainObject({}) // true
isPlainObject({ a: 1 }) // true
isPlainObject([]) // false
isPlainObject(new Date()) // falseClass name utilities
cn
@tetherto/mdk-react-devkit/core
Merge class names using clsx and tailwind-merge.
import { cn } from '@tetherto/mdk-react-devkit/core'
cn('px-4', 'py-2') // "px-4 py-2"
cn('text-red', isError && 'bg-red') // conditional classes
cn('p-4', { 'hidden': !visible }) // object syntaxConversion utilities
toMW / toMWh
@tetherto/mdk-react-devkit/core
Convert watts to megawatts.
import { toMW, toMWh } from '@tetherto/mdk-react-devkit/core'
toMW(1000000) // 1
toMWh(1000000) // 1toPHS
@tetherto/mdk-react-devkit/core
Convert raw hashrate to PH/s.
import { toPHS } from '@tetherto/mdk-react-devkit/core'
toPHS(1000000000000000) // 1convertMpaToBar
@tetherto/mdk-react-devkit/core
Convert pressure units.
import { convertMpaToBar } from '@tetherto/mdk-react-devkit/core'
convertMpaToBar(0.1) // 1unitToKilo
@tetherto/mdk-react-devkit/core
Convert to kilo units.
import { unitToKilo } from '@tetherto/mdk-react-devkit/core'
unitToKilo(1000) // 1Number utilities
percentage
@tetherto/mdk-react-devkit/core
Calculate percentage.
import { percentage } from '@tetherto/mdk-react-devkit/core'
percentage(25, 100) // 25
percentage(1, 4) // 25getPercentChange
@tetherto/mdk-react-devkit/core
Calculate percentage change.
import { getPercentChange } from '@tetherto/mdk-react-devkit/core'
getPercentChange(110, 100) // 10
getPercentChange(90, 100) // -10convertUnits
@tetherto/mdk-react-devkit/core
Convert between SI-prefix units.
import { convertUnits } from '@tetherto/mdk-react-devkit/core'
convertUnits(1, 'k', 'M') // 0.001
convertUnits(1000, 'decimal', 'k') // 1safeNumber
@tetherto/mdk-react-devkit/core
Safely convert to number.
import { safeNumber } from '@tetherto/mdk-react-devkit/core'
safeNumber('123') // 123
safeNumber('invalid') // 0
safeNumber(null) // 0String utilities
toTitleCase
@tetherto/mdk-react-devkit/core
Convert string to Title Case.
import { toTitleCase } from '@tetherto/mdk-react-devkit/core'
toTitleCase('hello world') // "Hello World"formatMacAddress
@tetherto/mdk-react-devkit/core
Format MAC addresses.
import { formatMacAddress } from '@tetherto/mdk-react-devkit/core'
formatMacAddress('aa:bb:cc:dd:ee:ff') // "AA:BB:CC:DD:EE:FF"safeString
@tetherto/mdk-react-devkit/core
Safely convert to string.
import { safeString } from '@tetherto/mdk-react-devkit/core'
safeString(123) // "123"
safeString(null) // ""Time utilities
secondsToMs
@tetherto/mdk-react-devkit/core
Convert seconds to milliseconds.
import { secondsToMs } from '@tetherto/mdk-react-devkit/core'
secondsToMs(60) // 60000breakTimeIntoIntervals
@tetherto/mdk-react-devkit/core
Split [start, end] into consecutive windows of intervalMs. Re-exported from @tetherto/mdk-ui-core.
import { breakTimeIntoIntervals } from '@tetherto/mdk-react-devkit/core'
breakTimeIntoIntervals(start, end, 3_600_000) // Array of 1-hour { start, end } windowstimeRangeWalker
@tetherto/mdk-react-devkit/core
Generator for iterating through time ranges.
import { timeRangeWalker } from '@tetherto/mdk-react-devkit/core'
for (const interval of timeRangeWalker(start, end, duration)) {
// Process each interval
}Color utilities
hexToRgba
@tetherto/mdk-react-devkit/core
Convert hex color to rgba.
import { hexToRgba } from '@tetherto/mdk-react-devkit/core'
hexToRgba('#72F59E', 0.5) // "rgba(114, 245, 158, 0.5)"Array utilities
getNestedValue
@tetherto/mdk-react-devkit/core
Get nested value by dot-path.
import { getNestedValue } from '@tetherto/mdk-react-devkit/core'
getNestedValue({ a: { b: 1 } }, 'a.b') // 1getWeightedAverage
@tetherto/mdk-react-devkit/core
Calculate weighted average.
import { getWeightedAverage } from '@tetherto/mdk-react-devkit/core'
getWeightedAverage(items, 'value', 'weight')circularArrayAccess
@tetherto/mdk-react-devkit/core
Create infinite cycling generator.
import { circularArrayAccess } from '@tetherto/mdk-react-devkit/core'
const colors = circularArrayAccess(['red', 'green', 'blue'])
colors.next().value // 'red'
colors.next().value // 'green'
colors.next().value // 'blue'
colors.next().value // 'red' (cycles)Foundation utilities
Helpers exported by @tetherto/mdk-react-devkit/foundation for filtering, formatting, validating, parsing, and exporting settings data.
Prerequisites
Import
@tetherto/mdk-react-devkit/foundation
import {
filterUsers,
formatRoleLabel,
formatLastActive,
validateSettingsJson,
parseSettingsFile,
exportSettingsToFile,
} from '@tetherto/mdk-react-devkit/foundation'Settings utilities
filterUsers
@tetherto/mdk-react-devkit/foundation
Filter a SettingsUser[] list by email substring (case-insensitive) and exact role match. Used by the user-management table search.
import { filterUsers } from '@tetherto/mdk-react-devkit/foundation'
filterUsers({
users,
email: 'alice', // partial, case-insensitive match on user.email
role: 'admin', // exact match on user.role; pass null to skip
})| Parameter | Type | Description |
|---|---|---|
users | SettingsUser[] | Source list |
email | string | null | undefined | Substring filter on email (case-insensitive). Skipped when falsy. |
role | string | null | undefined | Exact role match. Skipped when falsy. |
formatRoleLabel
@tetherto/mdk-react-devkit/foundation
Convert a snake case role identifier to a human-readable Title Case label.
import { formatRoleLabel } from '@tetherto/mdk-react-devkit/foundation'
formatRoleLabel('site_manager') // "Site Manager"
formatRoleLabel('reporting_tool_manager') // "Reporting Tool Manager"
formatRoleLabel('admin') // "Admin"formatLastActive
@tetherto/mdk-react-devkit/foundation
Format a timestamp string as MM/DD/YYYY - HH:MM. Returns '-' when the input is missing or invalid.
import { formatLastActive } from '@tetherto/mdk-react-devkit/foundation'
formatLastActive('2025-01-15T14:30:00Z') // "01/15/2025 - 14:30"
formatLastActive(undefined) // "-"
formatLastActive('not-a-date') // "-"validateSettingsJson
@tetherto/mdk-react-devkit/foundation
Type-guard that checks whether an unknown value is a valid SettingsExportData. Returns true if the value is an object that contains at least one of headerControls, featureFlags, or timestamp.
import { validateSettingsJson } from '@tetherto/mdk-react-devkit/foundation'
if (validateSettingsJson(parsed)) {
// parsed is now narrowed to SettingsExportData
}parseSettingsFile
@tetherto/mdk-react-devkit/foundation
Read a File containing settings JSON, validate it, and resolve to SettingsExportData. Rejects on invalid JSON, an invalid format, or a file read error.
import { parseSettingsFile } from '@tetherto/mdk-react-devkit/foundation'
try {
const settings = await parseSettingsFile(file)
applySettings(settings)
} catch (err) {
notifyError('Could not import settings', err.message)
}| Throws | Reason |
|---|---|
`Invalid settings file format. Please ensure the file is a valid ${WEBAPP_NAME} settings export.` | The JSON parsed but didn't match the SettingsExportData shape. The literal interpolates WEBAPP_NAME. |
Failed to parse JSON file. Please ensure the file is valid JSON. | The file contents weren't valid JSON. |
Failed to read file. | The browser couldn't read the file. |
exportSettingsToFile
@tetherto/mdk-react-devkit/foundation
Serialize SettingsExportData to JSON, package it as a downloadable Blob, and trigger a browser download. Returns the generated filename (e.g., mdk-settings-2025-01-15T14-30-00-000Z.json). The filename prefix is fixed in the foundation kit and does not interpolate WEBAPP_NAME.
import { exportSettingsToFile } from '@tetherto/mdk-react-devkit/foundation'
const filename = exportSettingsToFile({
headerControls: { poolMiners: true, consumption: false },
featureFlags: { betaCharts: true },
timestamp: new Date().toISOString(),
version: '1.0.0',
})UI core alert utilities
@tetherto/mdk-ui-core ships two utility modules for the Alerts feature: query-parameter builders (alert-queries) and time-range chunking helpers (historical-log-chunks). These are the source consumed directly by the useCurrentAlertDevices and useHistoricalAlerts adapter hooks.
Prerequisites
- Complete the @tetherto/mdk-ui-core installation
Import
@tetherto/mdk-ui-core
import {
ONE_DAY_MS,
DEFAULT_HISTORICAL_WINDOW_MS,
getDefaultHistoricalAlertsRange,
buildCurrentAlertDevicesParams,
buildHistoricalAlertsParams,
breakTimeIntoIntervals,
mergeAlertsByUuid,
fetchHistoricalAlertsInChunks,
} from '@tetherto/mdk-ui-core'Alert query builders
ONE_DAY_MS
@tetherto/mdk-ui-core
One day in milliseconds (86_400_000). The fetch-window size used by the historical-alerts data path.
import { ONE_DAY_MS } from '@tetherto/mdk-ui-core'
ONE_DAY_MS // 86400000DEFAULT_HISTORICAL_WINDOW_MS
@tetherto/mdk-ui-core
Default historical-alerts look-back window — 14 days (14 * ONE_DAY_MS). Matches the devkit <Alerts> feature default; wider ranges fan out into more 24-hour requests.
import { DEFAULT_HISTORICAL_WINDOW_MS } from '@tetherto/mdk-ui-core'
DEFAULT_HISTORICAL_WINDOW_MS // 1209600000 (14 days in ms)getDefaultHistoricalAlertsRange
@tetherto/mdk-ui-core
Returns the default historical-alerts range: the last DEFAULT_HISTORICAL_WINDOW_MS ending now. Used by the devkit <Alerts> feature and the shell Alerts page to seed their range state. Pass now to fix the upper bound in tests.
import { getDefaultHistoricalAlertsRange } from '@tetherto/mdk-ui-core'
const range = getDefaultHistoricalAlertsRange()
// { start: <14 days ago>, end: <now> }
// Injectable for tests
getDefaultHistoricalAlertsRange(1_700_000_000_000)
// { start: 1_700_000_000_000 - DEFAULT_HISTORICAL_WINDOW_MS, end: 1_700_000_000_000 }| Parameter | Status | Type | Default | Description |
|---|---|---|---|---|
now | Optional | number | Date.now() | Upper bound of the window (ms epoch). |
buildCurrentAlertDevicesParams
@tetherto/mdk-ui-core
Builds the list-things query params for the current-alerts table: every device that currently carries one or more alerts, with a 1 000 device limit. Consumed by useCurrentAlertDevices.
filterTags widen the server-side selector so the fetch narrows with the active search chips (ip-, sn-, mac-, firmware-) instead of relying on client-side filtering alone.
import { buildCurrentAlertDevicesParams } from '@tetherto/mdk-ui-core'
// All devices with active alerts
const params = buildCurrentAlertDevicesParams()
// Narrowed to devices matching the active search chips
const filtered = buildCurrentAlertDevicesParams(['ip-192.168.1.1', 'sn-ABC123'])| Parameter | Status | Type | Default | Description |
|---|---|---|---|---|
filterTags | Optional | string[] | [] | Active alert search chips to narrow the selector server-side. |
buildHistoricalAlertsParams
@tetherto/mdk-ui-core
Builds the history-log query params for a single alerts window (logType: 'alerts'). Called once per 24-hour sub-window by the chunked fetch.
import { buildHistoricalAlertsParams } from '@tetherto/mdk-ui-core'
const params = buildHistoricalAlertsParams({ start: range.start, end: range.end })
// { logType: 'alerts', start: ..., end: ... }| Parameter | Status | Type | Default | Description |
|---|---|---|---|---|
range | Required | HistoricalAlertsRange | none | { start: number; end: number } — ms epoch bounds for the window. |
Historical alert chunking
breakTimeIntoIntervals
@tetherto/mdk-ui-core
Splits [start, end] into consecutive windows of intervalMs (default ONE_DAY_MS); the final window is clamped to end. Returns an empty array for an empty or inverted range.
import { breakTimeIntoIntervals, ONE_DAY_MS } from '@tetherto/mdk-ui-core'
// 24-hour windows (default)
const windows = breakTimeIntoIntervals(start, end)
// Custom window size
const hourly = breakTimeIntoIntervals(start, end, 3_600_000)| Parameter | Status | Type | Default | Description |
|---|---|---|---|---|
start | Required | number | none | Range start (ms epoch). |
end | Required | number | none | Range end (ms epoch). |
intervalMs | Optional | number | ONE_DAY_MS | Window size in ms. |
Returns TimeInterval[] where each element is { start: number; end: number }.
mergeAlertsByUuid
@tetherto/mdk-ui-core
Concatenates next onto prev, replacing any row that shares a uuid (later windows win) and appending the rest. Rows without a uuid are always appended.
import { mergeAlertsByUuid } from '@tetherto/mdk-ui-core'
const merged = mergeAlertsByUuid(previousAlerts, newAlerts)| Parameter | Status | Type | Default | Description |
|---|---|---|---|---|
prev | Required | T[] | none | Existing alert rows. |
next | Required | T[] | none | Rows from the latest window fetch. |
Returns T[] where T extends { uuid?: string }.
fetchHistoricalAlertsInChunks
@tetherto/mdk-ui-core
Fetches a historical-alerts range as successive 24-hour windows (oldest to newest), merging results by uuid. Individual window failures are swallowed so one bad request does not drop the whole range. The loop bails out early when options.signal is aborted.
import {
fetchHistoricalAlertsInChunks,
buildHistoricalAlertsParams,
} from '@tetherto/mdk-ui-core'
const alerts = await fetchHistoricalAlertsInChunks(
{ start: range.start, end: range.end },
async (window) => {
const result = await myApi.historyLog(buildHistoricalAlertsParams(window))
return result.data
},
{ signal: abortController.signal },
)| Parameter | Status | Type | Default | Description |
|---|---|---|---|---|
range | Required | { start: number; end: number } | none | Full date range to paginate over (ms epoch). |
fetchWindow | Required | function | none | Called once per 24-hour window, oldest first. Receives a TimeInterval and returns Promise<T[]>. |
options.intervalMs | Optional | number | ONE_DAY_MS | Window size in ms. |
options.signal | Optional | AbortSignal | none | Checked between windows — aborts the loop early on range changes. |