diff --git a/bundles/org.openhab.ui/web/src/assets/i18n/common/en.json b/bundles/org.openhab.ui/web/src/assets/i18n/common/en.json index 36d13cb473..dc2229ef97 100644 --- a/bundles/org.openhab.ui/web/src/assets/i18n/common/en.json +++ b/bundles/org.openhab.ui/web/src/assets/i18n/common/en.json @@ -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", @@ -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." } diff --git a/bundles/org.openhab.ui/web/src/components/app.vue b/bundles/org.openhab.ui/web/src/components/app.vue index bc7d551abf..3571730fd9 100644 --- a/bundles/org.openhab.ui/web/src/components/app.vue +++ b/bundles/org.openhab.ui/web/src/components/app.vue @@ -1,5 +1,5 @@ @@ -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 auth from './auth-mixin' +import i18n from './i18n-mixin' +import connectionHealth from './connection-health-mixin' import dayjs from 'dayjs' import dayjsLocales from 'dayjs/locale.json' export default { - mixins: [auth, i18n], + mixins: [auth, i18n, connectionHealth], components: { + EmptyStatePlaceholder, PanelRight, DeveloperSidebar }, @@ -377,7 +362,6 @@ export default { pages: null, showSidebar: true, visibleBreakpointDisabled: false, - loginScreenOpened: false, loggedIn: false, themeOptions: { @@ -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 @@ -430,7 +414,6 @@ export default { this.loadData(true) return Promise.reject() } - this.loginScreenOpened = true this.$nextTick(() => { this.$f7.dialog.login( window.location.host, @@ -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) @@ -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') @@ -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() @@ -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 () { @@ -702,7 +661,7 @@ export default { window.OHApp.goFullscreen() } catch {} } - // this.loginScreenOpened = true + const refreshToken = this.getRefreshToken() if (refreshToken) { this.refreshAccessToken().then(() => { @@ -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) } diff --git a/bundles/org.openhab.ui/web/src/components/connection-health-mixin.js b/bundles/org.openhab.ui/web/src/components/connection-health-mixin.js new file mode 100644 index 0000000000..914482da6d --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/connection-health-mixin.js @@ -0,0 +1,94 @@ +import reloadMixin from './reload-mixin' + +export default { + mixins: [reloadMixin], + 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', () => { + this.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 + }) + } + } + } + }) + }) + } +} diff --git a/bundles/org.openhab.ui/web/src/components/reload-mixin.js b/bundles/org.openhab.ui/web/src/components/reload-mixin.js new file mode 100644 index 0000000000..df05485f4b --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/reload-mixin.js @@ -0,0 +1,64 @@ +import { loadLocaleMessages } from '@/js/i18n' + +export default { + data () { + return { + showCachePurgeOption: false + } + }, + i18n: { + messages: loadLocaleMessages(require.context('@/assets/i18n/about')) + }, + methods: { + checkPurgeServiceWorkerAndCachesAvailable () { + if (navigator.serviceWorker) { + navigator.serviceWorker.getRegistrations().then((registrations) => { + if (registrations.length > 0) { + this.showCachePurgeOption = true + } + }) + } + if (window.caches) { + window.caches.keys().then((cachesNames) => { + if (cachesNames.length > 0) { + this.showCachePurgeOption = true + } + }) + } + }, + purgeServiceWorkerAndCaches () { + this.$f7.dialog.confirm( + this.$t('about.reload.confirmPurge'), + () => { + navigator.serviceWorker.getRegistrations().then(function (registrations) { + for (let registration of registrations) { + registration.unregister().then(function () { + return self.clients.matchAll() + }).then(function (clients) { + clients.forEach(client => { + if (client.url && 'navigate' in client) { + setTimeout(() => { client.navigate(client.url.split('#')[0]) }, 1000) + } + }) + }) + } + }) + window.caches.keys().then(function (cachesNames) { + console.log('Deleting caches') + return Promise.all(cachesNames.map(function (cacheName) { + return caches.delete(cacheName).then(function () { + console.log('Cache with name ' + cacheName + ' is deleted') + }) + })) + }).then(function () { + console.log('Caches deleted') + setTimeout(() => { location.reload(true) }, 1000) + }) + } + ) + }, + reload () { + window.location.reload() + } + } +} diff --git a/bundles/org.openhab.ui/web/src/pages/about.vue b/bundles/org.openhab.ui/web/src/pages/about.vue index 93875cc995..0461dce5d5 100644 --- a/bundles/org.openhab.ui/web/src/pages/about.vue +++ b/bundles/org.openhab.ui/web/src/pages/about.vue @@ -107,7 +107,10 @@ import ThemeSwitcher from '../components/theme-switcher.vue' import YAML from 'yaml' import { loadLocaleMessages } from '@/js/i18n' +import reloadMixin from '../components/reload-mixin.js' + export default { + mixins: [reloadMixin], components: { ThemeSwitcher }, @@ -115,7 +118,6 @@ export default { return { systemInfo: null, textualSystemInfoOpened: false, - showCachePurgeOption: false, bindings: null } }, @@ -159,54 +161,7 @@ export default { this.$oh.api.get('/rest/systeminfo').then((data) => { this.systemInfo = data.systemInfo }) this.$oh.api.get('/rest/addons').then((data) => { this.addons = data.filter((a) => a.installed).map((a) => a.uid).sort() }) } - if (navigator.serviceWorker) { - navigator.serviceWorker.getRegistrations().then((registrations) => { - if (registrations.length > 0) { - this.showCachePurgeOption = true - } - }) - } - if (window.caches) { - window.caches.keys().then((cachesNames) => { - if (cachesNames.length > 0) { - this.showCachePurgeOption = true - } - }) - } - }, - purgeServiceWorkerAndCaches () { - this.$f7.dialog.confirm( - this.$t('about.reload.confirmPurge'), - () => { - navigator.serviceWorker.getRegistrations().then(function (registrations) { - for (let registration of registrations) { - registration.unregister().then(function () { - return self.clients.matchAll() - }).then(function (clients) { - clients.forEach(client => { - if (client.url && 'navigate' in client) { - setTimeout(() => { client.navigate(client.url.split('#')[0]) }, 1000) - } - }) - }) - } - }) - window.caches.keys().then(function (cachesNames) { - console.log('Deleting caches') - return Promise.all(cachesNames.map(function (cacheName) { - return caches.delete(cacheName).then(function () { - console.log('Cache with name ' + cacheName + ' is deleted') - }) - })) - }).then(function () { - console.log('Caches deleted') - setTimeout(() => { location.reload(true) }, 1000) - }) - } - ) - }, - reload () { - document.location.reload() + this.checkPurgeServiceWorkerAndCachesAvailable() }, copyTextualSystemInfo () { let el = document.getElementById('textual-systeminfo')