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); 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.import(monitor);
bean.user_id = socket.userID; bean.user_id = socket.userID;

View file

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

View file

@ -1,7 +1,9 @@
<template> <template>
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div v-if="monitor"> <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> <h1>
{{ monitor.name }} {{ monitor.name }}
<div class="monitor-id"> <div class="monitor-id">
@ -13,61 +15,124 @@
<p v-if="monitor.description" v-html="descriptionHTML"></p> <p v-if="monitor.description" v-html="descriptionHTML"></p>
<div class="d-flex"> <div class="d-flex">
<div class="tags"> <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>
</div> </div>
<p class="url"> <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 === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'keyword'">
<br> <br />
<span>{{ $t("Keyword") }}: </span> <span>{{ $t("Keyword") }}: </span>
<span class="keyword">{{ monitor.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>
<span v-if="monitor.type === 'json-query'"> <span v-if="monitor.type === 'json-query'">
<br> <br />
<span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span> <span>{{ $t("Json Query") }}:</span>
<br> <span class="keyword">{{ monitor.jsonPath }}</span>
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span> <br />
<span>{{ $t("Expected Value") }}:</span>
<span class="keyword">{{ monitor.expectedValue }}</span>
</span> </span>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }} <span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<br> <br />
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span> <span>{{ $t("Last Result") }}:</span>
<span class="keyword">{{ monitor.dns_last_result }}</span>
</span> </span>
<span v-if="monitor.type === 'docker'">Docker container: {{ monitor.docker_container }}</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) }} <span v-if="monitor.type === 'grpc-keyword'">gRPC - {{ filterPassword(monitor.grpcUrl) }}
<br> <br />
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span> <span>{{ $t("Keyword") }}:</span>
<span class="keyword">{{ monitor.keyword }}</span>
</span> </span>
<span v-if="monitor.type === 'mongodb'">{{ filterPassword(monitor.databaseConnectionString) }}</span> <span v-if="monitor.type === 'mongodb'">{{
<span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{ monitor.mqttTopic }}</span> filterPassword(monitor.databaseConnectionString)
<span v-if="monitor.type === 'mysql'">{{ filterPassword(monitor.databaseConnectionString) }}</span> }}</span>
<span v-if="monitor.type === 'postgres'">{{ filterPassword(monitor.databaseConnectionString) }}</span> <span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{
<span v-if="monitor.type === 'push'">Push: <a :href="pushURL" target="_blank" rel="noopener noreferrer">{{ pushURL }}</a></span> 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 === 'radius'">Radius: {{ filterPassword(monitor.hostname) }}</span>
<span v-if="monitor.type === 'redis'">{{ filterPassword(monitor.databaseConnectionString) }}</span> <span v-if="monitor.type === 'redis'">{{
<span v-if="monitor.type === 'sqlserver'">SQL Server: {{ filterPassword(monitor.databaseConnectionString) }}</span> filterPassword(monitor.databaseConnectionString)
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{ monitor.port }}</span> }}</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> </p>
<div class="functions"> <div class="functions">
<div class="btn-group" role="group"> <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") }} <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button> </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") }} <font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button> </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") }} <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
</router-link> </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") }} <font-awesome-icon icon="clone" /> {{ $t("Clone") }}
</router-link> </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") }} <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button> </button>
</div> </div>
@ -77,29 +142,53 @@
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" /> <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>
<div class="col-md-4 text-center"> <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> </div>
</div> </div>
<!-- Push Examples --> <!-- Push Examples -->
<div v-if="monitor.type === 'push'" class="shadow-box big-padding"> <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> <transition name="slide-fade" appear>
<div v-if="pushMonitor.showPushExamples" class="mt-3"> <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')"> <optgroup :label="$t('programmingLanguages')">
<option value="csharp">C#</option> <option value="csharp">C#</option>
<option value="go">Go</option> <option value="go">Go</option>
<option value="java">Java</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="php">PHP</option>
<option value="python">Python</option> <option value="python">Python</option>
<option value="typescript-fetch">TypeScript (fetch)</option> <option value="typescript-fetch">
TypeScript (fetch)
</option>
</optgroup> </optgroup>
<optgroup :label="$t('pushOthers')"> <optgroup :label="$t('pushOthers')">
<option value="bash-curl">Bash (curl)</option> <option value="bash-curl">Bash (curl)</option>
@ -108,7 +197,13 @@
</optgroup> </optgroup>
</select> </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> </div>
</transition> </transition>
</div> </div>
@ -116,55 +211,98 @@
<!-- Stats --> <!-- Stats -->
<div class="shadow-box big-padding text-center stats"> <div class="shadow-box big-padding text-center stats">
<div class="row"> <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> <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"> <span class="col-4 col-sm-12 num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox"> <a
href="#"
@click.prevent="
showPingChartBox = !showPingChartBox
"
>
<CountUp :value="ping" /> <CountUp :value="ping" />
</a> </a>
</span> </span>
</div> </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> <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"> <span class="col-4 col-sm-12 num">
<CountUp :value="avgPing" /> <CountUp :value="avgPing" />
</span> </span>
</div> </div>
<!-- Uptime (24-hour) --> <!-- 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> <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"> <span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="24" /> <Uptime :monitor="monitor" type="24" />
</span> </span>
</div> </div>
<!-- Uptime (30-day) --> <!-- 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> <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"> <span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="720" /> <Uptime :monitor="monitor" type="720" />
</span> </span>
</div> </div>
<!-- Uptime (1-year) --> <!-- 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> <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"> <span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="1y" /> <Uptime :monitor="monitor" type="1y" />
</span> </span>
</div> </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> <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"> <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> </span>
</div> </div>
</div> </div>
@ -172,17 +310,26 @@
<!-- Cert Info Box --> <!-- Cert Info Box -->
<transition name="slide-fade" appear> <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="row">
<div class="col"> <div class="col">
<certificate-info :certInfo="tlsInfo.certInfo" :valid="tlsInfo.valid" /> <certificate-info
:certInfo="tlsInfo.certInfo"
:valid="tlsInfo.valid"
/>
</div> </div>
</div> </div>
</div> </div>
</transition> </transition>
<!-- Ping Chart --> <!-- 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="row">
<div class="col"> <div class="col">
<PingChart :monitor-id="monitor.id" /> <PingChart :monitor-id="monitor.id" />
@ -194,25 +341,46 @@
<div v-if="monitor.type === 'real-browser'" class="shadow-box"> <div v-if="monitor.type === 'real-browser'" class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6 zoom-cursor"> <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> </div>
<ScreenshotDialog ref="screenshotDialog" :imageURL="screenshotURL" /> <ScreenshotDialog
ref="screenshotDialog"
:imageURL="screenshotURL"
/>
</div> </div>
</div> </div>
<div class="shadow-box table-shadow-box"> <div class="shadow-box table-shadow-box">
<div class="dropdown dropdown-clear-data"> <div class="dropdown dropdown-clear-data">
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown"> <button
<font-awesome-icon icon="trash" /> {{ $t("Clear Data") }} class="btn btn-sm btn-outline-danger dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
>
<font-awesome-icon icon="trash" />
{{ $t("Clear Data") }}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<button type="button" class="dropdown-item" @click="clearEventsDialog"> <button
type="button"
class="dropdown-item"
@click="clearEventsDialog"
>
{{ $t("Events") }} {{ $t("Events") }}
</button> </button>
</li> </li>
<li> <li>
<button type="button" class="dropdown-item" @click="clearHeartbeatsDialog"> <button
type="button"
class="dropdown-item"
@click="clearHeartbeatsDialog"
>
{{ $t("Heartbeats") }} {{ $t("Heartbeats") }}
</button> </button>
</li> </li>
@ -227,9 +395,15 @@
</tr> </tr>
</thead> </thead>
<tbody> <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><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> <td class="border-0">{{ beat.msg }}</td>
</tr> </tr>
@ -251,19 +425,42 @@
</div> </div>
</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") }} {{ $t("pauseMonitorMsg") }}
</Confirm> </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") }} {{ $t("deleteMonitorMsg") }}
</Confirm> </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") }} {{ $t("clearEventsMsg") }}
</Confirm> </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") }} {{ $t("clearHeartbeatsMsg") }}
</Confirm> </Confirm>
</div> </div>
@ -281,14 +478,16 @@ import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue"; import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3"; 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 Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue"; import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url"; import { URL } from "whatwg-url";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { marked } from "marked"; import { marked } from "marked";
import { getResBaseURL } from "../util-frontend"; import { getResBaseURL, relativeTimeFormatter } from "../util-frontend";
import { highlight, languages } from "prismjs/components/prism-core"; import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-clike"; import "prismjs/components/prism-clike";
import "prismjs/components/prism-javascript"; import "prismjs/components/prism-javascript";
@ -310,7 +509,7 @@ export default {
Tag, Tag,
CertificateInfo, CertificateInfo,
PrismEditor, PrismEditor,
ScreenshotDialog ScreenshotDialog,
}, },
data() { data() {
return { return {
@ -344,7 +543,10 @@ export default {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties // eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.cacheTime = Date.now(); 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]; return this.$root.lastHeartbeatList[this.monitor.id];
} }
@ -362,7 +564,10 @@ export default {
}, },
avgPing() { 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]; return this.$root.avgPingList[this.monitor.id];
} }
@ -374,14 +579,17 @@ export default {
return this.$root.statusList[this.monitor.id]; return this.$root.statusList[this.monitor.id];
} }
return { }; return {};
}, },
tlsInfo() { tlsInfo() {
// Add: this.$root.tlsInfoList[this.monitor.id].certInfo // Add: this.$root.tlsInfoList[this.monitor.id].certInfo
// Fix: TypeError: Cannot read properties of undefined (reading 'validTo') // 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. // 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]; return this.$root.tlsInfoList[this.monitor.id];
} }
@ -397,11 +605,21 @@ export default {
}, },
pushURL() { 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() { screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime; return (
getResBaseURL() +
this.monitor.screenshot +
"?time=" +
this.cacheTime
);
}, },
descriptionHTML() { descriptionHTML() {
@ -410,7 +628,7 @@ export default {
} else { } else {
return ""; return "";
} }
} },
}, },
watch: { watch: {
@ -434,7 +652,10 @@ export default {
mounted() { mounted() {
this.getImportantHeartbeatListLength(); 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.monitor && this.monitor.type === "push") {
if (this.lastHeartBeat.status === -1) { if (this.lastHeartBeat.status === -1) {
@ -445,7 +666,10 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
this.$root.emitter.off("newImportantHeartbeat", this.onNewImportantHeartbeat); this.$root.emitter.off(
"newImportantHeartbeat",
this.onNewImportantHeartbeat
);
}, },
methods: { methods: {
@ -472,9 +696,11 @@ export default {
* @returns {void} * @returns {void}
*/ */
resumeMonitor() { resumeMonitor() {
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => { this.$root
this.$root.toastRes(res); .getSocket()
}); .emit("resumeMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
}, },
/** /**
@ -482,9 +708,11 @@ export default {
* @returns {void} * @returns {void}
*/ */
pauseMonitor() { pauseMonitor() {
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => { this.$root
this.$root.toastRes(res); .getSocket()
}); .emit("pauseMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
}, },
/** /**
@ -552,7 +780,7 @@ export default {
*/ */
clearHeartbeats() { clearHeartbeats() {
this.$root.clearHeartbeats(this.monitor.id, (res) => { this.$root.clearHeartbeats(this.monitor.id, (res) => {
if (! res.ok) { if (!res.ok) {
toast.error(res.msg); toast.error(res.msg);
} }
}); });
@ -569,7 +797,11 @@ export default {
translationPrefix = "Avg. "; 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"); return this.$t(translationPrefix + "Response");
} }
@ -599,7 +831,10 @@ export default {
return parsedUrl.toString(); return parsedUrl.toString();
} catch (e) { } catch (e) {
// Handle SQL Server // Handle SQL Server
return urlString.replaceAll(/Password=(.+);/ig, "Password=******;"); return urlString.replaceAll(
/Password=(.+);/gi,
"Password=******;"
);
} }
}, },
@ -609,12 +844,18 @@ export default {
*/ */
getImportantHeartbeatListLength() { getImportantHeartbeatListLength() {
if (this.monitor) { if (this.monitor) {
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", this.monitor.id, (res) => { this.$root
if (res.ok) { .getSocket()
this.importantHeartBeatListLength = res.count; .emit(
this.getImportantHeartbeatListPaged(); "monitorImportantHeartbeatListCount",
} this.monitor.id,
}); (res) => {
if (res.ok) {
this.importantHeartBeatListLength = res.count;
this.getImportantHeartbeatListPaged();
}
}
);
} }
}, },
@ -625,11 +866,19 @@ export default {
getImportantHeartbeatListPaged() { getImportantHeartbeatListPaged() {
if (this.monitor) { if (this.monitor) {
const offset = (this.page - 1) * this.perPage; const offset = (this.page - 1) * this.perPage;
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", this.monitor.id, offset, this.perPage, (res) => { this.$root
if (res.ok) { .getSocket()
this.displayedRecords = res.data; .emit(
} "monitorImportantHeartbeatListPaged",
}); this.monitor.id,
offset,
this.perPage,
(res) => {
if (res.ok) {
this.displayedRecords = res.data;
}
}
);
} }
}, },
@ -661,13 +910,26 @@ export default {
loadPushExample() { loadPushExample() {
this.pushMonitor.code = ""; this.pushMonitor.code = "";
this.$root.getSocket().emit("getPushExample", this.pushMonitor.currentExample, (res) => { this.$root
let code = res.code .getSocket()
.replace("60", this.monitor.interval) .emit(
.replace("https://example.com/api/push/key?status=up&msg=OK&ping=", this.pushURL); "getPushExample",
this.pushMonitor.code = code; 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
);
this.pushMonitor.code = code;
}
);
},
secondsToHumanReadableFormat(seconds) {
return relativeTimeFormatter.secondsToHumanReadableFormat(seconds);
},
}, },
}; };
</script> </script>

View file

@ -629,6 +629,9 @@
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <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"> <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>
<div class="my-3"> <div class="my-3">
@ -1170,7 +1173,7 @@ import {
MIN_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
sleep, sleep,
} from "../util.ts"; } from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend"; import { hostNameRegexPattern, relativeTimeFormatter } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue"; import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue"; import EditMonitorConditions from "../components/EditMonitorConditions.vue";
@ -1186,6 +1189,7 @@ const monitorDefaults = {
method: "GET", method: "GET",
ipFamily: null, ipFamily: null,
interval: 60, interval: 60,
humanReadableInterval: relativeTimeFormatter.secondsToHumanReadableFormat(60),
retryInterval: 60, retryInterval: 60,
resendInterval: 0, resendInterval: 0,
maxretries: 0, maxretries: 0,
@ -1568,6 +1572,8 @@ message HealthCheckResponse {
if (this.monitor.retryInterval === oldValue) { if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value; this.monitor.retryInterval = value;
} }
// Converting monitor.interval to human readable format.
this.monitor.humanReadableInterval = relativeTimeFormatter.secondsToHumanReadableFormat(value);
}, },
"monitor.timeout"(value, oldValue) { "monitor.timeout"(value, oldValue) {

View file

@ -213,3 +213,78 @@ export function getToastErrorTimeout() {
return errorTimeout; 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();