mirror of
https://github.com/eclipse-mosquitto/mosquitto.git
synced 2026-09-21 23:47:52 +08:00
Finsh dashboard
* Introduce a queue to exclude contention between tasks while chartjs is updating charts/animating views. * Fix a bug where chart updates triggered by interval timeout aren't stored in the SessionStore. * Reinit all charts instead of reusing old chart objects when switching between raw data and smoothed data views. This avoids issues with buffered/stale state, which led to charts rendering incorrect data in certain cases. * Pull uptime informatin from the systree endpoint. Signed-off-by: Serhii Orlivskyi <serhii.orlivskyi@cedalo.com> (Cedalo GmbH)
This commit is contained in:
committed by
Roger Light
parent
d76389340c
commit
2a440bd3af
@@ -13,6 +13,7 @@ const SMOOTHED_CHART_UPDATE_INTERVAL_IN_MILLISECONDS =
|
||||
60 * // 1 minute
|
||||
5; // 5 minutes
|
||||
const INTERVAL_5SECS_IN_MILLISECONDS = 1000 * 5;
|
||||
const CHARTJS_ANIMATION_DURATION_MS = 400;
|
||||
const SYSTOPIC_ENDPOINT = "/api/systree";
|
||||
const VERSION_ENDPOINT = "/api/version";
|
||||
const CHART_DISPLAY_WINDOW = 16;
|
||||
|
||||
+143
-103
@@ -24,6 +24,7 @@ class MosquittoDashboard {
|
||||
this.timeoutHandler = null;
|
||||
|
||||
this.initializeCharts();
|
||||
this.addToggle();
|
||||
this.startDataUpdates();
|
||||
}
|
||||
|
||||
@@ -440,6 +441,11 @@ class MosquittoDashboard {
|
||||
chart.options.scales.x.min = newStart;
|
||||
chart.options.scales.x.max = newTotalLen - 1;
|
||||
chart.update();
|
||||
//chart.update("none");
|
||||
//Object.values(chart.options.scales).forEach((axisOptions) => {
|
||||
// delete axisOptions.min;
|
||||
// delete axisOptions.max;
|
||||
//});
|
||||
chart.resetZoom();
|
||||
break;
|
||||
default:
|
||||
@@ -449,6 +455,46 @@ class MosquittoDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
addToggle() {
|
||||
const toggleChartDataTypeButton = document.getElementById(
|
||||
"chart-data-type-global-toggle",
|
||||
);
|
||||
const toggleChartDataTypeText = document.getElementById(
|
||||
"chart-data-type-text",
|
||||
);
|
||||
|
||||
// set to an opposite state and toggle once to refresh the button captions etc
|
||||
if (this.dashboardDataObject.options.chartDataType === "raw") {
|
||||
this.dashboardDataObject.options.chartDataType = "smooth";
|
||||
} else {
|
||||
this.dashboardDataObject.options.chartDataType = "raw";
|
||||
}
|
||||
const handleChartDataTypeToggle = () => {
|
||||
if (this.dashboardDataObject.options.chartDataType === "raw") {
|
||||
this.dashboardDataObject.options.chartDataType = "smooth";
|
||||
this.destroyCharts();
|
||||
toggleChartDataTypeText.textContent = "Show Raw Data";
|
||||
this.addHtmlElementClass("smooth-state-svg", "hidden");
|
||||
this.removeHtmlElementClass("raw-state-svg", "hidden");
|
||||
} else {
|
||||
this.dashboardDataObject.options.chartDataType = "raw";
|
||||
this.destroyCharts();
|
||||
toggleChartDataTypeText.textContent = "Show Smoothed Data";
|
||||
this.addHtmlElementClass("raw-state-svg", "hidden");
|
||||
this.removeHtmlElementClass("smooth-state-svg", "hidden");
|
||||
}
|
||||
this.initializeCharts();
|
||||
sessionStorage.setItem(
|
||||
"options",
|
||||
JSON.stringify(this.dashboardDataObject.options),
|
||||
);
|
||||
};
|
||||
toggleChartDataTypeButton.addEventListener("click", () => {
|
||||
queue.enqueue(toAsyncAndWaitAfter(handleChartDataTypeToggle));
|
||||
});
|
||||
queue.enqueue(toAsyncAndWaitAfter(handleChartDataTypeToggle));
|
||||
}
|
||||
|
||||
initializeCharts() {
|
||||
let id = "";
|
||||
|
||||
@@ -508,48 +554,12 @@ class MosquittoDashboard {
|
||||
const chartId = e.target.dataset.chart;
|
||||
if (chartId) {
|
||||
const action = e.target.dataset.action;
|
||||
this.handleChartAction(chartId, action);
|
||||
queue.enqueue(
|
||||
toAsyncAndWaitAfter(() => this.handleChartAction(chartId, action)),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toggleChartDataTypeButton = document.getElementById(
|
||||
"chart-data-type-global-toggle",
|
||||
);
|
||||
const toggleChartDataTypeText = document.getElementById(
|
||||
"chart-data-type-text",
|
||||
);
|
||||
|
||||
// set to an opposite state and toggle once to refresh the button captions etc
|
||||
if (this.dashboardDataObject.options.chartDataType === "raw") {
|
||||
this.dashboardDataObject.options.chartDataType = "smooth";
|
||||
} else {
|
||||
this.dashboardDataObject.options.chartDataType = "raw";
|
||||
}
|
||||
const handleChartDataTypeToggle = () => {
|
||||
if (this.dashboardDataObject.options.chartDataType === "raw") {
|
||||
this.dashboardDataObject.options.chartDataType = "smooth";
|
||||
this.updateChartDataTypes("smooth");
|
||||
toggleChartDataTypeText.textContent = "Show Raw Data";
|
||||
this.addHtmlElementClass("smoothed-state-svg", "hidden");
|
||||
this.removeHtmlElementClass("raw-state-svg", "hidden");
|
||||
} else {
|
||||
this.dashboardDataObject.options.chartDataType = "raw";
|
||||
this.updateChartDataTypes("raw");
|
||||
toggleChartDataTypeText.textContent = "Show Smoothed Data";
|
||||
this.addHtmlElementClass("raw-state-svg", "hidden");
|
||||
this.removeHtmlElementClass("smoothed-state-svg", "hidden");
|
||||
}
|
||||
sessionStorage.setItem(
|
||||
"options",
|
||||
JSON.stringify(this.dashboardDataObject.options),
|
||||
);
|
||||
};
|
||||
toggleChartDataTypeButton.addEventListener(
|
||||
"click",
|
||||
handleChartDataTypeToggle,
|
||||
);
|
||||
handleChartDataTypeToggle();
|
||||
}
|
||||
|
||||
getChartDatasets(chartId) {
|
||||
@@ -606,38 +616,13 @@ class MosquittoDashboard {
|
||||
chart.options.scales.x.max = newTotalLen - 1;
|
||||
}
|
||||
|
||||
updateChartDataTypes(dataType) {
|
||||
for (const [chartId, chart] of Object.entries(this.charts)) {
|
||||
destroyCharts() {
|
||||
for (const [chartId, _] of Object.entries(this.charts)) {
|
||||
let data;
|
||||
let data2;
|
||||
let labels;
|
||||
[labels, data, data2] = this.getChartDatasets(chartId);
|
||||
|
||||
const [zoomLevel, currentEnd, lastX] = this.getChartPositionalData(chart);
|
||||
|
||||
if (dataType === "smooth") {
|
||||
data = data.smoothedData;
|
||||
data2 = data2 ? data.smoothedData : data2;
|
||||
labels = labels.smoothedLabels;
|
||||
} else {
|
||||
data = data.rawData;
|
||||
data2 = data2 ? data.smoothedData : data2;
|
||||
labels = labels.rawLabels;
|
||||
}
|
||||
|
||||
chart.data.labels = labels;
|
||||
chart.data.datasets[0].data = data;
|
||||
if (data2) {
|
||||
chart.data.datasets[1].data = data2;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isEndElementVisibleAndDefaultZoom(lastX, currentEnd, zoomLevel)
|
||||
) {
|
||||
this.slideChart(chart);
|
||||
}
|
||||
|
||||
chart.update();
|
||||
this.charts[chartId].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +632,9 @@ class MosquittoDashboard {
|
||||
|
||||
updateMatchingChart(chartId, sysTopics, chartIdsToUpdate) {
|
||||
const createErrorMsg = (matchingChartId, matchingChartSysTopic) =>
|
||||
`datapoint doesn't exist in current sysTopic data for the chart "${matchingChartId}" matching the chart "${chartId}". Matching chart sys topic: ${matchingChartSysTopic}. Available sys topics: ${JSON.stringify(sysTopics)}`;
|
||||
`datapoint doesn't exist in current sysTopic data for the chart "${matchingChartId}" matching the chart "${chartId}". Matching chart sys topic: ${matchingChartSysTopic}. Available sys topics: ${JSON.stringify(
|
||||
sysTopics,
|
||||
)}`;
|
||||
|
||||
if (chartId === "chart-messages-sent") {
|
||||
const matchingChartId = "chart-messages-received";
|
||||
@@ -699,6 +686,7 @@ class MosquittoDashboard {
|
||||
|
||||
// sys topics object looks as follows:
|
||||
//{
|
||||
// "$SYS/broker/uptime": 99999,
|
||||
// "$SYS/broker/clients/total": 0,
|
||||
// "$SYS/broker/clients/maximum": 1,
|
||||
// "$SYS/broker/clients/disconnected": 0,
|
||||
@@ -724,6 +712,16 @@ class MosquittoDashboard {
|
||||
// "$SYS/broker/publish/messages/received": 0,
|
||||
// "$SYS/broker/publish/messages/sent": 0
|
||||
//}
|
||||
topic = "$SYS/broker/uptime";
|
||||
if (
|
||||
sysTopics[topic] !== undefined &&
|
||||
this.dashboardDataObject.lastSysTopics[topic] !== sysTopics[topic]
|
||||
) {
|
||||
this.updateLastSysTopics(topic, sysTopics[topic]);
|
||||
htmlIdsToUpdate["broker-uptime"] = secondsToIntervalString(
|
||||
sysTopics[topic],
|
||||
);
|
||||
}
|
||||
|
||||
topic = "$SYS/broker/clients/total";
|
||||
if (
|
||||
@@ -1094,7 +1092,9 @@ class MosquittoDashboard {
|
||||
|
||||
setMustUpdateForMatchingGraph(chartId) {
|
||||
const createAssertErrorMsg = (id) =>
|
||||
`mustUpdate option not found for chart "${id}". Available options: ${JSON.stringify(this.dashboardDataObject.charts[id]?.options)}. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`;
|
||||
`mustUpdate option not found for chart "${id}". Available options: ${JSON.stringify(
|
||||
this.dashboardDataObject.charts[id]?.options,
|
||||
)}. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`;
|
||||
let oppositeChartId;
|
||||
|
||||
if (chartId === "chart-messages-sent") {
|
||||
@@ -1187,15 +1187,21 @@ class MosquittoDashboard {
|
||||
);
|
||||
assertExistence(
|
||||
chartData,
|
||||
`Data for the chart "${id}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Data for the chart "${id}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
assertExistence(
|
||||
chartLabels,
|
||||
`Labels for the chart "${id}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Labels for the chart "${id}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
assertExistence(
|
||||
chartOptions,
|
||||
`Options for the chart "${id}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Options for the chart "${id}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
|
||||
this.processChartOverflow(
|
||||
@@ -1252,20 +1258,28 @@ class MosquittoDashboard {
|
||||
);
|
||||
assertExistence(
|
||||
firstChartData,
|
||||
`Data for the first sub chart with id "${firstSubChartId}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Data for the first sub chart with id "${firstSubChartId}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
assertExistence(
|
||||
firstChartLabels,
|
||||
`Labels for the first sub chart with id "${firstSubChartId}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Labels for the first sub chart with id "${firstSubChartId}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
|
||||
assertExistence(
|
||||
secondChartData,
|
||||
`Data for the second sub chart with "${secondSubChartId}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Data for the second sub chart with "${secondSubChartId}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
assertExistence(
|
||||
secondChartLabels,
|
||||
`Labels for the second sub chart with id "${secondSubChartId}" not found. Available charts: ${Object.keys(this.dashboardDataObject.charts)}`,
|
||||
`Labels for the second sub chart with id "${secondSubChartId}" not found. Available charts: ${Object.keys(
|
||||
this.dashboardDataObject.charts,
|
||||
)}`,
|
||||
);
|
||||
|
||||
const [zoomLevel, currentEnd, lastX, secondToLastX] =
|
||||
@@ -1282,12 +1296,6 @@ class MosquittoDashboard {
|
||||
) {
|
||||
this.slideChart(chart);
|
||||
}
|
||||
id === "chart-message-overview" &&
|
||||
console.log(
|
||||
"chart message overview is updating",
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
chart.update();
|
||||
}
|
||||
|
||||
@@ -1343,13 +1351,8 @@ class MosquittoDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
updateCharts(
|
||||
chartData,
|
||||
dashboardDataObject,
|
||||
timestampMilliseconds,
|
||||
isUpdatingAllCharts,
|
||||
) {
|
||||
const lastDataPoints = {
|
||||
getLastChartsDataPoints(dashboardDataObject) {
|
||||
const lastChartsDataPoints = {
|
||||
"chart-messages-sent":
|
||||
dashboardDataObject.lastSysTopics["$SYS/broker/messages/sent"],
|
||||
"chart-messages-received":
|
||||
@@ -1367,6 +1370,16 @@ class MosquittoDashboard {
|
||||
"chart-clients-disconnected":
|
||||
dashboardDataObject.lastSysTopics["$SYS/broker/clients/disconnected"],
|
||||
};
|
||||
return lastChartsDataPoints;
|
||||
}
|
||||
|
||||
updateCharts(
|
||||
chartData,
|
||||
dashboardDataObject,
|
||||
timestampMilliseconds,
|
||||
isUpdatingAllCharts,
|
||||
) {
|
||||
const lastDataPoints = this.getLastChartsDataPoints(dashboardDataObject);
|
||||
let id = "";
|
||||
|
||||
id = "chart-messages-sent";
|
||||
@@ -1467,7 +1480,6 @@ class MosquittoDashboard {
|
||||
}
|
||||
|
||||
async checkForDataUpdates() {
|
||||
// TODO: update uptime here
|
||||
const nowTimestampMilliseconds = new Date().getTime();
|
||||
let sysTopics = null;
|
||||
try {
|
||||
@@ -1520,11 +1532,23 @@ class MosquittoDashboard {
|
||||
updateAllCharts,
|
||||
);
|
||||
|
||||
this.updateStore(this.dashboardDataObject, chartsToUpdate || {});
|
||||
let chartsIds;
|
||||
if (updateAllCharts) {
|
||||
const lastDataPointsOfAllCharts = this.getLastChartsDataPoints(
|
||||
this.dashboardDataObject,
|
||||
);
|
||||
// importantly this gives us ids of all charts
|
||||
chartsIds = Object.keys(lastDataPointsOfAllCharts);
|
||||
} else if (chartsToUpdate) {
|
||||
chartsIds = Object.keys(chartsToUpdate);
|
||||
} else {
|
||||
chartsIds = []; // nothing to update
|
||||
}
|
||||
this.updateStore(this.dashboardDataObject, chartsIds);
|
||||
}
|
||||
}
|
||||
|
||||
updateStore(dashboardDataObject, chartsToUpdate) {
|
||||
updateStore(dashboardDataObject, idsOfChartsToUpdate) {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"options",
|
||||
@@ -1538,7 +1562,7 @@ class MosquittoDashboard {
|
||||
"updateDueToIntervalTimestamp",
|
||||
JSON.stringify(dashboardDataObject.lastUpdateDueToIntervalTimestamp),
|
||||
);
|
||||
for (const key of Object.keys(chartsToUpdate)) {
|
||||
for (const key of idsOfChartsToUpdate) {
|
||||
const chartData = dashboardDataObject.charts[key];
|
||||
if (!chartData) {
|
||||
throw new Error(
|
||||
@@ -1554,10 +1578,10 @@ class MosquittoDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
startDataUpdates() {
|
||||
const checkForDataUpdatesWrapper = () => {
|
||||
async startDataUpdates() {
|
||||
const checkForDataUpdatesWrapper = async () => {
|
||||
try {
|
||||
this.checkForDataUpdates();
|
||||
await this.checkForDataUpdates();
|
||||
} catch (error) {
|
||||
const errorMsg = `Error while checking for dashboard data updates ${error?.message}. Reopen the page to try again.`;
|
||||
console.error(errorMsg);
|
||||
@@ -1567,7 +1591,7 @@ class MosquittoDashboard {
|
||||
};
|
||||
|
||||
try {
|
||||
checkForDataUpdatesWrapper();
|
||||
await checkForDataUpdatesWrapper();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
@@ -1580,20 +1604,36 @@ class MosquittoDashboard {
|
||||
|
||||
const interval = nextTimestampDivisibleBy5Seconds - timestampNow;
|
||||
|
||||
const doDataUpdate = () => {
|
||||
const doDataUpdate = async () => {
|
||||
clearTimeout(this.timeoutHandler);
|
||||
|
||||
let startTs, endTs;
|
||||
try {
|
||||
checkForDataUpdatesWrapper();
|
||||
startTs = Date.now();
|
||||
await checkForDataUpdatesWrapper();
|
||||
endTs = Date.now();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
// TODO: await for the checkForDataUpdatesWrapper and then calculate next tick taking into account the execution time the function took
|
||||
const executionTimeMs = endTs - startTs;
|
||||
|
||||
this.timeoutHandler = setTimeout(
|
||||
doDataUpdate,
|
||||
INTERVAL_5SECS_IN_MILLISECONDS,
|
||||
// don't want anything to get into a contending state while animation is running, so wait a bit after doDataUpdate returns
|
||||
() =>
|
||||
queue.enqueue(
|
||||
toAsyncAndWaitAfter(
|
||||
doDataUpdate,
|
||||
CHARTJS_ANIMATION_DURATION_MS + 50,
|
||||
),
|
||||
),
|
||||
INTERVAL_5SECS_IN_MILLISECONDS - executionTimeMs > 0
|
||||
? INTERVAL_5SECS_IN_MILLISECONDS - executionTimeMs
|
||||
: 0,
|
||||
);
|
||||
};
|
||||
this.timeoutHandler = setTimeout(doDataUpdate, interval);
|
||||
this.timeoutHandler = setTimeout(
|
||||
() => queue.enqueue(doDataUpdate),
|
||||
interval,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">uptime:</span>
|
||||
<span class="ml-2" id="broker-uptime">6d 18h 43m 22sec</span>
|
||||
<span class="ml-2" id="broker-uptime">?</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">status:</span>
|
||||
@@ -182,7 +182,7 @@
|
||||
<div class="mb-4 flex justify-end">
|
||||
<button id="chart-data-type-global-toggle" class="mr-2 inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors duration-200">
|
||||
<!-- for when we are on raw data state -->
|
||||
<svg id="smoothed-state-svg" class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg id="smooth-state-svg" class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12c2-4 4-4 6 0s4 4 6 0 4-4 6 0"></path>
|
||||
</svg>
|
||||
<!-- for when we are on smoothed data state -->
|
||||
@@ -697,6 +697,7 @@
|
||||
<script src="lib/chart.umd.js"></script>
|
||||
<script src="lib/chartjs-plugin-zoom.min.js"></script>
|
||||
<script src="consts.js"></script>
|
||||
<script src="queue.js"></script>
|
||||
<script src="assert.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script src="sidebar.js"></script>
|
||||
|
||||
@@ -31,7 +31,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
storedSetting = JSON.parse(storedSetting);
|
||||
if (storedSetting === false) {
|
||||
// set isGridView from the default value of true to match the "false" coming from the session store by calling the toggle function
|
||||
toggleView();
|
||||
queue.enqueue(toAsyncAndWaitAfter(toggleView));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class Queue {
|
||||
constructor() {
|
||||
this.tasks = [];
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
enqueue(task) {
|
||||
this.tasks.push(task);
|
||||
this.#dequeue();
|
||||
}
|
||||
|
||||
async #dequeue() {
|
||||
if (this.active) {
|
||||
return;
|
||||
}
|
||||
this.active = true;
|
||||
while (this.tasks.length) {
|
||||
const task = this.tasks.shift();
|
||||
try {
|
||||
await task();
|
||||
} catch (err) {
|
||||
console.error("Error in queue:", err);
|
||||
}
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
const queue = new Queue();
|
||||
@@ -1,3 +1,24 @@
|
||||
function toAsyncAndWaitAfter(task, delay = 0) {
|
||||
return () => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
let result;
|
||||
try {
|
||||
result = task();
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
if (delay) {
|
||||
setTimeout(() => {
|
||||
resolve(result);
|
||||
}, delay);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchData(endpoint) {
|
||||
if (!endpoint) {
|
||||
throw new Error("No endpoint provided to fetch data function");
|
||||
@@ -64,3 +85,58 @@ function prettifyNumber(number) {
|
||||
|
||||
return prettifiedNumber;
|
||||
}
|
||||
|
||||
function secondsToIntervalString(number) {
|
||||
const minuteInSeconds = 60;
|
||||
const hourInSeconds = minuteInSeconds * 60;
|
||||
const dayInSeconds = hourInSeconds * 24;
|
||||
const yearInSeconds = dayInSeconds * 365;
|
||||
|
||||
if (typeof number !== "number") {
|
||||
throw new Error(
|
||||
`Invalid datatype for converting into interval string. Expected: number. Got: ${typeof number}`,
|
||||
);
|
||||
}
|
||||
if (number < 0) {
|
||||
throw new Error(
|
||||
`Invalid value for converting into interval string. Received negative number: ${number}`,
|
||||
);
|
||||
}
|
||||
|
||||
let intervalString = "";
|
||||
|
||||
const years = Math.floor(number / yearInSeconds);
|
||||
number = number % yearInSeconds;
|
||||
if (years) {
|
||||
intervalString += years === 1 ? "1 year " : `${years} years `;
|
||||
}
|
||||
|
||||
const days = Math.floor(number / dayInSeconds);
|
||||
number = number % dayInSeconds;
|
||||
if (days) {
|
||||
intervalString += days === 1 ? "1 day " : `${days} days `;
|
||||
}
|
||||
|
||||
const hours = Math.floor(number / hourInSeconds);
|
||||
number = number % hourInSeconds;
|
||||
if (hours) {
|
||||
intervalString += hours === 1 ? "1 hour " : `${hours} hours `;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(number / minuteInSeconds);
|
||||
number = number % minuteInSeconds;
|
||||
if (minutes) {
|
||||
intervalString += minutes === 1 ? "1 minute " : `${minutes} minutes `;
|
||||
}
|
||||
|
||||
const seconds = number;
|
||||
if (seconds) {
|
||||
intervalString += seconds === 1 ? "1 second " : `${seconds} seconds `;
|
||||
}
|
||||
|
||||
if (!intervalString) {
|
||||
return "0 seconds"; // This would be strange if this happened. Maybe better to throw an error
|
||||
}
|
||||
|
||||
return intervalString;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user