mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 23:04:04 +00:00
Compare commits
3 commits
4dde5a9234
...
13dea826e2
Author | SHA1 | Date | |
---|---|---|---|
|
13dea826e2 | ||
|
a7e9bdd43e | ||
|
a6610340a5 |
8 changed files with 123 additions and 47 deletions
12
db/knex_migrations/2024-10-16-0000-timezones.js
Normal file
12
db/knex_migrations/2024-10-16-0000-timezones.js
Normal 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");
|
||||||
|
});
|
||||||
|
};
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,18 @@
|
||||||
<div
|
<div
|
||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
class="beat-hover-area"
|
||||||
|
:class="{ 'empty': (beat === 0) }"
|
||||||
|
:style="beatHoverAreaStyle"
|
||||||
|
:title="getBeatTitle(beat)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
:title="getBeatTitle(beat)"
|
|
||||||
/>
|
/>
|
||||||
</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'"
|
||||||
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
||||||
|
@ -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,11 +284,25 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hp-bar-big {
|
.hp-bar-big {
|
||||||
.beat {
|
.beat-hover-area {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
&:not(.empty):hover {
|
||||||
|
transition: all ease-in-out 0.15s;
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(var(--hover-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat {
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
border-radius: $border-radius;
|
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 {
|
&.empty {
|
||||||
background-color: aliceblue;
|
background-color: aliceblue;
|
||||||
}
|
}
|
||||||
|
@ -293,11 +318,6 @@ export default {
|
||||||
&.maintenance {
|
&.maintenance {
|
||||||
background-color: $maintenance;
|
background-color: $maintenance;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.empty):hover {
|
|
||||||
transition: all ease-in-out 0.15s;
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: scale(var(--hover-scale));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue