Add UI for health checks (broken links) (#2420)

Refs on https://github.com/openhab/openhab-core/pull/4115.

This is the starting point for a UI that shows issues with the users installation,
the first function is broken links.

---------

Also-by: Florian Hotze <florianh_dev@icloud.com>
Signed-off-by: Arne Seime <arne.seime@gmail.com>
pull/2638/head
Arne Seime 2024-06-29 16:13:00 +02:00 committed by GitHub
parent e9210dc8c2
commit 32688a562e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 234 additions and 5 deletions

View File

@ -39,6 +39,10 @@
</f7-list-item>
<li v-if="showSettingsSubmenu">
<ul class="menu-sublinks">
<f7-list-item v-if="$store.getters.apiEndpoint('links')" link="/settings/health/" title="Health checks" view=".view-main" panel-close :animate="false" no-chevron
:class="{ currentsection: currentUrl.indexOf('/settings/health') === 0 }">
<f7-icon slot="media" f7="heart" color="gray" />
</f7-list-item>
<f7-list-item v-if="$store.getters.apiEndpoint('things')" link="/settings/things/" title="Things" view=".view-main" panel-close :animate="false" no-chevron
:class="{ currentsection: currentUrl.indexOf('/settings/things') === 0 }">
<f7-icon slot="media" f7="lightbulb" color="gray" />

View File

@ -23,6 +23,8 @@ const ItemEditPage = () => import(/* webpackChunkName: "admin-config" */ '../pag
const ItemMetadataEditPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/items/metadata/item-metadata-edit.vue')
const ItemsAddFromTextualDefinition = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/items/parser/items-add-from-textual-definition.vue')
const HealthOverviewPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/health/health-overview.vue')
const HealthOrphanLinksPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/health/health-orphanlinks.vue')
const ThingsListPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/things/things-list.vue')
const ThingDetailsPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/things/thing-details.vue')
const AddThingChooseBindingPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/things/add/choose-binding.vue')
@ -67,7 +69,7 @@ const SetupWizardPage = () => import(/* webpackChunkName: "setup-wizard" */ '../
const checkDirtyBeforeLeave = function (routeTo, routeFrom, resolve, reject) {
if (this.currentPageEl && this.currentPageEl.__vue__ && this.currentPageEl.__vue__.$parent && this.currentPageEl.__vue__.$parent.beforeLeave &&
!routeTo.path.startsWith(routeFrom.path)) {
!routeTo.path.startsWith(routeFrom.path)) {
this.currentPageEl.__vue__.$parent.beforeLeave(this, routeTo, routeFrom, resolve, reject)
} else {
resolve()
@ -77,11 +79,17 @@ const checkDirtyBeforeLeave = function (routeTo, routeFrom, resolve, reject) {
const loadAsync = (page, props) => {
return (routeTo, routeFrom, resolve, reject) => {
if (!props) {
page().then((c) => { resolve({ component: c.default }) })
page().then((c) => {
resolve({ component: c.default })
})
} else if (typeof props === 'object') {
page().then((c) => { resolve({ component: c.default }, { props }) })
page().then((c) => {
resolve({ component: c.default }, { props })
})
} else if (typeof props === 'function') {
page().then((c) => { resolve({ component: c.default }, { props: props(routeTo, routeFrom, resolve, reject) }) })
page().then((c) => {
resolve({ component: c.default }, { props: props(routeTo, routeFrom, resolve, reject) })
})
}
}
}
@ -216,7 +224,9 @@ export default [
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: (routeTo, routeFrom, resolve, reject) => {
PageEditors[routeTo.params.type]().then((c) => { resolve({ component: c.default }, (routeTo.params.uid === 'add') ? { props: { createMode: true } } : {}) })
PageEditors[routeTo.params.type]().then((c) => {
resolve({ component: c.default }, (routeTo.params.uid === 'add') ? { props: { createMode: true } } : {})
})
}
}
]
@ -232,6 +242,18 @@ export default [
}
]
},
{
path: 'health',
beforeEnter: [enforceAdminForRoute],
async: loadAsync(HealthOverviewPage),
routes: [
{
path: 'orphanlinks',
beforeEnter: [enforceAdminForRoute],
async: loadAsync(HealthOrphanLinksPage)
}
]
},
{
path: 'things/',
beforeEnter: [enforceAdminForRoute],

View File

@ -0,0 +1,112 @@
<template>
<f7-page @page:afterin="onPageAfterIn">
<f7-navbar title="Orphan links" back-link="Health checks" back-link-url="/settings/health/" back-link-force>
<f7-nav-right>
<developer-dock-icon />
</f7-nav-right>
</f7-navbar>
<f7-block class="block-narrow">
<f7-col>
<f7-block-footer class="padding-horizontal">
Orphan links are items pointing to non-existent thing channels or vica versa.
<br>
<br>
Note that only the links of managed Items can be purged, not links defined
in <code>.items</code> files - these must be fixed manually in the corresponding file.
The latter are marked with <f7-icon f7="lock_fill" size="1rem" color="gray" />.
</f7-block-footer>
</f7-col>
</f7-block>
<f7-block class="block-narrow">
<!-- skeleton for not ready -->
<f7-col v-if="!ready">
<f7-block-title>&nbsp;Loading...</f7-block-title>
<f7-list contacts-list class="col">
<f7-list-group>
<f7-list-item media-item v-for="n in 10" :key="n" :class="`skeleton-text skeleton-effect-blink`"
title="Type of problem" subtitle="Item name" footer="Channel link" />
</f7-list-group>
</f7-list>
</f7-col>
<f7-col v-else>
<f7-block-title>
{{ orphanLinks.length }} orphan links found
</f7-block-title>
<f7-list class="col" contacts-list>
<f7-list-item v-for="orphanLink in orphanLinks" :key="orphanLink.itemChannelLink.channelUID" media-item
:link="getLinkForProblem(orphanLink)" :title="'Problem: ' + orphanLinkProblemExplanation[orphanLink.problem]"
:subtitle="'Item name: ' + orphanLink.itemChannelLink.itemName"
:footer="'Channel UID: ' + orphanLink.itemChannelLink.channelUID">
<f7-icon v-if="!orphanLink.itemChannelLink.editable" slot="after-title" f7="lock_fill" size="1rem" color="gray" />
</f7-list-item>
</f7-list>
</f7-col>
</f7-block>
<f7-block class="block-narrow">
<f7-col>
<f7-list>
<f7-list-button color="red" @click="purgeAllManaged()">
Purge all managed links (will purge {{ purgeableLinksCount }} managed links)
</f7-list-button>
</f7-list>
</f7-col>
</f7-block>
</f7-page>
</template>
<script>
export default {
data () {
return {
ready: false,
loading: false,
orphanLinks: [],
orphanLinkProblemExplanation: {
THING_CHANNEL_MISSING: 'The item is linked to a thing channel that does not exist',
ITEM_MISSING: 'The item does not exist',
ITEM_AND_THING_CHANNEL_MISSING: 'Neither the item nor thing channel exists'
}
}
},
computed: {
purgeableLinksCount () {
return this.orphanLinks.filter((l) => l.itemChannelLink.editable).length
}
},
methods: {
onPageAfterIn () {
this.load()
},
load () {
this.loading = true
this.$oh.api.get('/rest/links/orphans').then((data) => {
this.orphanLinks = data
this.loading = false
this.ready = true
})
},
getLinkForProblem (orphanLink) {
if (orphanLink.problem === 'THING_CHANNEL_MISSING') {
return '/settings/items/' + orphanLink.itemChannelLink.itemName
}
return null
},
purgeAllManaged () {
this.loading = true
this.$oh.api.post('/rest/links/purge').catch((e) => {
// ignore parseerror due to empty response
if (e === 'parseerror') return
console.error(e)
}).finally(() => {
this.load()
})
}
}
}
</script>
<style></style>

View File

@ -0,0 +1,77 @@
<template>
<f7-page @page:afterin="onPageAfterIn">
<f7-navbar title="Health checks" back-link="Settings" back-link-url="/settings/" back-link-force>
<f7-nav-right>
<developer-dock-icon />
</f7-nav-right>
</f7-navbar>
<f7-block class="block-narrow">
<f7-col>
<f7-block-footer class="padding-horizontal">
This page provides information about potential issues with your openHAB setup.
<br>
It is recommended to fix these issues to ensure a stable and reliable system.
</f7-block-footer>
</f7-col>
</f7-block>
<f7-block class="block-narrow">
<f7-col>
<f7-list media-list>
<f7-list-item
media-item
link="orphanlinks/"
title="Orphan Links"
:badge="orphanLinksCount > 0 ? orphanLinksCount : undefined"
:after="orphanLinksCount > 0 ? undefined : orphanLinksCount"
:badge-color="orphanLinksCount ? 'red' : 'blue'"
:footer="objectsSubtitles.orphanLinks">
<f7-icon slot="media" f7="link" color="gray" />
</f7-list-item>
</f7-list>
</f7-col>
</f7-block>
</f7-page>
</template>
<script>
export default {
data () {
return {
objectsSubtitles: {
orphanLinks:
'Items pointing to non-existent thing channels or vica versa'
},
orphanLinksCount: '',
expandedTypes: {
systemSettings: this.$f7.width >= 1450
}
}
},
computed: {
apiEndpoints () {
return this.$store.state.apiEndpoints
}
},
watch: {
apiEndpoints () {
this.loadCounters()
}
},
methods: {
loadCounters () {
if (!this.apiEndpoints) return
if (this.$store.getters.apiEndpoint('links')) {
this.$oh.api.get('/rest/links/orphans').then((data) => {
this.orphanLinksCount = data.length.toString()
})
}
},
onPageAfterIn () {
this.loadCounters()
}
}
}
</script>

View File

@ -23,6 +23,17 @@
<f7-col :class="!addonsLoaded || (addonsLoaded && addonsInstalled.length > 0) ? 'settings-col' : ''" width="100" medium="50">
<f7-block-title>Configuration</f7-block-title>
<f7-list media-list class="search-list">
<f7-list-item
v-if="$store.getters.apiEndpoint('links')"
media-item
link="health/"
title="Health check"
:badge="(healthCount > 0) ? healthCount : undefined"
:after="(healthCount > 0) ? undefined : 0"
badge-color="red"
:footer="objectsSubtitles.health">
<f7-icon slot="media" f7="heart" color="gray" />
</f7-list-item>
<f7-list-item
v-if="$store.getters.apiEndpoint('things')"
media-item
@ -207,6 +218,7 @@ export default {
addonsServices: [],
systemServices: [],
objectsSubtitles: {
health: 'Manage detected system health issues',
things: 'Manage the physical layer',
model: 'The semantic model of your home',
items: 'Manage the functional layer',
@ -218,6 +230,7 @@ export default {
scripts: 'Rules dedicated to running code',
schedule: 'View upcoming time-based rules'
},
healthCount: '',
inboxCount: '',
thingsCount: '',
itemsCount: '',
@ -291,6 +304,7 @@ export default {
},
loadCounters () {
if (!this.apiEndpoints) return
if (this.$store.getters.apiEndpoint('links')) this.$oh.api.get('/rest/links/orphans').then((data) => { this.healthCount = data.length.toString() })
if (this.$store.getters.apiEndpoint('inbox')) this.$oh.api.get('/rest/inbox?includeIgnored=false').then((data) => { this.inboxCount = data.filter((e) => e.flag === 'NEW').length.toString() })
if (this.$store.getters.apiEndpoint('things')) this.$oh.api.get('/rest/things?staticDataOnly=true').then((data) => { this.thingsCount = data.length.toString() })
if (this.$store.getters.apiEndpoint('items')) this.$oh.api.get('/rest/items?staticDataOnly=true').then((data) => { this.itemsCount = data.length.toString() })