diff options
author | Daniel Baumann <daniel@debian.org> | 2024-11-26 09:28:28 +0100 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-11-26 12:25:58 +0100 |
commit | a1882b67c41fe9901a0cd8059b5cc78a5beadec0 (patch) | |
tree | 2a24507c67aa99a15416707b2f7e645142230ed8 /src/components/HeartbeatBar.vue | |
parent | Initial commit. (diff) | |
download | uptime-kuma-upstream.tar.xz uptime-kuma-upstream.zip |
Adding upstream version 2.0.0~beta.0+dfsg.upstream/2.0.0_beta.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'src/components/HeartbeatBar.vue')
-rw-r--r-- | src/components/HeartbeatBar.vue | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue new file mode 100644 index 0000000..429ca9f --- /dev/null +++ b/src/components/HeartbeatBar.vue @@ -0,0 +1,348 @@ +<template> + <div ref="wrap" class="wrap" :style="wrapStyle"> + <div class="hp-bar-big" :style="barStyle"> + <div + v-for="(beat, index) in shortBeatList" + :key="index" + 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'" + class="d-flex justify-content-between align-items-center word" :style="timeStyle" + > + <div>{{ timeSinceFirstBeat }}</div> + <div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div> + <div>{{ timeSinceLastBeat }}</div> + </div> + </div> +</template> + +<script> +import dayjs from "dayjs"; + +export default { + props: { + /** Size of the heartbeat bar */ + size: { + type: String, + default: "big", + }, + /** ID of the monitor */ + monitorId: { + type: Number, + required: true, + }, + /** Array of the monitors heartbeats */ + heartbeatList: { + type: Array, + default: null, + } + }, + data() { + return { + beatWidth: 10, + beatHeight: 30, + hoverScale: 1.5, + beatHoverAreaPadding: 4, + move: false, + maxBeat: -1, + }; + }, + computed: { + + /** + * If heartbeatList is null, get it from $root.heartbeatList + * @returns {object} Heartbeat list + */ + beatList() { + if (this.heartbeatList === null) { + return this.$root.heartbeatList[this.monitorId]; + } else { + return this.heartbeatList; + } + }, + + /** + * Calculates the amount of beats of padding needed to fill the length of shortBeatList. + * @returns {number} The amount of beats of padding needed to fill the length of shortBeatList. + */ + numPadding() { + if (!this.beatList) { + return 0; + } + let num = this.beatList.length - this.maxBeat; + + if (this.move) { + num = num - 1; + } + + if (num > 0) { + return 0; + } + + return -1 * num; + }, + + shortBeatList() { + if (!this.beatList) { + return []; + } + + let placeholders = []; + + let start = this.beatList.length - this.maxBeat; + + if (this.move) { + start = start - 1; + } + + if (start < 0) { + // Add empty placeholder + for (let i = start; i < 0; i++) { + placeholders.push(0); + } + start = 0; + } + + return placeholders.concat(this.beatList.slice(start)); + }, + + wrapStyle() { + let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2); + let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2); + + return { + padding: `${topBottom}px ${leftRight}px`, + width: "100%", + }; + }, + + barStyle() { + if (this.move && this.shortBeatList.length > this.maxBeat) { + let width = -(this.beatWidth + this.beatHoverAreaPadding * 2); + + return { + transition: "all ease-in-out 0.25s", + transform: `translateX(${width}px)`, + }; + + } + return { + transform: "translateX(0)", + }; + + }, + + beatHoverAreaStyle() { + return { + padding: this.beatHoverAreaPadding + "px", + "--hover-scale": this.hoverScale, + }; + }, + + beatStyle() { + return { + width: this.beatWidth + "px", + height: this.beatHeight + "px", + }; + }, + + /** + * Returns the style object for positioning the time element. + * @returns {object} The style object containing the CSS properties for positioning the time element. + */ + timeStyle() { + return { + "margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px", + }; + }, + + /** + * Calculates the time elapsed since the first valid beat. + * @returns {string} The time elapsed in minutes or hours. + */ + timeSinceFirstBeat() { + const firstValidBeat = this.shortBeatList.at(this.numPadding); + const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes"); + if (minutes > 60) { + return (minutes / 60).toFixed(0) + "h"; + } else { + return minutes + "m"; + } + }, + + /** + * Calculates the elapsed time since the last valid beat was registered. + * @returns {string} The elapsed time in a minutes, hours or "now". + */ + timeSinceLastBeat() { + const lastValidBeat = this.shortBeatList.at(-1); + const seconds = dayjs().diff(dayjs.utc(lastValidBeat?.time), "seconds"); + + let tolerance = 60 * 2; // default for when monitorList not available + if (this.$root.monitorList[this.monitorId] != null) { + tolerance = this.$root.monitorList[this.monitorId].interval * 2; + } + + if (seconds < tolerance) { + return this.$t("now"); + } else if (seconds < 60 * 60) { + return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]); + } else { + return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]); + } + } + }, + watch: { + beatList: { + handler(val, oldVal) { + this.move = true; + + setTimeout(() => { + this.move = false; + }, 300); + }, + deep: true, + }, + }, + unmounted() { + window.removeEventListener("resize", this.resize); + }, + beforeMount() { + if (this.heartbeatList === null) { + if (!(this.monitorId in this.$root.heartbeatList)) { + this.$root.heartbeatList[this.monitorId] = []; + } + } + }, + + mounted() { + if (this.size !== "big") { + this.beatWidth = 5; + this.beatHeight = 16; + 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 actualHoverAreaPadding = this.beatHoverAreaPadding * window.devicePixelRatio; + + if (!Number.isInteger(actualWidth)) { + this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio; + } + + if (!Number.isInteger(actualHoverAreaPadding)) { + this.beatHoverAreaPadding = Math.round(actualHoverAreaPadding) / window.devicePixelRatio; + } + + window.addEventListener("resize", this.resize); + this.resize(); + }, + methods: { + /** + * Resize the heartbeat bar + * @returns {void} + */ + resize() { + if (this.$refs.wrap) { + this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2)); + } + }, + + /** + * Get the title of the beat. + * Used as the hover tooltip on the heartbeat bar. + * @param {object} beat Beat to get title from + * @returns {string} Beat title + */ + getBeatTitle(beat) { + return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ""); + }, + + }, +}; +</script> + +<style lang="scss" scoped> +@import "../assets/vars.scss"; + +.wrap { + overflow: hidden; + width: 100%; + white-space: nowrap; +} + +.hp-bar-big { + .beat-hover-area { + 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; + 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; + } + } + } +} + +.dark { + .hp-bar-big .beat.empty { + background-color: #848484; + } +} + +.word { + color: $secondary-text; + font-size: 12px; +} + +.connecting-line { + flex-grow: 1; + height: 1px; + background-color: #ededed; + margin-left: 10px; + margin-right: 10px; + margin-top: 2px; + + .dark & { + background-color: #333; + } +} +</style> |