Compare commits

...

3 commits

Author SHA1 Message Date
Marcello Domenis
13dea826e2
Merge a6610340a5 into a7e9bdd43e 2024-10-20 13:30:14 +00:00
Ryo Hanafusa
a7e9bdd43e
fix: expand hover trigger area of beat (#5223)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2024-10-20 15:30:03 +02:00
Marcello Domenis
a6610340a5
feat: different monitor timezones from user timezone (#5201) 2024-10-16 19:53:17 -07:00
8 changed files with 123 additions and 47 deletions

View file

@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.text("timezone").notNullable().defaultTo("auto");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("timezone");
});
};

View file

@ -154,6 +154,7 @@ class Monitor extends BeanModel {
jsonPathOperator: this.jsonPathOperator, jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion, snmpVersion: this.snmpVersion,
conditions: JSON.parse(this.conditions), conditions: JSON.parse(this.conditions),
timezone: this.timezone,
}; };
if (includeSensitiveData) { if (includeSensitiveData) {

View file

@ -869,6 +869,7 @@ let needSetup = false;
bean.jsonPathOperator = monitor.jsonPathOperator; bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout; bean.timeout = monitor.timeout;
bean.conditions = JSON.stringify(monitor.conditions); bean.conditions = JSON.stringify(monitor.conditions);
bean.timezone = monitor.timezone;
bean.validate(); bean.validate();

View file

@ -4,11 +4,17 @@
<div <div
v-for="(beat, index) in shortBeatList" v-for="(beat, index) in shortBeatList"
:key="index" :key="index"
class="beat" class="beat-hover-area"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }" :class="{ 'empty': (beat === 0) }"
:style="beatStyle" :style="beatHoverAreaStyle"
:title="getBeatTitle(beat)" :title="getBeatTitle(beat)"
/> >
<div
class="beat"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
/>
</div>
</div> </div>
<div <div
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'" v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
@ -47,7 +53,7 @@ export default {
beatWidth: 10, beatWidth: 10,
beatHeight: 30, beatHeight: 30,
hoverScale: 1.5, hoverScale: 1.5,
beatMargin: 4, beatHoverAreaPadding: 4,
move: false, move: false,
maxBeat: -1, maxBeat: -1,
}; };
@ -123,7 +129,7 @@ export default {
barStyle() { barStyle() {
if (this.move && this.shortBeatList.length > this.maxBeat) { if (this.move && this.shortBeatList.length > this.maxBeat) {
let width = -(this.beatWidth + this.beatMargin * 2); let width = -(this.beatWidth + this.beatHoverAreaPadding * 2);
return { return {
transition: "all ease-in-out 0.25s", transition: "all ease-in-out 0.25s",
@ -137,12 +143,17 @@ export default {
}, },
beatHoverAreaStyle() {
return {
padding: this.beatHoverAreaPadding + "px",
"--hover-scale": this.hoverScale,
};
},
beatStyle() { beatStyle() {
return { return {
width: this.beatWidth + "px", width: this.beatWidth + "px",
height: this.beatHeight + "px", height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
}; };
}, },
@ -152,7 +163,7 @@ export default {
*/ */
timeStyle() { timeStyle() {
return { return {
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px", "margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px",
}; };
}, },
@ -219,20 +230,20 @@ export default {
if (this.size !== "big") { if (this.size !== "big") {
this.beatWidth = 5; this.beatWidth = 5;
this.beatHeight = 16; this.beatHeight = 16;
this.beatMargin = 2; this.beatHoverAreaPadding = 2;
} }
// Suddenly, have an idea how to handle it universally. // Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!! // If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio; const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio; const actualHoverAreaPadding = this.beatHoverAreaPadding * window.devicePixelRatio;
if (!Number.isInteger(actualWidth)) { if (!Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio; this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
} }
if (!Number.isInteger(actualMargin)) { if (!Number.isInteger(actualHoverAreaPadding)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio; this.beatHoverAreaPadding = Math.round(actualHoverAreaPadding) / window.devicePixelRatio;
} }
window.addEventListener("resize", this.resize); window.addEventListener("resize", this.resize);
@ -245,7 +256,7 @@ export default {
*/ */
resize() { resize() {
if (this.$refs.wrap) { if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2)); this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
} }
}, },
@ -273,32 +284,41 @@ export default {
} }
.hp-bar-big { .hp-bar-big {
.beat { .beat-hover-area {
display: inline-block; display: inline-block;
background-color: $primary;
border-radius: $border-radius;
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
&:not(.empty):hover { &:not(.empty):hover {
transition: all ease-in-out 0.15s; transition: all ease-in-out 0.15s;
opacity: 0.8; opacity: 0.8;
transform: scale(var(--hover-scale)); transform: scale(var(--hover-scale));
} }
.beat {
background-color: $primary;
border-radius: $border-radius;
/*
pointer-events needs to be changed because
tooltip momentarily disappears when crossing between .beat-hover-area and .beat
*/
pointer-events: none;
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
}
} }
} }

View file

@ -40,6 +40,11 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
/** Monitor Timezone */
monitorTimezone: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
@ -251,7 +256,7 @@ export default {
}, },
// push datapoint to chartData // push datapoint to chartData
pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData) { pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData) {
const x = this.$root.unixToDateTime(datapoint.timestamp); const x = this.$root.unixToDateTime(datapoint.timestamp, this.monitorTimezone);
// Show ping values if it was up in this period // Show ping values if it was up in this period
avgPingData.push({ avgPingData.push({
@ -306,7 +311,7 @@ export default {
let heartbeatList = (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || []; let heartbeatList = (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || [];
for (const beat of heartbeatList) { for (const beat of heartbeatList) {
const beatTime = this.$root.toDayjs(beat.time); const beatTime = this.$root.toDayjs(beat.time, this.monitorTimezone);
const x = beatTime.format("YYYY-MM-DD HH:mm:ss"); const x = beatTime.format("YYYY-MM-DD HH:mm:ss");
// Insert empty datapoint to separate big gaps // Insert empty datapoint to separate big gaps
@ -407,7 +412,7 @@ export default {
continue; continue;
} }
const beatTime = this.$root.unixToDayjs(datapoint.timestamp); const beatTime = this.$root.unixToDayjs(datapoint.timestamp, this.monitorTimezone);
// Insert empty datapoint to separate big gaps // Insert empty datapoint to separate big gaps
if (lastHeartbeatTime && monitorInterval) { if (lastHeartbeatTime && monitorInterval) {
@ -427,7 +432,7 @@ export default {
const gapX = [ const gapX = [
lastHeartbeatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"), lastHeartbeatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
this.$root.unixToDateTime(datapoint.timestamp + 60), this.$root.unixToDateTime(datapoint.timestamp + 60, this.monitorTimezone),
]; ];
for (const x of gapX) { for (const x of gapX) {

View file

@ -44,28 +44,31 @@ export default {
/** /**
* Converts a Unix timestamp to a formatted date and time string. * Converts a Unix timestamp to a formatted date and time string.
* @param {number} value - The Unix timestamp to convert. * @param {number} value - The Unix timestamp to convert.
* @param {string} timezone - The timezone to use for the conversion.
* @returns {string} The formatted date and time string. * @returns {string} The formatted date and time string.
*/ */
unixToDateTime(value) { unixToDateTime(value, timezone) {
return dayjs.unix(value).tz(this.timezone).format("YYYY-MM-DD HH:mm:ss"); return dayjs.unix(value).tz(timezone === "auto" ? this.timezone : timezone).format("YYYY-MM-DD HH:mm:ss");
}, },
/** /**
* Converts a Unix timestamp to a dayjs object. * Converts a Unix timestamp to a dayjs object.
* @param {number} value - The Unix timestamp to convert. * @param {number} value - The Unix timestamp to convert.
* @param {string} timezone - The timezone to use for the conversion.
* @returns {dayjs.Dayjs} The dayjs object representing the given timestamp. * @returns {dayjs.Dayjs} The dayjs object representing the given timestamp.
*/ */
unixToDayjs(value) { unixToDayjs(value, timezone) {
return dayjs.unix(value).tz(this.timezone); return dayjs.unix(value).tz(timezone === "auto" ? this.timezone : timezone);
}, },
/** /**
* Converts the given value to a dayjs object. * Converts the given value to a dayjs object.
* @param {string} value - the value to be converted * @param {string} value - The value to be converted.
* @returns {dayjs.Dayjs} a dayjs object in the timezone of this instance * @param {string} timezone - The timezone to use for the conversion.
* @returns {dayjs.Dayjs} The dayjs object in the timezone of this instance.
*/ */
toDayjs(value) { toDayjs(value, timezone) {
return dayjs.utc(value).tz(this.timezone); return dayjs.utc(value).tz(timezone === "auto" ? this.timezone : timezone);
}, },
/** /**

View file

@ -184,7 +184,7 @@
<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" :monitor-timezone="monitor.timezone" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -8,6 +8,7 @@
<div class="col-md-6"> <div class="col-md-6">
<h2 class="mb-2">{{ $t("General") }}</h2> <h2 class="mb-2">{{ $t("General") }}</h2>
<!-- Monitor Type -->
<div class="my-3"> <div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label> <label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select" data-testid="monitor-type-select"> <select id="type" v-model="monitor.type" class="form-select" data-testid="monitor-type-select">
@ -563,6 +564,23 @@
<input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1"> <input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
</div> </div>
<!-- Monitor Timezone -->
<div class="my-3">
<label for="timezone" class="form-label">{{ $t("Monitor Timezone") }}</label>
<select id="timezone" v-model="monitor.timezone" class="form-select">
<option value="auto">
{{ $t("Default") }}: {{ getUserTimezone }}
</option>
<option
v-for="(timezone, index) in timezoneList"
:key="index"
:value="timezone.value"
>
{{ timezone.name }}
</option>
</select>
</div>
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
@ -1077,6 +1095,8 @@ import { hostNameRegexPattern } 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";
import { version } from "../../package.json"; import { version } from "../../package.json";
import dayjs from "dayjs";
import { timezoneList } from "../util-frontend";
const userAgent = `'Uptime-Kuma/${version}'`; const userAgent = `'Uptime-Kuma/${version}'`;
const toast = useToast(); const toast = useToast();
@ -1122,7 +1142,8 @@ const monitorDefaults = {
kafkaProducerAllowAutoTopicCreation: false, kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true, gamedigGivenPortOnly: true,
remote_browser: null, remote_browser: null,
conditions: [] conditions: [],
timezone: "auto"
}; };
export default { export default {
@ -1165,6 +1186,7 @@ export default {
}, },
draftGroupName: null, draftGroupName: null,
remoteBrowsersEnabled: false, remoteBrowsersEnabled: false,
timezoneList: timezoneList(),
}; };
}, },
@ -1217,6 +1239,18 @@ export default {
return command.join(" "); return command.join(" ");
}, },
getUserTimezone() {
let timezone = localStorage.timezone;
if (timezone === null || timezone === "auto") {
timezone = this.guessTimezone;
}
return timezone;
},
guessTimezone() {
return dayjs.tz.guess();
},
ipRegex() { ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300) // Allow to test with simple dns server with port (127.0.0.1:5300)