feat: auto-generate self-signed certs, hoover modal processing guard, refactor backends/proxy pages
This commit is contained in:
@@ -4,10 +4,16 @@
|
||||
* Data display components: Badge, StatusDot, Empty, Card.
|
||||
*/
|
||||
|
||||
import { h } from '../vdom.js?v=8';
|
||||
import { esc } from '../helpers.js?v=8';
|
||||
import { apiFetch, toast } from '../api.js?v=8';
|
||||
import { modelFetch } from '../model.js?v=8';
|
||||
import { h } from '../vdom.js?v=9';
|
||||
import { esc } from '../helpers.js?v=9';
|
||||
import { apiFetch, toast } from '../api.js?v=9';
|
||||
import { modelFetch } from '../model.js?v=9';
|
||||
import { requestUpdate } from '../reactivity.js?v=9';
|
||||
|
||||
const _actionPending = new Map();
|
||||
const _confirmPending = new Map();
|
||||
|
||||
export const _deleting = new Set();
|
||||
|
||||
/**
|
||||
* Colored badge/span.
|
||||
@@ -75,30 +81,59 @@ export function Card(props = {}) {
|
||||
* @param {string|string[]} [props.refresh] - Model name(s) to refresh via modelFetch
|
||||
* @param {string} [props.label] - Button text (default: 'Remove')
|
||||
* @param {object} [props.body] - Optional JSON body to send with DELETE
|
||||
* @param {string} [props.deleteKey] - Unique ID for pending-delete row styling
|
||||
*/
|
||||
export function ConfirmDelete(props = {}) {
|
||||
const opts = { method: 'DELETE' };
|
||||
if (props.body) opts.body = props.body;
|
||||
return h('button', { class: 'btn btn-sm btn-danger',
|
||||
const deleteKey = props.url + (props.body ? '::' + JSON.stringify(props.body) : '');
|
||||
const pending = _confirmPending.get(deleteKey) || false;
|
||||
|
||||
return h('button', {
|
||||
class: 'btn btn-sm btn-danger',
|
||||
disabled: pending,
|
||||
'on:click': async () => {
|
||||
if (!confirm(props.message)) return;
|
||||
const r = await apiFetch(props.url, opts);
|
||||
if (r.ok) {
|
||||
const synced = r.data?.synced;
|
||||
let msg = props.success || 'Removed';
|
||||
if (synced && synced.length) {
|
||||
msg += ' (auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
_confirmPending.set(deleteKey, true);
|
||||
requestUpdate();
|
||||
try {
|
||||
const r = await apiFetch(props.url, opts);
|
||||
if (r.ok) {
|
||||
const synced = r.data?.synced;
|
||||
let msg = props.success || 'Removed';
|
||||
if (synced && synced.length) {
|
||||
msg += ' (auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
}
|
||||
toast(msg, 'success');
|
||||
|
||||
if (props.deleteKey) {
|
||||
_deleting.add(props.deleteKey);
|
||||
}
|
||||
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
const promises = names.map(n => modelFetch(n));
|
||||
|
||||
if (props.deleteKey && promises.length) {
|
||||
Promise.all(promises).finally(() => {
|
||||
_deleting.delete(props.deleteKey);
|
||||
});
|
||||
}
|
||||
} else if (props.deleteKey) {
|
||||
// Cleanup dimming synchronously on DELETE completion rather than
|
||||
// relying on a heuristic timeout that breaks when tabs are throttled.
|
||||
_deleting.delete(props.deleteKey);
|
||||
}
|
||||
} else {
|
||||
toast(r.error || 'Failed', 'error');
|
||||
}
|
||||
toast(msg, 'success');
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(r.error || 'Failed', 'error');
|
||||
} finally {
|
||||
_confirmPending.delete(deleteKey);
|
||||
requestUpdate();
|
||||
}
|
||||
}}, props.label || 'Remove');
|
||||
}
|
||||
}, pending ? h('span', { class: 'btn-spinner' }) : (props.label || 'Remove'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,32 +163,42 @@ export function ActionButton(props = {}) {
|
||||
? (props.condition ? props.labelOn : props.labelOff)
|
||||
: 'Action');
|
||||
const cls = props.cls || 'btn btn-outline';
|
||||
const pending = _actionPending.get(props.url) || false;
|
||||
|
||||
return h('button', {
|
||||
class: cls,
|
||||
disabled: props.disabled,
|
||||
disabled: !!props.disabled || pending,
|
||||
'on:click': async () => {
|
||||
const body = props.body ? props.body() : undefined;
|
||||
const opts = { method: props.method || 'POST' };
|
||||
if (body !== undefined) opts.body = body;
|
||||
const resp = await apiFetch(props.url, opts);
|
||||
if (resp.ok) {
|
||||
const synced = resp.data?.synced;
|
||||
let msg = props.successMsg || '';
|
||||
if (synced && synced.length) {
|
||||
if (msg) msg += ' ';
|
||||
msg += '(auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
if (pending) return;
|
||||
_actionPending.set(props.url, true);
|
||||
requestUpdate();
|
||||
try {
|
||||
const body = props.body ? props.body() : undefined;
|
||||
const opts = { method: props.method || 'POST' };
|
||||
if (body !== undefined) opts.body = body;
|
||||
const resp = await apiFetch(props.url, opts);
|
||||
if (resp.ok) {
|
||||
const synced = resp.data?.synced;
|
||||
let msg = props.successMsg || '';
|
||||
if (synced && synced.length) {
|
||||
if (msg) msg += ' ';
|
||||
msg += '(auto-synced: ' + synced.join(', ') + ')';
|
||||
synced.forEach(s => modelFetch(s));
|
||||
}
|
||||
if (msg) toast(msg, 'success');
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(resp.error || 'Failed', props.errorType || 'error');
|
||||
}
|
||||
if (msg) toast(msg, 'success');
|
||||
if (props.refresh) {
|
||||
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
|
||||
names.forEach(n => modelFetch(n));
|
||||
}
|
||||
} else {
|
||||
toast(resp.error || 'Failed', props.errorType || 'error');
|
||||
} finally {
|
||||
_actionPending.delete(props.url);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
}, label);
|
||||
}, pending ? h('span', { class: 'btn-spinner' }) : label);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +324,7 @@ export function ServiceStatus(props = {}) {
|
||||
* @param {string} [props.removeLabel] - Delete button label (default: 'Remove')
|
||||
* @param {object} [props.removeBody] - Optional JSON body to send with DELETE
|
||||
* @param {string} [props.editCls] - Override classes for edit button (default: 'btn btn-sm btn-outline')
|
||||
* @param {string} [props.deleteKey] - Unique ID forwarded to ConfirmDelete for pending-delete styling
|
||||
*/
|
||||
export function ActionCell(props = {}) {
|
||||
return h('td', null,
|
||||
@@ -294,6 +340,7 @@ export function ActionCell(props = {}) {
|
||||
refresh: props.removeRefresh,
|
||||
label: props.removeLabel || 'Remove',
|
||||
body: props.removeBody,
|
||||
deleteKey: props.deleteKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user