feat(ui): Convert interval seconds to days, hours, minutes, and seconds (#5220)
Some checks are pending
Auto Test / auto-test (18, ARM64) (push) Blocked by required conditions
Auto Test / auto-test (18, macos-latest) (push) Blocked by required conditions
Auto Test / auto-test (18, ubuntu-latest) (push) Blocked by required conditions
Auto Test / auto-test (18, windows-latest) (push) Blocked by required conditions
Auto Test / auto-test (20, ARM64) (push) Blocked by required conditions
Auto Test / auto-test (20, macos-latest) (push) Blocked by required conditions
Auto Test / auto-test (20, ubuntu-latest) (push) Blocked by required conditions
Auto Test / auto-test (20, windows-latest) (push) Blocked by required conditions
Auto Test / armv7-simple-test (18, ARMv7) (push) Waiting to run
Auto Test / armv7-simple-test (20, ARMv7) (push) Waiting to run
Auto Test / check-linters (push) Waiting to run
Auto Test / e2e-test (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Merge Conflict Labeler / Labeling (push) Waiting to run
validate / json-yaml-validate (push) Waiting to run
validate / validate (push) Waiting to run

Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
Vivek Pandey 2025-07-26 13:36:51 +05:30 committed by GitHub
parent 2a6d9b4acd
commit c1adcfbfc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 463 additions and 107 deletions

View file

@ -720,6 +720,17 @@ let needSetup = false;
monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
/*
* List of frontend-only properties that should not be saved to the database.
* Should clean up before saving to the database.
*/
const frontendOnlyProperties = [ "humanReadableInterval" ];
for (const prop of frontendOnlyProperties) {
if (prop in monitor) {
delete monitor[prop];
}
}
bean.import(monitor);
bean.user_id = socket.userID;

View file

@ -1,5 +1,5 @@
import { currentLocale } from "../i18n";
import { setPageLocale } from "../util-frontend";
import { setPageLocale, relativeTimeFormatter } from "../util-frontend";
const langModules = import.meta.glob("../lang/*.json");
export default {
@ -28,11 +28,13 @@ export default {
* @returns {Promise<void>}
*/
async changeLang(lang) {
let message = (await langModules["../lang/" + lang + ".json"]()).default;
let message = (await langModules["../lang/" + lang + ".json"]())
.default;
this.$i18n.setLocaleMessage(lang, message);
this.$i18n.locale = lang;
localStorage.locale = lang;
setPageLocale();
}
}
relativeTimeFormatter.updateLocale(lang);
},
},
};

View file

