From c9182e23dc18419e2c9eef6470ca566f45d16398 Mon Sep 17 00:00:00 2001 From: Stephan de Wit Date: Fri, 31 May 2024 12:47:13 +0200 Subject: [PATCH] dashboard: handle error cases per widget If any widget failed to import/instantiate/update in the previous logic, this would halt execution for the entire dashboard. This commit takes care of these cases, but it cannot account for asynchronous callbacks executed in the widget logic itself, these should be caught there. --- src/opnsense/www/css/dashboard.css | 5 ++ .../www/js/opnsense_widget_manager.js | 68 ++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/opnsense/www/css/dashboard.css b/src/opnsense/www/css/dashboard.css index de3898312..fccf0572a 100644 --- a/src/opnsense/www/css/dashboard.css +++ b/src/opnsense/www/css/dashboard.css @@ -52,6 +52,11 @@ border-radius: 0.5em 0.5em 0.5em 0.5em; } +.widget-error { + margin: 50px; + color: #721c24 +} + .widget-content { position: relative; width: 100%; diff --git a/src/opnsense/www/js/opnsense_widget_manager.js b/src/opnsense/www/js/opnsense_widget_manager.js index 4f62458b4..16bcf1fd0 100644 --- a/src/opnsense/www/js/opnsense_widget_manager.js +++ b/src/opnsense/www/js/opnsense_widget_manager.js @@ -137,10 +137,9 @@ class WidgetManager { }); // Load all modules simultaneously - this shouldn't take long - await Promise.all(promises).catch((error) => { - console.error('Failed to load widgets', error); - null; - }); + const results = await Promise.all(promises.map(p => p.catch(e => e))); + const errors = results.filter(result => (result instanceof Error)); + if (errors.length > 0) console.error('Failed to load one or more widgets:', errors); }); } @@ -153,13 +152,21 @@ class WidgetManager { // restore for (const [id, configuration] of Object.entries(this.widgetConfigurations)) { if (id in this.loadedModules) { - this._createGridStackWidget(id, this.loadedModules[id], configuration); + try { + this._createGridStackWidget(id, this.loadedModules[id], configuration); + } catch (error) { + console.error('Failed to create widget', id, error); + } } } } else { // default for (const [identifier, widgetClass] of Object.entries(this.loadedModules)) { - this._createGridStackWidget(identifier, widgetClass); + try { + this._createGridStackWidget(identifier, widgetClass); + } catch (error) { + console.error('Failed to create widget', identifier, error); + } } } @@ -382,18 +389,47 @@ class WidgetManager { * individual widget tick() callbacks are not bound to a master timer, * this has the benefit of making it configurable per widget. */ - async _loadDynamicContent() { - // map to an array of context-bound _onMarkupRendered functions - let fns = Object.values(this.widgetClasses).map((widget) => { - return this._onMarkupRendered.bind(this, widget); + async _loadDynamicContent() { + // map to an array of context-bound _onMarkupRendered functions and their associated widget ids + let tasks = Object.entries(this.widgetClasses).map(([id, widget]) => { + return { + id, + func: this._onMarkupRendered.bind(this, widget) + }; }); - // convert each _onMarkupRendered(widget) to a promise - let promises = fns.map(func => new Promise(resolve => resolve(func()))); - // fire away - await Promise.all(promises).catch((error) => { - console.error('Failed to load dynamic content', error); - null; + + // Convert each _onMarkupRendered(widget) to a promise + let promises = tasks.map(({ id, func }) => ({ + id, + promise: new Promise(resolve => resolve(func())) + })); + + // Fire away and handle errors + const results = await Promise.all(promises.map(({ id, promise }) => + promise.catch(error => ({ error, id })) + )); + + const errors = results.filter(result => { + if (result && 'error' in result) { + return result.error instanceof Error + } }); + + if (errors.length > 0) { + errors.forEach(({ error, id }) => { + console.error(`Failed to load content for widget: ${id}, Error:`, error); + + const widget = $(`.widget-${id} > .widget-content > .panel-divider`); + widget.nextAll().remove() + widget.after(` +
+ +
+ Failed to load content +
+ `); + }); + } } // Executed for each widget; starts the widget-specific tick routine.