refactor: introduce model layer for centralized data synchronization
Add hoover model.js as a central reactive store per subsystem, replacing per-component data fetching with a single source of truth. - Add hoover/model.js with modelRegister, modelFetch, and WS invalidation - Refactor websocket.js to route messages to model refresh (drop per-component subscribe/unsubscribe) - Simplify component.js by removing WS subscription management - Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort - Rewrite all pages to use getModel() instead of inline data fetching - Bootstrap model registrations in app.js - Add GET /api/firewall/state endpoint - Fix restart-services.sh restart order and add service health verification - Update hoover.md docs with model layer architecture
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
* Layout components: PageHeader, Tabs, SectionTitle, DataTableSection, ActionGroup.
|
||||
*/
|
||||
|
||||
import { h } from '../vdom.js?v=6';
|
||||
import { Table } from './data.js?v=6';
|
||||
import { h } from '../vdom.js?v=7';
|
||||
import { Table } from './data.js?v=7';
|
||||
import { collectLoadingModels } from '../model.js?v=7';
|
||||
|
||||
/**
|
||||
* Page header with title, optional subtitle, and action buttons.
|
||||
@@ -25,14 +26,17 @@ export function PageHeader(props = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Handle loading/error/no-data states and return early if applicable.
|
||||
* Returns null when data is ready for the page to render its content.
|
||||
*
|
||||
* Accepts a model object (with loading/refreshing/error/data properties) as
|
||||
* the `data` parameter to check the model's data property directly.
|
||||
*
|
||||
* @param {object} state - Page state with loading/error flags
|
||||
* @param {string} title - Page header title
|
||||
* @param {string} [subtitle] - Page header subtitle
|
||||
* @param {*} [data] - Data presence check for "no data" state
|
||||
* @param {*} [data] - Data to check (or model object with .data property)
|
||||
* @returns {VNode[]|null}
|
||||
*/
|
||||
export function renderGuard(state, title, subtitle, data) {
|
||||
@@ -54,7 +58,7 @@ export function renderGuard(state, title, subtitle, data) {
|
||||
),
|
||||
];
|
||||
}
|
||||
if ((data === undefined || data === null) && !state.loading) {
|
||||
if (isEmpty(data) && !state.loading) {
|
||||
return [
|
||||
PageHeader({ title, subtitle }),
|
||||
h('div', { class: 'card', key: 'no-data' },
|
||||
@@ -65,6 +69,33 @@ export function renderGuard(state, title, subtitle, data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for pages consuming multiple models.
|
||||
* Internally calls collectLoadingModels then delegates to renderGuard.
|
||||
*
|
||||
* @param {string} title - Page header title
|
||||
* @param {string} [subtitle] - Page header subtitle
|
||||
* @param {...object} models - Model objects to combine
|
||||
* @returns {VNode[]|null}
|
||||
*/
|
||||
export function renderGuardMulti(title, subtitle, ...models) {
|
||||
const combined = collectLoadingModels(...models);
|
||||
return renderGuard(combined, title, subtitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is "empty" for renderGuard's no-data check.
|
||||
* @param {*} data
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEmpty(data) {
|
||||
if (data === null || data === undefined || data === '') return true;
|
||||
if (Array.isArray(data)) return data.length === 0;
|
||||
if (typeof data === 'object') return Object.keys(data).length === 0;
|
||||
if (typeof data === 'number') return false;
|
||||
return !data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab bar component. Writes to state[prop] on tab click.
|
||||
* The caller is responsible for rendering tab body content.
|
||||
|
||||
Reference in New Issue
Block a user