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,
snmpVersion: this.snmpVersion,
conditions: JSON.parse(this.conditions),
timezone: this.timezone,
};
if (includeSensitiveData) {

View file

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

View file

@ -4,11 +4,17 @@
<div
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
class="beat-hover-area"
:class="{ 'empty': (beat === 0) }"
:style="beatHoverAreaStyle"
: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
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
@ -47,7 +53,7 @@ export default {
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
beatMargin: 4,
beatHoverAreaPadding: 4,
move: false,
maxBeat: -1,
};
@ -123,7 +129,7 @@ export default {
barStyle() {
if (this.move && this.shortBeatList.length > this.maxBeat) {
let width = -(this.beatWidth + this.beatMargin * 2);
let width = -(this.beatWidth + this.beatHoverAreaPadding * 2);
return {
transition: "all ease-in-out 0.25s",
@ -137,12 +143,17 @@ export default {
},
beatHoverAreaStyle() {
return {
padding: this.beatHoverAreaPadding + "px",
"--hover-scale": this.hoverScale,
};
},
beatStyle() {
return {
width: this.beatWidth + "px",
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
};
},
@ -152,7 +163,7 @@ export default {
*/
timeStyle() {
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") {
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
this.beatHoverAreaPadding = 2;
}
// Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
const actualHoverAreaPadding = this.beatHoverAreaPadding * window.devicePixelRatio;
if (!Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (!Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
if (!Number.isInteger(actualHoverAreaPadding)) {
this.beatHoverAreaPadding = Math.round(actualHoverAreaPadding) / window.devicePixelRatio;
}
window.addEventListener("resize", this.resize);
@ -245,7 +256,7 @@ export default {
*/
resize() {
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 {
.beat {
.beat-hover-area {
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 {
transition: all ease-in-out 0.15s;
opacity: 0.8;
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,
required: true,
},
/** Monitor Timezone */
monitorTimezone: {
type: String,
required: true,
},
},
data() {
return {
@ -251,7 +256,7 @@ export default {
},
// push datapoint to chartData
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
avgPingData.push({
@ -306,7 +311,7 @@ export default {
let heartbeatList = (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || [];
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");
// Insert empty datapoint to separate big gaps
@ -407,7 +412,7 @@ export default {
continue;
}
const beatTime = this.$root.unixToDayjs(datapoint.timestamp);
const beatTime = this.$root.unixToDayjs(datapoint.timestamp, this.monitorTimezone);
// Insert empty datapoint to separate big gaps
if (lastHeartbeatTime && monitorInterval) {
@ -427,7 +432,7 @@ export default {
const gapX = [
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) {

View file

@ -44,28 +44,31 @@ export default {
/**
* Converts a Unix timestamp to a formatted date and time string.
* @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.
*/
unixToDateTime(value) {
return dayjs.unix(value).tz(this.timezone).format("YYYY-MM-DD HH:mm:ss");
unixToDateTime(value, timezone) {
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.
* @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.
*/
unixToDayjs(value) {
return dayjs.unix(value).tz(this.timezone);
unixToDayjs(value, timezone) {
return dayjs.unix(value).tz(timezone === "auto" ? this.timezone : timezone);
},
/**
* Converts the given value to a dayjs object.
* @param {string} value - the value to be converted
* @returns {dayjs.Dayjs} a dayjs object in the timezone of this instance
* @param {string} value - The value to be converted.
* @param {string} timezone - The timezone to use for the conversion.
* @returns {dayjs.Dayjs} The dayjs object in the timezone of this instance.
*/
toDayjs(value) {
return dayjs.utc(value).tz(this.timezone);
toDayjs(value, 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 class="row">
<div class="col">
<PingChart :monitor-id="monitor.id" />
<PingChart :monitor-id="monitor.id" :monitor-timezone="monitor.timezone" />
</div>
</div>
</div>

View file

@ -8,6 +8,7 @@
<div class="col-md-6">
<h2 class="mb-2">{{ $t("General") }}</h2>
<!-- Monitor Type -->
<div class="my-3">
<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">
@ -563,6 +564,23 @@
<input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
</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>
<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 EditMonitorConditions from "../components/EditMonitorConditions.vue";
import { version } from "../../package.json";
import dayjs from "dayjs";
import { timezoneList } from "../util-frontend";
const userAgent = `'Uptime-Kuma/${version}'`;
const toast = useToast();
@ -1122,7 +1142,8 @@ const monitorDefaults = {
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
remote_browser: null,
conditions: []
conditions: [],
timezone: "auto"
};
export default {
@ -1165,6 +1186,7 @@ export default {
},
draftGroupName: null,
remoteBrowsersEnabled: false,
timezoneList: timezoneList(),
};
},
@ -1217,6 +1239,18 @@ export default {
return command.join(" ");
},
getUserTimezone() {
let timezone = localStorage.timezone;
if (timezone === null || timezone === "auto") {
timezone = this.guessTimezone;
}
return timezone;
},
guessTimezone() {
return dayjs.tz.guess();
},
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)