<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" :class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }" :style="beatStyle" :title="getBeatTitle(beat)" /> </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, beatMargin: 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.beatMargin * 2); return { transition: "all ease-in-out 0.25s", transform: `translateX(${width}px)`, }; } return { transform: "translateX(0)", }; }, beatStyle() { return { width: this.beatWidth + "px", height: this.beatHeight + "px", margin: this.beatMargin + "px", "--hover-scale": this.hoverScale, }; }, /** * 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.beatMargin * 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.beatMargin = 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; if (!Number.isInteger(actualWidth)) { this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio; } if (!Number.isInteger(actualMargin)) { this.beatMargin = Math.round(actualMargin) / 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.beatMargin * 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 { 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)); } } } .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>