mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-31 19:39:20 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into weblate-uptime-kuma-uptime-kuma
# Conflicts: # src/lang/de-DE.json
This commit is contained in:
		
							
								
								
									
										6
									
								
								db/patch-add-parent-monitor.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								db/patch-add-parent-monitor.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| ALTER TABLE monitor | ||||
|     ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE; | ||||
|  | ||||
| COMMIT | ||||
| @@ -69,6 +69,7 @@ class Database { | ||||
|         "patch-api-key-table.sql": true, | ||||
|         "patch-monitor-tls.sql": true, | ||||
|         "patch-maintenance-cron.sql": true, | ||||
|         "patch-add-parent-monitor.sql": true, | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -74,13 +74,17 @@ class Monitor extends BeanModel { | ||||
|             id: this.id, | ||||
|             name: this.name, | ||||
|             description: this.description, | ||||
|             pathName: await this.getPathName(), | ||||
|             parent: this.parent, | ||||
|             childrenIDs: await Monitor.getAllChildrenIDs(this.id), | ||||
|             url: this.url, | ||||
|             method: this.method, | ||||
|             hostname: this.hostname, | ||||
|             port: this.port, | ||||
|             maxretries: this.maxretries, | ||||
|             weight: this.weight, | ||||
|             active: this.active, | ||||
|             active: await this.isActive(), | ||||
|             forceInactive: !await Monitor.isParentActive(this.id), | ||||
|             type: this.type, | ||||
|             interval: this.interval, | ||||
|             retryInterval: this.retryInterval, | ||||
| @@ -144,6 +148,16 @@ class Monitor extends BeanModel { | ||||
|         return data; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| 	 * Checks if the monitor is active based on itself and its parents | ||||
| 	 * @returns {Promise<Boolean>} | ||||
| 	 */ | ||||
|     async isActive() { | ||||
|         const parentActive = await Monitor.isParentActive(this.id); | ||||
|  | ||||
|         return this.active && parentActive; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get all tags applied to this monitor | ||||
|      * @returns {Promise<LooseObject<any>[]>} | ||||
| @@ -259,6 +273,36 @@ class Monitor extends BeanModel { | ||||
|                 if (await Monitor.isUnderMaintenance(this.id)) { | ||||
|                     bean.msg = "Monitor under maintenance"; | ||||
|                     bean.status = MAINTENANCE; | ||||
|                 } else if (this.type === "group") { | ||||
|                     const children = await Monitor.getChildren(this.id); | ||||
|  | ||||
|                     if (children.length > 0) { | ||||
|                         bean.status = UP; | ||||
|                         bean.msg = "All children up and running"; | ||||
|                         for (const child of children) { | ||||
|                             if (!child.active) { | ||||
|                                 // Ignore inactive childs | ||||
|                                 continue; | ||||
|                             } | ||||
|                             const lastBeat = await Monitor.getPreviousHeartbeat(child.id); | ||||
|  | ||||
|                             // Only change state if the monitor is in worse conditions then the ones before | ||||
|                             if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) { | ||||
|                                 bean.status = lastBeat.status; | ||||
|                             } else if (bean.status === PENDING && lastBeat.status === DOWN) { | ||||
|                                 bean.status = lastBeat.status; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         if (bean.status !== UP) { | ||||
|                             bean.msg = "Child inaccessible"; | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Set status pending if group is empty | ||||
|                         bean.status = PENDING; | ||||
|                         bean.msg = "Group empty"; | ||||
|                     } | ||||
|  | ||||
|                 } else if (this.type === "http" || this.type === "keyword") { | ||||
|                     // Do not do any queries/high loading things before the "bean.ping" | ||||
|                     let startTime = dayjs().valueOf(); | ||||
| @@ -1329,6 +1373,11 @@ class Monitor extends BeanModel { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const parent = await Monitor.getParent(monitorID); | ||||
|         if (parent != null) { | ||||
|             return await Monitor.isUnderMaintenance(parent.id); | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
| @@ -1341,6 +1390,94 @@ class Monitor extends BeanModel { | ||||
|             throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets Parent of the monitor | ||||
|      * @param {number} monitorID ID of monitor to get | ||||
|      * @returns {Promise<LooseObject<any>>} | ||||
|      */ | ||||
|     static async getParent(monitorID) { | ||||
|         return await R.getRow(` | ||||
|             SELECT parent.* FROM monitor parent | ||||
|     		LEFT JOIN monitor child | ||||
|     			ON child.parent = parent.id | ||||
|             WHERE child.id = ? | ||||
|         `, [ | ||||
|             monitorID, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets all Children of the monitor | ||||
|      * @param {number} monitorID ID of monitor to get | ||||
|      * @returns {Promise<LooseObject<any>>} | ||||
|      */ | ||||
|     static async getChildren(monitorID) { | ||||
|         return await R.getAll(` | ||||
|             SELECT * FROM monitor | ||||
|             WHERE parent = ? | ||||
|         `, [ | ||||
|             monitorID, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets Full Path-Name (Groups and Name) | ||||
|      * @returns {Promise<String>} | ||||
|      */ | ||||
|     async getPathName() { | ||||
|         let path = this.name; | ||||
|  | ||||
|         if (this.parent === null) { | ||||
|             return path; | ||||
|         } | ||||
|  | ||||
|         let parent = await Monitor.getParent(this.id); | ||||
|         while (parent !== null) { | ||||
|             path = `${parent.name} / ${path}`; | ||||
|             parent = await Monitor.getParent(parent.id); | ||||
|         } | ||||
|  | ||||
|         return path; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets recursive all child ids | ||||
| 	 * @param {number} monitorID ID of the monitor to get | ||||
|      * @returns {Promise<Array>} | ||||
|      */ | ||||
|     static async getAllChildrenIDs(monitorID) { | ||||
|         const childs = await Monitor.getChildren(monitorID); | ||||
|  | ||||
|         if (childs === null) { | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         let childrenIDs = []; | ||||
|  | ||||
|         for (const child of childs) { | ||||
|             childrenIDs.push(child.id); | ||||
|             childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id)); | ||||
|         } | ||||
|  | ||||
|         return childrenIDs; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| 	 * Checks recursive if parent (ancestors) are active | ||||
| 	 * @param {number} monitorID ID of the monitor to get | ||||
| 	 * @returns {Promise<Boolean>} | ||||
| 	 */ | ||||
|     static async isParentActive(monitorID) { | ||||
|         const parent = await Monitor.getParent(monitorID); | ||||
|  | ||||
|         if (parent === null) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         const parentActive = await Monitor.isParentActive(parent.id); | ||||
|         return parent.active && parentActive; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = Monitor; | ||||
|   | ||||
| @@ -684,8 +684,17 @@ let needSetup = false; | ||||
|                     throw new Error("Permission denied."); | ||||
|                 } | ||||
|  | ||||
|                 // Check if Parent is Decendant (would cause endless loop) | ||||
|                 if (monitor.parent !== null) { | ||||
|                     const childIDs = await Monitor.getAllChildrenIDs(monitor.id); | ||||
|                     if (childIDs.includes(monitor.parent)) { | ||||
|                         throw new Error("Invalid Monitor Group"); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 bean.name = monitor.name; | ||||
|                 bean.description = monitor.description; | ||||
|                 bean.parent = monitor.parent; | ||||
|                 bean.type = monitor.type; | ||||
|                 bean.url = monitor.url; | ||||
|                 bean.method = monitor.method; | ||||
| @@ -745,7 +754,7 @@ let needSetup = false; | ||||
|  | ||||
|                 await updateMonitorNotification(bean.id, monitor.notificationIDList); | ||||
|  | ||||
|                 if (bean.active) { | ||||
|                 if (bean.isActive()) { | ||||
|                     await restartMonitor(socket.userID, bean.id); | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -186,7 +186,7 @@ module.exports.maintenanceSocketHandler = (socket) => { | ||||
|  | ||||
|             log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`); | ||||
|  | ||||
|             let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [ | ||||
|             let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [ | ||||
|                 maintenanceID, | ||||
|             ]); | ||||
|  | ||||
|   | ||||
| @@ -19,43 +19,18 @@ | ||||
|                 {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> | ||||
|             </div> | ||||
|  | ||||
|             <router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" :title="item.description"> | ||||
|                 <div class="row"> | ||||
|                     <div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }"> | ||||
|                         <div class="info"> | ||||
|                             <Uptime :monitor="item" type="24" :pill="true" /> | ||||
|                             {{ item.name }} | ||||
|                         </div> | ||||
|                         <div class="tags"> | ||||
|                             <Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" /> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4"> | ||||
|                         <HeartbeatBar size="small" :monitor-id="item.id" /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <div v-if="$root.userHeartbeatBar == 'bottom'" class="row"> | ||||
|                     <div class="col-12 bottom-style"> | ||||
|                         <HeartbeatBar size="small" :monitor-id="item.id" /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </router-link> | ||||
|             <MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" /> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import HeartbeatBar from "../components/HeartbeatBar.vue"; | ||||
| import Tag from "../components/Tag.vue"; | ||||
| import Uptime from "../components/Uptime.vue"; | ||||
| import MonitorListItem from "../components/MonitorListItem.vue"; | ||||
| import { getMonitorRelativeURL } from "../util.ts"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Uptime, | ||||
|         HeartbeatBar, | ||||
|         Tag, | ||||
|         MonitorListItem, | ||||
|     }, | ||||
|     props: { | ||||
|         /** Should the scrollbar be shown */ | ||||
| @@ -91,6 +66,20 @@ export default { | ||||
|         sortedMonitorList() { | ||||
|             let result = Object.values(this.$root.monitorList); | ||||
|  | ||||
|             // Simple filter by search text | ||||
|             // finds monitor name, tag name or tag value | ||||
|             if (this.searchText !== "") { | ||||
|                 const loweredSearchText = this.searchText.toLowerCase(); | ||||
|                 result = result.filter(monitor => { | ||||
|                     return monitor.name.toLowerCase().includes(loweredSearchText) | ||||
|                     || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText) | ||||
|                     || tag.value?.toLowerCase().includes(loweredSearchText)); | ||||
|                 }); | ||||
|             } else { | ||||
|                 result = result.filter(monitor => monitor.parent === null); | ||||
|             } | ||||
|  | ||||
|             // Filter result by active state, weight and alphabetical | ||||
|             result.sort((m1, m2) => { | ||||
|  | ||||
|                 if (m1.active !== m2.active) { | ||||
| @@ -116,17 +105,6 @@ export default { | ||||
|                 return m1.name.localeCompare(m2.name); | ||||
|             }); | ||||
|  | ||||
|             // Simple filter by search text | ||||
|             // finds monitor name, tag name or tag value | ||||
|             if (this.searchText !== "") { | ||||
|                 const loweredSearchText = this.searchText.toLowerCase(); | ||||
|                 result = result.filter(monitor => { | ||||
|                     return monitor.name.toLowerCase().includes(loweredSearchText) | ||||
|                     || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText) | ||||
|                     || tag.value?.toLowerCase().includes(loweredSearchText)); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										204
									
								
								src/components/MonitorListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								src/components/MonitorListItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,204 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }"> | ||||
|             <div class="row"> | ||||
|                 <div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }"> | ||||
|                     <div class="info" :style="depthMargin"> | ||||
|                         <Uptime :monitor="monitor" type="24" :pill="true" /> | ||||
|                         <span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed"> | ||||
|                             <font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" /> | ||||
|                         </span> | ||||
|                         {{ monitorName }} | ||||
|                     </div> | ||||
|                     <div class="tags"> | ||||
|                         <Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4"> | ||||
|                     <HeartbeatBar size="small" :monitor-id="monitor.id" /> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div v-if="$root.userHeartbeatBar == 'bottom'" class="row"> | ||||
|                 <div class="col-12 bottom-style"> | ||||
|                     <HeartbeatBar size="small" :monitor-id="monitor.id" /> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </router-link> | ||||
|  | ||||
|         <transition name="slide-fade-up"> | ||||
|             <div v-if="!isCollapsed" class="childs"> | ||||
|                 <MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" /> | ||||
|             </div> | ||||
|         </transition> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import HeartbeatBar from "../components/HeartbeatBar.vue"; | ||||
| import Tag from "../components/Tag.vue"; | ||||
| import Uptime from "../components/Uptime.vue"; | ||||
| import { getMonitorRelativeURL } from "../util.ts"; | ||||
|  | ||||
| export default { | ||||
|     name: "MonitorListItem", | ||||
|     components: { | ||||
|         Uptime, | ||||
|         HeartbeatBar, | ||||
|         Tag, | ||||
|     }, | ||||
|     props: { | ||||
|         /** Monitor this represents */ | ||||
|         monitor: { | ||||
|             type: Object, | ||||
|             default: null, | ||||
|         }, | ||||
|         /** If the user is currently searching */ | ||||
|         isSearch: { | ||||
|             type: Boolean, | ||||
|             default: false, | ||||
|         }, | ||||
|         /** How many ancestors are above this monitor */ | ||||
|         depth: { | ||||
|             type: Number, | ||||
|             default: 0, | ||||
|         }, | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isCollapsed: true, | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         sortedChildMonitorList() { | ||||
|             let result = Object.values(this.$root.monitorList); | ||||
|  | ||||
|             result = result.filter(childMonitor => childMonitor.parent === this.monitor.id); | ||||
|  | ||||
|             result.sort((m1, m2) => { | ||||
|  | ||||
|                 if (m1.active !== m2.active) { | ||||
|                     if (m1.active === 0) { | ||||
|                         return 1; | ||||
|                     } | ||||
|  | ||||
|                     if (m2.active === 0) { | ||||
|                         return -1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (m1.weight !== m2.weight) { | ||||
|                     if (m1.weight > m2.weight) { | ||||
|                         return -1; | ||||
|                     } | ||||
|  | ||||
|                     if (m1.weight < m2.weight) { | ||||
|                         return 1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return m1.name.localeCompare(m2.name); | ||||
|             }); | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|         hasChildren() { | ||||
|             return this.sortedChildMonitorList.length > 0; | ||||
|         }, | ||||
|         depthMargin() { | ||||
|             return { | ||||
|                 marginLeft: `${31 * this.depth}px`, | ||||
|             }; | ||||
|         }, | ||||
|         monitorName() { | ||||
|             if (this.isSearch) { | ||||
|                 return this.monitor.pathName; | ||||
|             } else { | ||||
|                 return this.monitor.name; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     beforeMount() { | ||||
|  | ||||
|         // Always unfold if monitor is accessed directly | ||||
|         if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) { | ||||
|             this.isCollapsed = false; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Set collapsed value based on local storage | ||||
|         let storage = window.localStorage.getItem("monitorCollapsed"); | ||||
|         if (storage === null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let storageObject = JSON.parse(storage); | ||||
|         if (storageObject[`monitor_${this.monitor.id}`] == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.isCollapsed = storageObject[`monitor_${this.monitor.id}`]; | ||||
|     }, | ||||
|     methods: { | ||||
|         /** | ||||
|          * Changes the collapsed value of the current monitor and saves it to local storage | ||||
|          */ | ||||
|         changeCollapsed() { | ||||
|             this.isCollapsed = !this.isCollapsed; | ||||
|  | ||||
|             // Save collapsed value into local storage | ||||
|             let storage = window.localStorage.getItem("monitorCollapsed"); | ||||
|             let storageObject = {}; | ||||
|             if (storage !== null) { | ||||
|                 storageObject = JSON.parse(storage); | ||||
|             } | ||||
|             storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed; | ||||
|  | ||||
|             window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject)); | ||||
|         }, | ||||
|         /** | ||||
|          * Get URL of monitor | ||||
|          * @param {number} id ID of monitor | ||||
|          * @returns {string} Relative URL of monitor | ||||
|          */ | ||||
|         monitorURL(id) { | ||||
|             return getMonitorRelativeURL(id); | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .small-padding { | ||||
|     padding-left: 5px !important; | ||||
|     padding-right: 5px !important; | ||||
| } | ||||
|  | ||||
| .collapse-padding { | ||||
|     padding-left: 8px !important; | ||||
|     padding-right: 2px !important; | ||||
| } | ||||
|  | ||||
| // .monitor-item { | ||||
| //     width: 100%; | ||||
| // } | ||||
|  | ||||
| .tags { | ||||
|     margin-top: 4px; | ||||
|     padding-left: 67px; | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     gap: 0; | ||||
| } | ||||
|  | ||||
| .collapsed { | ||||
|     transform: rotate(-90deg); | ||||
| } | ||||
|  | ||||
| .animated { | ||||
|     transition: all 0.2s $easing-in; | ||||
| } | ||||
|  | ||||
| </style> | ||||
| @@ -776,5 +776,7 @@ | ||||
|     "Monitor Setting": "{0}'s Monitor Einstellung", | ||||
|     "Badge Prefix": "Badge Präfix", | ||||
|     "Badge Suffix": "Badge Suffix", | ||||
|     "Badge Warn Days": "Badge Warnung Tage" | ||||
|     "Badge Warn Days": "Badge Warnung Tage", | ||||
|     "Group": "Gruppe", | ||||
|     "Monitor Group": "Monitor Gruppe" | ||||
| } | ||||
|   | ||||
| @@ -744,5 +744,7 @@ | ||||
|     "Badge Down Days": "Badge Down Days", | ||||
|     "Badge Style": "Badge Style", | ||||
|     "Badge value (For Testing only.)": "Badge value (For Testing only.)", | ||||
|     "Badge URL": "Badge URL" | ||||
|     "Badge URL": "Badge URL", | ||||
|     "Group": "Group", | ||||
|     "Monitor Group": "Monitor Group" | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| <template> | ||||
|     <transition name="slide-fade" appear> | ||||
|         <div v-if="monitor"> | ||||
|             <router-link v-if="group !== ''" :to="monitorURL(monitor.parent)"> {{ group }}</router-link> | ||||
|             <h1> {{ monitor.name }}</h1> | ||||
|             <p v-if="monitor.description">{{ monitor.description }}</p> | ||||
|             <div class="tags"> | ||||
| @@ -40,7 +41,7 @@ | ||||
|                     <button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog"> | ||||
|                         <font-awesome-icon icon="pause" /> {{ $t("Pause") }} | ||||
|                     </button> | ||||
|                     <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor"> | ||||
|                     <button v-if="! monitor.active" class="btn btn-primary" :disabled="monitor.forceInactive" @click="resumeMonitor"> | ||||
|                         <font-awesome-icon icon="play" /> {{ $t("Resume") }} | ||||
|                     </button> | ||||
|                     <router-link :to=" '/edit/' + monitor.id " class="btn btn-normal"> | ||||
| @@ -69,7 +70,7 @@ | ||||
|  | ||||
|             <div class="shadow-box big-padding text-center stats"> | ||||
|                 <div class="row"> | ||||
|                     <div class="col-12 col-sm col row d-flex align-items-center d-sm-block"> | ||||
|                     <div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block"> | ||||
|                         <h4 class="col-4 col-sm-12">{{ pingTitle() }}</h4> | ||||
|                         <p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("Current") }})</p> | ||||
|                         <span class="col-4 col-sm-12 num"> | ||||
| @@ -78,7 +79,7 @@ | ||||
|                             </a> | ||||
|                         </span> | ||||
|                     </div> | ||||
|                     <div class="col-12 col-sm col row d-flex align-items-center d-sm-block"> | ||||
|                     <div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block"> | ||||
|                         <h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4> | ||||
|                         <p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p> | ||||
|                         <span class="col-4 col-sm-12 num"> | ||||
| @@ -214,6 +215,7 @@ import Pagination from "v-pagination-3"; | ||||
| const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue")); | ||||
| import Tag from "../components/Tag.vue"; | ||||
| import CertificateInfo from "../components/CertificateInfo.vue"; | ||||
| import { getMonitorRelativeURL } from "../util.ts"; | ||||
| import { URL } from "whatwg-url"; | ||||
|  | ||||
| export default { | ||||
| @@ -313,6 +315,13 @@ export default { | ||||
|             return this.heartBeatList.slice(startIndex, endIndex); | ||||
|         }, | ||||
|  | ||||
|         group() { | ||||
|             if (!this.monitor.pathName.includes("/")) { | ||||
|                 return ""; | ||||
|             } | ||||
|             return this.monitor.pathName.substr(0, this.monitor.pathName.lastIndexOf("/")); | ||||
|         }, | ||||
|  | ||||
|         pushURL() { | ||||
|             return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping="; | ||||
|         }, | ||||
| @@ -409,6 +418,15 @@ export default { | ||||
|             return this.$t(translationPrefix + "Ping"); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Get URL of monitor | ||||
|          * @param {number} id ID of monitor | ||||
|          * @returns {string} Relative URL of monitor | ||||
|          */ | ||||
|         monitorURL(id) { | ||||
|             return getMonitorRelativeURL(id); | ||||
|         }, | ||||
|  | ||||
|         /** Filter and hide password in URL for display */ | ||||
|         filterPassword(urlString) { | ||||
|             try { | ||||
|   | ||||
| @@ -36,7 +36,7 @@ | ||||
|                                     v-model="affectedMonitors" | ||||
|                                     :options="affectedMonitorsOptions" | ||||
|                                     track-by="id" | ||||
|                                     label="name" | ||||
|                                     label="pathName" | ||||
|                                     :multiple="true" | ||||
|                                     :close-on-select="false" | ||||
|                                     :clear-on-select="false" | ||||
| @@ -381,17 +381,39 @@ export default { | ||||
|         }, | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.init(); | ||||
|  | ||||
|         this.$root.getMonitorList((res) => { | ||||
|             if (res.ok) { | ||||
|                 Object.values(this.$root.monitorList).map(monitor => { | ||||
|                 Object.values(this.$root.monitorList).sort((m1, m2) => { | ||||
|  | ||||
|                     if (m1.active !== m2.active) { | ||||
|                         if (m1.active === 0) { | ||||
|                             return 1; | ||||
|                         } | ||||
|  | ||||
|                         if (m2.active === 0) { | ||||
|                             return -1; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (m1.weight !== m2.weight) { | ||||
|                         if (m1.weight > m2.weight) { | ||||
|                             return -1; | ||||
|                         } | ||||
|  | ||||
|                         if (m1.weight < m2.weight) { | ||||
|                             return 1; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return m1.pathName.localeCompare(m2.pathName); | ||||
|                 }).map(monitor => { | ||||
|                     this.affectedMonitorsOptions.push({ | ||||
|                         id: monitor.id, | ||||
|                         name: monitor.name, | ||||
|                         pathName: monitor.pathName, | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|             this.init(); | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
| @@ -429,7 +451,7 @@ export default { | ||||
|                         this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => { | ||||
|                             if (res.ok) { | ||||
|                                 Object.values(res.monitors).map(monitor => { | ||||
|                                     this.affectedMonitors.push(monitor); | ||||
|                                     this.affectedMonitors.push(this.affectedMonitorsOptions.find(item => item.id === monitor.id)); | ||||
|                                 }); | ||||
|                             } else { | ||||
|                                 toast.error(res.msg); | ||||
|   | ||||
| @@ -12,6 +12,9 @@ | ||||
|                                 <label for="type" class="form-label">{{ $t("Monitor Type") }}</label> | ||||
|                                 <select id="type" v-model="monitor.type" class="form-select"> | ||||
|                                     <optgroup :label="$t('General Monitor Type')"> | ||||
|                                         <option value="group"> | ||||
|                                             {{ $t("Group") }} | ||||
|                                         </option> | ||||
|                                         <option value="http"> | ||||
|                                             HTTP(s) | ||||
|                                         </option> | ||||
| @@ -89,6 +92,15 @@ | ||||
|                                 <input id="name" v-model="monitor.name" type="text" class="form-control" required> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Parent Monitor --> | ||||
|                             <div class="my-3"> | ||||
|                                 <label for="parent" class="form-label">{{ $t("Monitor Group") }}</label> | ||||
|                                 <select v-model="monitor.parent" class="form-select" :disabled="sortedMonitorList.length === 0"> | ||||
|                                     <option :value="null" selected>{{ $t("None") }}</option> | ||||
|                                     <option v-for="parentMonitor in sortedMonitorList" :key="parentMonitor.id" :value="parentMonitor.id">{{ parentMonitor.pathName }}</option> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- URL --> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3"> | ||||
|                                 <label for="url" class="form-label">{{ $t("URL") }}</label> | ||||
| @@ -807,6 +819,48 @@ message HealthCheckResponse { | ||||
|             return null; | ||||
|         }, | ||||
|  | ||||
|         // Filter result by active state, weight and alphabetical | ||||
|         // Only return groups which arent't itself and one of its decendants | ||||
|         sortedMonitorList() { | ||||
|             let result = Object.values(this.$root.monitorList); | ||||
|             console.log(this.monitor.childrenIDs); | ||||
|  | ||||
|             // Only groups, not itself, not a decendant | ||||
|             result = result.filter( | ||||
|                 monitor => monitor.type === "group" && | ||||
| 				monitor.id !== this.monitor.id && | ||||
| 				!this.monitor.childrenIDs?.includes(monitor.id) | ||||
|             ); | ||||
|  | ||||
|             // Filter result by active state, weight and alphabetical | ||||
|             result.sort((m1, m2) => { | ||||
|  | ||||
|                 if (m1.active !== m2.active) { | ||||
|                     if (m1.active === 0) { | ||||
|                         return 1; | ||||
|                     } | ||||
|  | ||||
|                     if (m2.active === 0) { | ||||
|                         return -1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (m1.weight !== m2.weight) { | ||||
|                     if (m1.weight > m2.weight) { | ||||
|                         return -1; | ||||
|                     } | ||||
|  | ||||
|                     if (m1.weight < m2.weight) { | ||||
|                         return 1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return m1.pathName.localeCompare(m2.pathName); | ||||
|             }); | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|  | ||||
|     }, | ||||
|     watch: { | ||||
|         "$root.proxyList"() { | ||||
| @@ -926,6 +980,7 @@ message HealthCheckResponse { | ||||
|                 this.monitor = { | ||||
|                     type: "http", | ||||
|                     name: "", | ||||
|                     parent: null, | ||||
|                     url: "https://", | ||||
|                     method: "GET", | ||||
|                     interval: 60, | ||||
|   | ||||
| @@ -278,11 +278,11 @@ | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="mt-3"> | ||||
|                     <div v-if="allMonitorList.length > 0 && loadedData"> | ||||
|                     <div v-if="sortedMonitorList.length > 0 && loadedData"> | ||||
|                         <label>{{ $t("Add a monitor") }}:</label> | ||||
|                         <VueMultiselect | ||||
|                             v-model="selectedMonitor" | ||||
|                             :options="allMonitorList" | ||||
|                             :options="sortedMonitorList" | ||||
|                             :multiple="false" | ||||
|                             :searchable="true" | ||||
|                             :placeholder="$t('Add a monitor')" | ||||
| @@ -292,7 +292,7 @@ | ||||
|                         > | ||||
|                             <template #option="{ option }"> | ||||
|                                 <div class="d-inline-flex"> | ||||
|                                     <span>{{ option.name }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span> | ||||
|                                     <span>{{ option.pathName }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span> | ||||
|                                 </div> | ||||
|                             </template> | ||||
|                         </VueMultiselect> | ||||
| @@ -449,7 +449,7 @@ export default { | ||||
|         /** | ||||
|          * If the monitor is added to public list, which will not be in this list. | ||||
|          */ | ||||
|         allMonitorList() { | ||||
|         sortedMonitorList() { | ||||
|             let result = []; | ||||
|  | ||||
|             for (let id in this.$root.monitorList) { | ||||
| @@ -459,6 +459,31 @@ export default { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             result.sort((m1, m2) => { | ||||
|  | ||||
|                 if (m1.active !== m2.active) { | ||||
|                     if (m1.active === 0) { | ||||
|                         return 1; | ||||
|                     } | ||||
|  | ||||
|                     if (m2.active === 0) { | ||||
|                         return -1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (m1.weight !== m2.weight) { | ||||
|                     if (m1.weight > m2.weight) { | ||||
|                         return -1; | ||||
|                     } | ||||
|  | ||||
|                     if (m1.weight < m2.weight) { | ||||
|                         return 1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return m1.pathName.localeCompare(m2.pathName); | ||||
|             }); | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user