633505e7dc
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
/**
|
|
* Hoover — components/layout.js
|
|
*
|
|
* Layout components: PageHeader, Tabs, SectionTitle, DataTableSection, ActionGroup.
|
|
*/
|
|
|
|
import { h } from '../vdom.js?v=6';
|
|
import { Table } from './data.js?v=6';
|
|
|
|
/**
|
|
* Page header with title, optional subtitle, and action buttons.
|
|
*
|
|
* @param {object} props
|
|
* @param {string} props.title
|
|
* @param {string} [props.subtitle]
|
|
* @param {VNode} [props.actions]
|
|
*/
|
|
export function PageHeader(props = {}) {
|
|
return h('div', { class: 'page-header' },
|
|
h('div', null,
|
|
h('h1', null, props.title || ''),
|
|
props.subtitle ? h('div', { class: 'subtitle' }, props.subtitle) : null,
|
|
),
|
|
props.actions ? h('div', { class: 'page-actions' }, props.actions) : null,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle loading/error/no-data states and return early if applicable.
|
|
* Returns null when data is ready for the page to render its content.
|
|
*
|
|
* @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
|
|
* @returns {VNode[]|null}
|
|
*/
|
|
export function renderGuard(state, title, subtitle, data) {
|
|
if (state.loading && !state.refreshing) {
|
|
return [
|
|
PageHeader({ title, subtitle }),
|
|
h('div', { class: 'card', key: 'loading' },
|
|
h('div', { class: 'card-body loading' },
|
|
state.refreshing ? 'Refreshing...' : 'Loading...',
|
|
),
|
|
),
|
|
];
|
|
}
|
|
if (state.error) {
|
|
return [
|
|
PageHeader({ title, subtitle }),
|
|
h('div', { class: 'card', key: 'error' },
|
|
h('div', { class: 'card-body error-msg' }, state.error),
|
|
),
|
|
];
|
|
}
|
|
if ((data === undefined || data === null) && !state.loading) {
|
|
return [
|
|
PageHeader({ title, subtitle }),
|
|
h('div', { class: 'card', key: 'no-data' },
|
|
h('div', { class: 'card-body loading' }, 'No data available'),
|
|
),
|
|
];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Tab bar component. Writes to state[prop] on tab click.
|
|
* The caller is responsible for rendering tab body content.
|
|
*
|
|
* @param {object} props
|
|
* @param {object} props.state - Reactive state object
|
|
* @param {string[]} props.tabs - Array of tab keys (e.g. ['ranges', 'leases'])
|
|
* @param {string} [props.prop] - State property name for active tab (default: 'activeTab')
|
|
* @param {function} [props.formatLabel] - (key) => label string (default: capitalize)
|
|
* @param {function} [props.onTabClick] - (key) => void, called after state update (for async side effects)
|
|
*/
|
|
export function Tabs(props = {}) {
|
|
const tabKeys = props.tabs || [];
|
|
const prop = props.prop || 'activeTab';
|
|
const formatLabel = props.formatLabel || ((k) => k.charAt(0).toUpperCase() + k.slice(1));
|
|
return h('div', { class: 'tabs' },
|
|
tabKeys.map(t => h('span', {
|
|
class: 'tab ' + (props.state[prop] === t ? 'active' : ''),
|
|
'on:click': () => {
|
|
props.state[prop] = t;
|
|
if (props.onTabClick) props.onTabClick(t);
|
|
},
|
|
style: 'cursor:pointer;',
|
|
}, formatLabel(t))),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Section header.
|
|
*
|
|
* @param {object} props
|
|
* @param {string} props.title
|
|
*/
|
|
export function SectionTitle(props = {}) {
|
|
return h('h3', { class: 'section-title' }, props.title);
|
|
}
|
|
|
|
/**
|
|
* Flex button container with 8px gap.
|
|
*
|
|
* @param {VNode[]} children
|
|
*/
|
|
export function ActionGroup(...children) {
|
|
return h('div', { style: 'display:flex;gap:8px;' }, ...children);
|
|
}
|
|
|
|
/**
|
|
* DataTableSection — SectionTitle heading followed by a Table.
|
|
*
|
|
* @param {object} props
|
|
* @param {string} props.title - Section heading
|
|
* @param {string[]} props.columns
|
|
* @param {VNode[]} props.rows
|
|
* @param {string} [props.emptyText]
|
|
* @param {string} [props.key]
|
|
*/
|
|
export function DataTableSection(props = {}) {
|
|
const key = props.key !== undefined ? { key: props.key } : {};
|
|
return h('div', { class: 'data-table-section', ...key },
|
|
SectionTitle({ title: props.title }),
|
|
Table({
|
|
columns: props.columns,
|
|
rows: props.rows,
|
|
emptyText: props.emptyText,
|
|
}),
|
|
);
|
|
}
|