@ -1,7 +1,9 @@
<template>
<transition name="slide-fade" appear>
<div v-if="monitor">
<router-link v-if="group !== ''" :to="monitorURL(monitor.parent)"> {{ group }}</router-link>
<router-link v-if="group !== ''" :to="monitorURL(monitor.parent)">
{{ group }}
</router-link>
<h1>
{{ monitor.name }}
<div class="monitor-id">
@ -13,61 +15,124 @@
<p v-if="monitor.description" v-html="descriptionHTML"></p>
<div class="d-flex">
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
<Tag
v-for="tag in monitor.tags"
:key="tag.id"
:item="tag"
:size="'sm'"
/>
</div>
</div>
<p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' || monitor.type === 'real-browser' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
<a
v-if="
monitor.type === 'http' ||
monitor.type === 'keyword' ||
monitor.type === 'json-query' ||
monitor.type === 'mp-health' ||
monitor.type === 'real-browser'
"
:href="monitor.url"
target="_blank"
rel="noopener noreferrer"
>{{ filterPassword(monitor.url) }}</a>
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">
<br>
<br />
<span>{{ $t("Keyword") }}: </span>
<span class="keyword">{{ monitor.keyword }}</span>
<span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> </span>
<span
v-if="monitor.invertKeyword"
alt="Inverted keyword"
class="keyword-inverted"
>
</span>
</span>
<span v-if="monitor.type === 'json-query'">
<br>
<span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span>
<br>
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
<br />
<span>{{ $t("Json Query") }}:</span>
<span class="keyword">{{ monitor.jsonPath }}</span>
<br />
<span>{{ $t("Expected Value") }}:</span>
<span class="keyword">{{ monitor.expectedValue }}</span>
</span>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<br>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
<br />
<span>{{ $t("Last Result") }}:</span>
<span class="keyword">{{ monitor.dns_last_result }}</span>
</span>
<span v-if="monitor.type === 'docker'">Docker container: {{ monitor.docker_container }}</span>
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{
monitor.port
}}</span>
<span v-if="monitor.type === 'grpc-keyword'">gRPC - {{ filterPassword(monitor.grpcUrl) }}
<br>
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
<br />
<span>{{ $t("Keyword") }}:</span>
<span class="keyword">{{ monitor.keyword }}</span>
</span>
<span v-if="monitor.type === 'mongodb'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{ monitor.mqttTopic }}</span>
<span v-if="monitor.type === 'mysql'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'postgres'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'push'">Push: <a :href="pushURL" target="_blank" rel="noopener noreferrer">{{ pushURL }}</a></span>
<span v-if="monitor.type === 'mongodb'">{{
filterPassword(monitor.databaseConnectionString)
}}</span>
<span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{
monitor.mqttTopic
}}</span>
<span v-if="monitor.type === 'mysql'">{{
filterPassword(monitor.databaseConnectionString)
}}</span>
<span v-if="monitor.type === 'postgres'">{{
filterPassword(monitor.databaseConnectionString)
}}</span>
<span v-if="monitor.type === 'push'">Push:
<a
:href="pushURL"
target="_blank"
rel="noopener noreferrer"
>{{ pushURL }}</a></span>
<span v-if="monitor.type === 'radius'">Radius: {{ filterPassword(monitor.hostname) }}</span>
<span v-if="monitor.type === 'redis'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'sqlserver'">SQL Server: {{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'redis'">{{
filterPassword(monitor.databaseConnectionString)
}}</span>
<span v-if="monitor.type === 'sqlserver'">SQL Server:
{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{
monitor.port
}}</span>
</p>
<div class="functions">
<div class="btn-group" role="group">
<button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
<button
v-if="monitor.active"
class="btn btn-normal"
@click="pauseDialog"
>
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button>
<button v-if="! monitor.active" class="btn btn-primary" :disabled="monitor.forceInactive" @click="resumeMonitor">
<button
v-if="!monitor.active"
class="btn btn-primary"
:disabled="monitor.forceInactive"
@click="resumeMonitor"
>
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
<router-link
:to="'/edit/' + monitor.id"
class="btn btn-normal"
>
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
</router-link>
<router-link :to=" '/clone/' + monitor.id " class="btn btn-normal">
<router-link
:to="'/clone/' + monitor.id"
class="btn btn-normal"
>
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
</router-link>
<button class="btn btn-normal text-danger" @click="deleteDialog">
<button
class="btn btn-normal text-danger"
@click="deleteDialog"
>
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
</div>
@ -77,29 +142,53 @@
<div class="row">
<div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" />
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
<span class="word">{{
$t("checkEverySecond", [monitor.interval])
}}
({{
secondsToHumanReadableFormat(monitor.interval)
}})</span>
</div>
<div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;" data-testid="monitor-status">{{ status.text }}</span>
<span
class="badge rounded-pill"
:class="'bg-' + status.color"
style="font-size: 30px;"
data-testid="monitor-status"
>{{ status.text }}</span>
</div>
</div>
</div>
<!-- Push Examples -->
<div v-if="monitor.type === 'push'" class="shadow-box big-padding">
<a href="#" @click="pushMonitor.showPushExamples = !pushMonitor.showPushExamples">{{ $t("pushViewCode") }}</a>
<a
href="#"
@click="
pushMonitor.showPushExamples =
!pushMonitor.showPushExamples
"
>{{ $t("pushViewCode") }}</a>
<transition name="slide-fade" appear>
<div v-if="pushMonitor.showPushExamples" class="mt-3">
<select id="push-current-example" v-model="pushMonitor.currentExample" class="form-select">
<select
id="push-current-example"
v-model="pushMonitor.currentExample"
class="form-select"
>
<optgroup :label="$t('programmingLanguages')">
<option value="csharp">C#</option>
<option value="go">Go</option>
<option value="java">Java</option>
<option value="javascript-fetch">JavaScript (fetch)</option>
<option value="javascript-fetch">
JavaScript (fetch)
</option>
<option value="php">PHP</option>
<option value="python">Python</option>
<option value="typescript-fetch">TypeScript (fetch)</option>
<option value="typescript-fetch">
TypeScript (fetch)
</option>
</optgroup>
<optgroup :label="$t('pushOthers')">
<option value="bash-curl">Bash (curl)</option>
@ -108,7 +197,13 @@
</optgroup>
</select>
<prism-editor v-model="pushMonitor.code" class="css-editor mt-3" :highlight="pushExampleHighlighter" line-numbers readonly></prism-editor>
<prism-editor
v-model="pushMonitor.code"
class="css-editor mt-3"
:highlight="pushExampleHighlighter"
line-numbers
readonly
></prism-editor>
</div>
</transition>
</div>
@ -116,55 +211,98 @@
<!-- Stats -->
<div class="shadow-box big-padding text-center stats">
<div class="row">
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<div
v-if="monitor.type !== 'group'"
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
>
<h4 class="col-4 col-sm-12">{{ pingTitle() }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("Current") }})</p>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
({{ $t("Current") }})
</p>
<span class="col-4 col-sm-12 num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
<a
href="#"
@click.prevent="
showPingChartBox = !showPingChartBox
"
>
<CountUp :value="ping" />
</a>
</span>
</div>
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<div
v-if="monitor.type !== 'group'"
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
>
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(24{{ $t("-hour") }})
</p>
<span class="col-4 col-sm-12 num">
<CountUp :value="avgPing" />
</span>
</div>
<!-- Uptime (24-hour) -->
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<div
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
>
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(24{{ $t("-hour") }})
</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="24" />
</span>
</div>
<!-- Uptime (30-day) -->
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<div
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
>
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(30{{ $t("-day") }})</p>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(30{{ $t("-day") }})
</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="720" />
</span>
</div>
<!-- Uptime (1-year) -->
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<div
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
>
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(1{{ $t("-year") }})</p>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(1{{ $t("-year") }})
</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="1y" />
</span>
</div>
<div v-if="tlsInfo" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<div
v-if="tlsInfo"
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
>
<h4 class="col-4 col-sm-12">{{ $t("Cert Exp.") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
(<Datetime
:value="tlsInfo.certInfo.validTo"
date-only
/>)
</p>
<span class="col-4 col-sm-12 num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
<a
href="#"
@click.prevent="
toggleCertInfoBox = !toggleCertInfoBox
"
>{{ tlsInfo.certInfo.daysRemaining }}
{{
$tc("day", tlsInfo.certInfo.daysRemaining)
}}</a>
</span>
</div>
</div>
@ -172,17 +310,26 @@
<!-- Cert Info Box -->
<transition name="slide-fade" appear>
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
<div
v-if="showCertInfoBox"
class="shadow-box big-padding text-center"
>
<div class="row">
<div class="col">
<certificate-info :certInfo="tlsInfo.certInfo" :valid="tlsInfo.valid" />
<certificate-info
:certInfo="tlsInfo.certInfo"
:valid="tlsInfo.valid"
/>
</div>
</div>
</div>
</transition>
<!-- Ping Chart -->
<div v-if="showPingChartBox" class="shadow-box big-padding text-center ping-chart-wrapper">
<div
v-if="showPingChartBox"
class="shadow-box big-padding text-center ping-chart-wrapper"
>
<div class="row">
<div class="col">
<PingChart :monitor-id="monitor.id" />
@ -194,25 +341,46 @@
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
<div class="row">
<div class="col-md-6 zoom-cursor">
<img :src="screenshotURL" style="width: 100%;" alt="screenshot of the website" @click="showScreenshotDialog">
<img
:src="screenshotURL"
style="width: 100%;"
alt="screenshot of the website"
@click="showScreenshotDialog"
/>
</div>
<ScreenshotDialog ref="screenshotDialog" :imageURL="screenshotURL" />
<ScreenshotDialog
ref="screenshotDialog"
:imageURL="screenshotURL"
/>
</div>
</div>
<div class="shadow-box table-shadow-box">
<div class="dropdown dropdown-clear-data">
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown">
<font-awesome-icon icon="trash" /> {{ $t("Clear Data") }}
<button
class="btn btn-sm btn-outline-danger dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
>
<font-awesome-icon icon="trash" />
{{ $t("Clear Data") }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button type="button" class="dropdown-item" @click="clearEventsDialog">
<button
type="button"
class="dropdown-item"
@click="clearEventsDialog"
>
{{ $t("Events") }}
</button>
</li>
<li>
<button type="button" class="dropdown-item" @click="clearHeartbeatsDialog">
<button
type="button"
class="dropdown-item"
@click="clearHeartbeatsDialog"
>
{{ $t("Heartbeats") }}
</button>
</li>
@ -227,9 +395,15 @@
</tr>
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" style="padding: 10px;">
<tr
v-for="(beat, index) in displayedRecords"
:key="index"
style="padding: 10px;"
>
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td :class="{ 'border-0': !beat.msg }">
<Datetime :value="beat.time" />
</td>
<td class="border-0">{{ beat.msg }}</td>
</tr>
@ -251,19 +425,42 @@
</div>
</div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseMonitor">
<Confirm
ref="confirmPause"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="pauseMonitor"
>
{{ $t("pauseMonitorMsg") }}
</Confirm>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMonitor">
<Confirm
ref="confirmDelete"
btn-style="btn-danger"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="deleteMonitor"
>
{{ $t("deleteMonitorMsg") }}
</Confirm>
<Confirm ref="confirmClearEvents" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearEvents">
<Confirm
ref="confirmClearEvents"
btn-style="btn-danger"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="clearEvents"
>
{{ $t("clearEventsMsg") }}
</Confirm>
<Confirm ref="confirmClearHeartbeats" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearHeartbeats">
<Confirm
ref="confirmClearHeartbeats"
btn-style="btn-danger"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="clearHeartbeats"
>
{{ $t("clearHeartbeatsMsg") }}
</Confirm>
</div>
@ -281,14 +478,16 @@ import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
const PingChart = defineAsyncComponent(() =>
import("../components/PingChart.vue")
);
import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { getResBaseURL } from "../util-frontend";
import { getResBaseURL, relativeTimeFormatter } from "../util-frontend";
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-clike";
import "prismjs/components/prism-javascript";
@ -310,7 +509,7 @@ export default {
Tag,
CertificateInfo,
PrismEditor,
ScreenshotDialog
ScreenshotDialog,
},
data() {
return {
@ -344,7 +543,10 @@ export default {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.cacheTime = Date.now();
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
if (
this.monitor.id in this.$root.lastHeartbeatList &&
this.$root.lastHeartbeatList[this.monitor.id]
) {
return this.$root.lastHeartbeatList[this.monitor.id];
}
@ -362,7 +564,10 @@ export default {
},
avgPing() {
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
if (
this.$root.avgPingList[this.monitor.id] ||
this.$root.avgPingList[this.monitor.id] === 0
) {
return this.$root.avgPingList[this.monitor.id];
}
@ -381,7 +586,10 @@ export default {
// Add: this.$root.tlsInfoList[this.monitor.id].certInfo
// Fix: TypeError: Cannot read properties of undefined (reading 'validTo')
// Reason: TLS Info object format is changed in 1.8.0, if for some reason, it cannot connect to the site after update to 1.8.0, the object is still in the old format.
if (this.$root.tlsInfoList[this.monitor.id] && this.$root.tlsInfoList[this.monitor.id].certInfo) {
if (
this.$root.tlsInfoList[this.monitor.id] &&
this.$root.tlsInfoList[this.monitor.id].certInfo
) {
return this.$root.tlsInfoList[this.monitor.id];
}
@ -397,11 +605,21 @@ export default {
},
pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
return (
this.$root.baseURL +
"/api/push/" +
this.monitor.pushToken +
"?status=up&msg=OK&ping="
);
},
screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
return (
getResBaseURL() +
this.monitor.screenshot +
"?time=" +
this.cacheTime
);
},
descriptionHTML() {
@ -410,7 +628,7 @@ export default {
} else {
return "";
}
}
},
},
watch: {
@ -434,7 +652,10 @@ export default {
mounted() {
this.getImportantHeartbeatListLength();
this.$root.emitter.on("newImportantHeartbeat", this.onNewImportantHeartbeat);
this.$root.emitter.on(
"newImportantHeartbeat",
this.onNewImportantHeartbeat
);
if (this.monitor && this.monitor.type === "push") {
if (this.lastHeartBeat.status === -1) {
@ -445,7 +666,10 @@ export default {
},
beforeUnmount() {
this.$root.emitter.off("newImportantHeartbeat", this.onNewImportantHeartbeat);
this.$root.emitter.off(
"newImportantHeartbeat",
this.onNewImportantHeartbeat
);
},
methods: {
@ -472,7 +696,9 @@ export default {
* @returns {void}
*/
resumeMonitor() {
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
this.$root
.getSocket()
.emit("resumeMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
},
@ -482,7 +708,9 @@ export default {
* @returns {void}
*/
pauseMonitor() {
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
this.$root
.getSocket()
.emit("pauseMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
},
@ -569,7 +797,11 @@ export default {
translationPrefix = "Avg. ";
}
if (this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query") {
if (
this.monitor.type === "http" ||
this.monitor.type === "keyword" ||
this.monitor.type === "json-query"
) {
return this.$t(translationPrefix + "Response");
}
@ -599,7 +831,10 @@ export default {
return parsedUrl.toString();
} catch (e) {
// Handle SQL Server
return urlString.replaceAll(/Password=(.+);/ig, "Password=******;");
return urlString.replaceAll(
/Password=(.+);/gi,
"Password=******;"
);
}
},
@ -609,12 +844,18 @@ export default {
*/
getImportantHeartbeatListLength() {
if (this.monitor) {
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", this.monitor.id, (res) => {
this.$root
.getSocket()
.emit(
"monitorImportantHeartbeatListCount",
this.monitor.id,
(res) => {
if (res.ok) {
this.importantHeartBeatListLength = res.count;
this.getImportantHeartbeatListPaged();
}
});
}
);
}
},
@ -625,11 +866,19 @@ export default {
getImportantHeartbeatListPaged() {
if (this.monitor) {
const offset = (this.page - 1) * this.perPage;
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", this.monitor.id, offset, this.perPage, (res) => {
this.$root
.getSocket()
.emit(
"monitorImportantHeartbeatListPaged",
this.monitor.id,
offset,
this.perPage,
(res) => {
if (res.ok) {
this.displayedRecords = res.data;
}
});
}
);
}
},
@ -661,13 +910,26 @@ export default {
loadPushExample() {
this.pushMonitor.code = "";
this.$root.getSocket().emit("getPushExample", this.pushMonitor.currentExample, (res) => {
this.$root
.getSocket()
.emit(
"getPushExample",
this.pushMonitor.currentExample,
(res) => {
let code = res.code
.replace("60", this.monitor.interval)
.replace("https://example.com/api/push/key?status=up&msg=OK&ping=", this.pushURL);
.replace(
"https://example.com/api/push/key?status=up&msg=OK&ping=",
this.pushURL
);
this.pushMonitor.code = code;
});
}
);
},
secondsToHumanReadableFormat(seconds) {
return relativeTimeFormatter.secondsToHumanReadableFormat(seconds);
},
},
};
</script>

View file

@ -629,6 +629,9 @@
<div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval" @blur="finishUpdateInterval">
<div class="form-text">
{{ monitor.humanReadableInterval }}
</div>
</div>
<div class="my-3">
@ -1170,7 +1173,7 @@ import {
MIN_INTERVAL_SECOND,
sleep,
} from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
import { hostNameRegexPattern, relativeTimeFormatter } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
@ -1186,6 +1189,7 @@ const monitorDefaults = {
method: "GET",
ipFamily: null,
interval: 60,
humanReadableInterval: relativeTimeFormatter.secondsToHumanReadableFormat(60),
retryInterval: 60,
resendInterval: 0,
maxretries: 0,
@ -1568,6 +1572,8 @@ message HealthCheckResponse {
if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value;
}
// Converting monitor.interval to human readable format.
this.monitor.humanReadableInterval = relativeTimeFormatter.secondsToHumanReadableFormat(value);
},
"monitor.timeout"(value, oldValue) {

View file

@ -213,3 +213,78 @@ export function getToastErrorTimeout() {
return errorTimeout;
}
class RelativeTimeFormatter {
/**
* Default locale and options for Relative Time Formatter
*/
constructor() {
this.options = { numeric: "auto" };
this.instance = new Intl.RelativeTimeFormat(currentLocale(), this.options);
}
/**
* Method to update the instance locale and options
* @param {string} locale Localization identifier (e.g., "en", "ar-sy") to update the instance with.
* @returns {void} No return value.
*/
updateLocale(locale) {
this.instance = new Intl.RelativeTimeFormat(locale, this.options);
}
/**
* Method to convert seconds into Human readable format
* @param {number} seconds Receive value in seconds.
* @returns {string} String converted to Days Mins Seconds Format
*/
secondsToHumanReadableFormat(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
const secs = ((seconds % 86400) % 3600) % 60;
const parts = [];
/**
* Build the formatted string from parts
* 1. Get the relative time formatted parts from the instance.
* 2. Filter out the relevant parts literal (unit of time) or integer (value).
* 3. Map out the required values.
* @param {number} value Receives value in seconds.
* @param {string} unitOfTime Expected unit of time after conversion.
* @returns {void}
*/
const toFormattedPart = (value, unitOfTime) => {
const partsArray = this.instance.formatToParts(value, unitOfTime);
const filteredParts = partsArray
.filter(
(part, index) =>
(part.type === "literal" || part.type === "integer") &&
index > 0
)
.map((part) => part.value);
const formattedString = filteredParts.join("").trim();
parts.push(formattedString);
};
if (days > 0) {
toFormattedPart(days, "days");
}
if (hours > 0) {
toFormattedPart(hours, "hour");
}
if (minutes > 0) {
toFormattedPart(minutes, "minute");
}
if (secs > 0) {
toFormattedPart(secs, "second");
}
if (parts.length > 0) {
return `${parts.join(" ")}`;
}
return this.instance.format(0, "second"); // Handle case for 0 seconds
}
}
export const relativeTimeFormatter = new RelativeTimeFormatter();