Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error page for initial REST request failure & Abort further load #1987

Merged
merged 9 commits into from
Sep 30, 2023
5 changes: 4 additions & 1 deletion bundles/org.openhab.ui/web/src/assets/i18n/common/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dialogs.copy": "Copy",
"dialogs.delete": "Delete",
"dialogs.reload": "Reload",
"dialogs.retry": "Try Again",
"dialogs.showAll": "Show All",
"dialogs.search": "Search",
"dialogs.search.items": "Search items",
Expand Down Expand Up @@ -44,5 +45,7 @@
"page.navbar.edit": "Edit",
"admin.notTranslatedYet": "The administration area is not translated yet.",
"error.communicationFailure": "Communication failure",
"error.itemNotFound": "%s not found"
"error.itemNotFound": "%s not found",
"error.notReachable.title": "openHAB is offline",
"error.notReachable.msg": "The openHAB server cannot be reached at the moment. Please check your network connection and your server."
}
147 changes: 31 additions & 116 deletions bundles/org.openhab.ui/web/src/components/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<f7-app v-if="init" :style="{ visibility: (($store.getters.user || $store.getters.page('overview')) || loginScreenOpened) ? '' : 'hidden' }" :params="f7params" :class="{ 'theme-dark': this.themeOptions.dark === 'dark', 'theme-filled': this.themeOptions.bars === 'filled' }">
<f7-app v-if="init" :style="{ visibility: (($store.getters.user || $store.getters.page('overview')) || communicationFailureMsg) ? '' : 'hidden' }" :params="f7params" :class="{ 'theme-dark': this.themeOptions.dark === 'dark', 'theme-filled': this.themeOptions.bars === 'filled' }">
<!-- Left Panel -->
<f7-panel v-show="ready" left :cover="showSidebar" class="sidebar" :visible-breakpoint="1024">
<f7-page>
Expand Down Expand Up @@ -144,44 +144,24 @@
<developer-sidebar />
</f7-panel>

<f7-view main v-show="ready" class="safe-areas" url="/" :master-detail-breakpoint="960" :animate="themeOptions.pageTransitionAnimation !== 'disabled'" />

<!-- <f7-login-screen id="my-login-screen" :opened="loginScreenOpened">
<f7-view name="login" v-if="$device.cordova">
<f7-page login-screen>
<f7-login-screen-title><img src="@/images/openhab-logo.svg" type="image/svg+xml" width="200px"><br>Login</f7-login-screen-title>
<f7-list form>
<f7-list-input
type="text"
name="serverUrl"
placeholder="openHAB server URL"
:value="serverUrl"
@input="serverUrl = $event.target.value"
></f7-list-input>
<f7-list-input
type="text"
name="username"
placeholder="Username (optional)"
:value="username"
@input="username = $event.target.value"
></f7-list-input>
<f7-list-input
type="password"
name="password"
placeholder="Password (optional)"
:value="password"
@input="password = $event.target.value"
></f7-list-input>
</f7-list>
<f7-block v-if="!ready && communicationFailureMsg" class="block-narrow">
<empty-state-placeholder icon="wifi_slash" :title="$t('error.notReachable.title')" :text="$t('error.notReachable.msg') + '<br/><br/>Error: ' + communicationFailureMsg" />
<f7-col>
<f7-list>
<f7-list-button title="Sign In" @click="login"></f7-list-button>
<f7-block-footer>
Some text about login information.
</f7-block-footer>
<f7-list-button color="blue" @click="loadData">
{{ $t('dialogs.retry') }}
</f7-list-button>
<f7-list-button color="blue" @click="reload">
{{ $t('about.reload.reloadApp') }}
</f7-list-button>
<f7-list-button v-if="showCachePurgeOption" color="red" @click="purgeServiceWorkerAndCaches">
{{ $t('about.reload.purgeCachesAndRefresh') }}
</f7-list-button>
</f7-list>
</f7-page>
</f7-view>
</f7-login-screen> -->
</f7-col>
</f7-block>

<f7-view main v-show="ready" class="safe-areas" url="/" :master-detail-breakpoint="960" :animate="themeOptions.pageTransitionAnimation !== 'disabled'" />
</f7-app>
</template>

Expand Down Expand Up @@ -269,16 +249,21 @@ import cordovaApp from '../js/cordova-app.js'
import routes from '../js/routes.js'
import PanelRight from '../pages/panel-right.vue'
import DeveloperSidebar from './developer/developer-sidebar.vue'
import EmptyStatePlaceholder from '@/components/empty-state-placeholder.vue'

import { loadLocaleMessages } from '@/js/i18n'

import auth from './auth-mixin.js'
import i18n from './i18n-mixin.js'
import connectionHealth from './connection-health-mixin.js'

import dayjs from 'dayjs'
import dayjsLocales from 'dayjs/locale.json'

export default {
mixins: [auth, i18n],
mixins: [auth, i18n, connectionHealth],
components: {
EmptyStatePlaceholder,
PanelRight,
DeveloperSidebar
},
Expand Down Expand Up @@ -377,7 +362,6 @@ export default {
pages: null,
showSidebar: true,
visibleBreakpointDisabled: false,
loginScreenOpened: false,
loggedIn: false,

themeOptions: {
Expand All @@ -388,12 +372,12 @@ export default {
showSettingsSubmenu: false,
showDeveloperSubmenu: false,
showDeveloperSidebar: false,
currentUrl: '',

communicationFailureToast: null,
communicationFailureTimeoutId: null
currentUrl: ''
}
},
i18n: {
messages: loadLocaleMessages(require.context('@/assets/i18n/about'))
},
computed: {
isAdmin () {
if (!this.$store.getters.apiEndpoint('auth')) return true
Expand Down Expand Up @@ -430,7 +414,6 @@ export default {
this.loadData(true)
return Promise.reject()
}
this.loginScreenOpened = true
this.$nextTick(() => {
this.$f7.dialog.login(
window.location.host,
Expand Down Expand Up @@ -478,7 +461,9 @@ export default {
})
}
} else {
this.$f7.dialog.alert('openHAB REST API connection failed with error ' + err.message || err.status)
// Make sure this is set to a value, otherwise the page won't show up
this.communicationFailureMsg = err.message || err.status || 'Unknown Error'
return Promise.reject('openHAB REST API connection failed with error: ' + err.message || err.status)
}
})
.then((res) => res.data)
Expand Down Expand Up @@ -554,7 +539,6 @@ export default {
localStorage.setItem('openhab.ui:username', this.username)
localStorage.setItem('openhab.ui:password', this.password)
this.loadData().then(() => {
this.loginScreenOpened = false
this.loggedIn = true
}).catch((err) => {
localStorage.removeItem('openhab.ui:serverUrl')
Expand All @@ -578,7 +562,6 @@ export default {
this.$f7.views.main.router.navigate('/', { animate: false, clearPreviousHistory: true })
window.location = window.location.origin
if (this.$device.cordova) {
this.loginScreenOpened = true
}
}).catch((err) => {
this.$f7.preloader.hide()
Expand Down Expand Up @@ -667,30 +650,6 @@ export default {
function unlock () { audioContext.resume().then(clean) }
function clean () { events.forEach(e => b.removeEventListener(e, unlock)) }
}
},
/**
* Creates and opens a toast message that indicates a failure, e.g. of SSE connection
* @param {string} message message to show
* @param {boolean} [reloadButton=false] displays a reload button
* @param {boolean} [autoClose=true] closes toast automatically
* @returns {Toast.Toast}
*/
displayFailureToast (message, reloadButton = false, autoClose = true) {
const toast = this.$f7.toast.create({
text: message,
closeButton: reloadButton,
closeButtonText: this.$t('dialogs.reload'),
destroyOnClose: true,
closeTimeout: (autoClose) ? 5000 : undefined,
cssClass: 'failure-toast button-outline',
position: 'bottom',
horizontalPosition: 'center'
})
toast.on('closeButtonClick', () => {
window.location.reload()
})
toast.open()
return toast
}
},
created () {
Expand All @@ -702,7 +661,7 @@ export default {
window.OHApp.goFullscreen()
} catch {}
}
// this.loginScreenOpened = true

const refreshToken = this.getRefreshToken()
if (refreshToken) {
this.refreshAccessToken().then(() => {
Expand Down Expand Up @@ -788,50 +747,6 @@ export default {
}
})

this.$store.subscribe((mutation, state) => {
if (mutation.type === 'sseConnected') {
if (!window.OHApp && this.$f7) {
if (mutation.payload === false) {
if (this.communicationFailureToast === null) {
this.communicationFailureTimeoutId = setTimeout(() => {
if (this.communicationFailureToast !== null) return
this.communicationFailureToast = this.displayFailureToast(this.$t('error.communicationFailure'), true, false)
this.communicationFailureTimeoutId = null
}, 1000)
}
} else if (mutation.payload === true) {
if (this.communicationFailureTimeoutId !== null) clearTimeout(this.communicationFailureTimeoutId)
if (this.communicationFailureToast !== null) {
this.communicationFailureToast.close()
this.communicationFailureToast = null
}
}
}
}
})

this.$store.subscribeAction({
error: (action, state, error) => {
if (action.type === 'sendCommand') {
let reloadButton = true
let msg = this.$t('error.communicationFailure')
switch (error) {
case 404:
case 'Not Found':
msg = this.$t('error.itemNotFound').replace('%s', action.payload.itemName)
reloadButton = false
return this.displayFailureToast(msg, reloadButton)
}
if (this.communicationFailureToast === null) {
this.communicationFailureToast = this.displayFailureToast(this.$t('error.communicationFailure'), true, true)
this.communicationFailureToast.on('closed', () => {
this.communicationFailureToast = null
})
}
}
}
})

if (window) {
window.addEventListener('keydown', this.keyDown)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import mixin from 'reload-mixin.js'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - this could have waited, enjoy your concert! (who is it?)
I'll tentatively do the testing tomorrow. The builds still fail, though:

[INFO] ERROR in ./src/components/connection-health-mixin.js
[INFO] Module not found: Error: Can't resolve 'reload-mixin.js' in '/home/runner/work/openhab-webui/openhab-webui/bundles/org.openhab.ui/web/src/components'
[INFO]  @ ./src/components/connection-health-mixin.js 1:0-36 3:11-16
Suggested change
import mixin from 'reload-mixin.js'
import mixin from './reload-mixin'

As an habit I dropped the .js extensions in imports, except in assets where is it required to build the component docs (as it is run by Node which requires them).


export default {
mixins: [mixin],
data () {
return {
// For the communication failure toast
communicationFailureToast: null,
communicationFailureTimeoutId: null,
// For the communication failure page
communicationFailureMsg: null
}
},
methods: {
/**
* Creates and opens a toast message that indicates a failure, e.g. of SSE connection
* @param {string} message message to show
* @param {boolean} [reloadButton=false] displays a reload button
* @param {boolean} [autoClose=true] closes toast automatically
* @returns {Toast.Toast}
*/
displayFailureToast (message, reloadButton = false, autoClose = true) {
const toast = this.$f7.toast.create({
text: message,
closeButton: reloadButton,
closeButtonText: this.$t('dialogs.reload'),
destroyOnClose: autoClose,
closeTimeout: (autoClose) ? 5000 : undefined,
cssClass: 'failure-toast button-outline',
position: 'bottom',
horizontalPosition: 'center'
})
toast.on('closeButtonClick', () => {
window.location.reload()
})
toast.open()
return toast
}
},
created () {
this.checkPurgeServiceWorkerAndCachesAvailable()
},
mounted () {
this.$f7ready((f7) => {
this.$store.subscribe((mutation, state) => {
if (this.ready) {
if (mutation.type === 'sseConnected') {
if (!window.OHApp && this.$f7) {
if (mutation.payload === false) {
if (this.communicationFailureToast === null) {
this.communicationFailureTimeoutId = setTimeout(() => {
if (this.communicationFailureToast !== null) return
this.communicationFailureToast = this.displayFailureToast(this.$t('error.communicationFailure'), true, false)
this.communicationFailureToast.open()
this.communicationFailureTimeoutId = null
}, 1000)
}
} else if (mutation.payload === true) {
if (this.communicationFailureTimeoutId !== null) clearTimeout(this.communicationFailureTimeoutId)
if (this.communicationFailureToast !== null) {
this.communicationFailureToast.close()
this.communicationFailureToast.destroy()
this.communicationFailureToast = null
}
}
}
}
}
})

this.$store.subscribeAction({
error: (action, state, error) => {
if (action.type === 'sendCommand') {
let reloadButton = true
let msg = this.$t('error.communicationFailure')
switch (error) {
case 404:
case 'Not Found':
msg = this.$t('error.itemNotFound').replace('%s', action.payload.itemName)
reloadButton = false
return this.displayFailureToast(msg, reloadButton)
}
if (this.communicationFailureToast === null) {
this.communicationFailureToast = this.displayFailureToast(this.$t('error.communicationFailure'), true, true)
this.communicationFailureToast.on('closed', () => {
this.communicationFailureToast = null
})
}
}
}
})
})
}
}
Loading
Loading