update: status page, api
This commit is contained in:
@@ -146,6 +146,69 @@ async function getAccountId(cookie: any, headers: any): Promise<{ accountId: str
|
||||
return await resolveKey(key) ?? null;
|
||||
}
|
||||
|
||||
// Parse the status page edit form's monitor list. The form posts:
|
||||
// monitor_order — full list of monitor IDs in DOM order (every row, not just checked)
|
||||
// monitor_ids — only the *checked* IDs, also in DOM order
|
||||
// display_name[<id>] — optional per-page name override
|
||||
// display_mode[<id>] — '', 'compact', or 'expanded'
|
||||
// Bun's body parser surfaces bracket-keyed fields either as nested objects
|
||||
// (`b.display_name = { id: value }`) or as flat string keys
|
||||
// (`b['display_name[id]'] = value`) depending on parser version, so handle both.
|
||||
function pickMap(b: any, prefix: string): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (b[prefix] && typeof b[prefix] === "object" && !Array.isArray(b[prefix])) {
|
||||
for (const [k, v] of Object.entries(b[prefix])) {
|
||||
if (typeof v === "string" && v.trim()) out[k] = v.trim();
|
||||
}
|
||||
} else {
|
||||
const re = new RegExp(`^${prefix}\\[(.+)\\]$`);
|
||||
for (const k of Object.keys(b)) {
|
||||
const m = k.match(re);
|
||||
if (m && typeof b[k] === "string" && b[k].trim()) out[m[1]!] = b[k].trim();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseStatusPageMonitors(b: any): {
|
||||
monitorIds: string[];
|
||||
monitorsForApi: Array<{ monitor_id: string; position: number; display_name: string | null; display_mode: string | null }>;
|
||||
} {
|
||||
const order: string[] = Array.isArray(b.monitor_order) ? b.monitor_order : (b.monitor_order ? [b.monitor_order] : []);
|
||||
const checked: string[] = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
|
||||
const checkedSet = new Set(checked);
|
||||
const displayNames = pickMap(b, "display_name");
|
||||
const displayModes = pickMap(b, "display_mode");
|
||||
|
||||
// Walk the rendered order and keep only the checked monitors. Position is
|
||||
// their index in this filtered list.
|
||||
const monitorIds: string[] = [];
|
||||
const monitorsForApi: Array<{ monitor_id: string; position: number; display_name: string | null; display_mode: string | null }> = [];
|
||||
for (const id of order) {
|
||||
if (!checkedSet.has(id)) continue;
|
||||
monitorsForApi.push({
|
||||
monitor_id: id,
|
||||
position: monitorIds.length,
|
||||
display_name: displayNames[id] ?? null,
|
||||
display_mode: (displayModes[id] === "compact" || displayModes[id] === "expanded") ? displayModes[id]! : null,
|
||||
});
|
||||
monitorIds.push(id);
|
||||
}
|
||||
// If the form somehow posted a checked ID that wasn't in the order list
|
||||
// (shouldn't happen, defensive), append it at the end.
|
||||
for (const id of checked) {
|
||||
if (monitorIds.includes(id)) continue;
|
||||
monitorsForApi.push({
|
||||
monitor_id: id,
|
||||
position: monitorIds.length,
|
||||
display_name: displayNames[id] ?? null,
|
||||
display_mode: (displayModes[id] === "compact" || displayModes[id] === "expanded") ? displayModes[id]! : null,
|
||||
});
|
||||
monitorIds.push(id);
|
||||
}
|
||||
return { monitorIds, monitorsForApi };
|
||||
}
|
||||
|
||||
const dashDir = resolve(import.meta.dir, "../dashboard");
|
||||
|
||||
export const dashboard = new Elysia()
|
||||
@@ -636,7 +699,9 @@ export const dashboard = new Elysia()
|
||||
`;
|
||||
if (!page) return redirect("/dashboard/status-pages");
|
||||
const monitors = await sql`
|
||||
SELECT monitor_id, display_name FROM status_page_monitors WHERE status_page_id = ${params.id}
|
||||
SELECT monitor_id, display_name, display_mode
|
||||
FROM status_page_monitors WHERE status_page_id = ${params.id}
|
||||
ORDER BY position ASC
|
||||
`;
|
||||
const allMonitors = await sql`
|
||||
SELECT id, name FROM monitors WHERE account_id = ${resolved.accountId} ORDER BY created_at DESC
|
||||
@@ -649,22 +714,7 @@ export const dashboard = new Elysia()
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
const b = body as any;
|
||||
const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
|
||||
// The form posts per-monitor display name overrides as display_name[<id>].
|
||||
// Bun's body parser surfaces them as nested object keys (display_name) or
|
||||
// as flat "display_name[id]" string keys depending on parser version, so
|
||||
// handle both. Empty strings mean "no override" — don't send them.
|
||||
const displayNames: Record<string, string> = {};
|
||||
if (b.display_name && typeof b.display_name === "object" && !Array.isArray(b.display_name)) {
|
||||
for (const [k, v] of Object.entries(b.display_name)) {
|
||||
if (typeof v === "string" && v.trim()) displayNames[k] = v.trim();
|
||||
}
|
||||
} else {
|
||||
for (const k of Object.keys(b)) {
|
||||
const m = k.match(/^display_name\[(.+)\]$/);
|
||||
if (m && typeof b[k] === "string" && b[k].trim()) displayNames[m[1]!] = b[k].trim();
|
||||
}
|
||||
}
|
||||
const { monitorsForApi } = parseStatusPageMonitors(b);
|
||||
try {
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
@@ -684,7 +734,7 @@ export const dashboard = new Elysia()
|
||||
password: b.password || undefined,
|
||||
custom_css: b.custom_css || null,
|
||||
footer_text: b.footer_text || null,
|
||||
monitors: monitorIds.map((id: string, i: number) => ({ monitor_id: id, position: i, display_name: displayNames[id] ?? null })),
|
||||
monitors: monitorsForApi,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
@@ -695,18 +745,7 @@ export const dashboard = new Elysia()
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
const b = body as any;
|
||||
const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
|
||||
const displayNames: Record<string, string> = {};
|
||||
if (b.display_name && typeof b.display_name === "object" && !Array.isArray(b.display_name)) {
|
||||
for (const [k, v] of Object.entries(b.display_name)) {
|
||||
if (typeof v === "string" && v.trim()) displayNames[k] = v.trim();
|
||||
}
|
||||
} else {
|
||||
for (const k of Object.keys(b)) {
|
||||
const m = k.match(/^display_name\[(.+)\]$/);
|
||||
if (m && typeof b[k] === "string" && b[k].trim()) displayNames[m[1]!] = b[k].trim();
|
||||
}
|
||||
}
|
||||
const { monitorsForApi } = parseStatusPageMonitors(b);
|
||||
try {
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
@@ -722,7 +761,7 @@ export const dashboard = new Elysia()
|
||||
index_search: !!b.index_search,
|
||||
custom_css: b.custom_css || null,
|
||||
footer_text: b.footer_text || null,
|
||||
monitors: monitorIds.map((id: string, i: number) => ({ monitor_id: id, position: i, display_name: displayNames[id] ?? null })),
|
||||
monitors: monitorsForApi,
|
||||
};
|
||||
// Only send `password` if the user actually typed something. An empty box
|
||||
// means "leave the existing password as-is" — sending null would clear it.
|
||||
|
||||
@@ -6,9 +6,22 @@
|
||||
const allMonitors = it.allMonitors || [];
|
||||
const attachedRows = (it.page?.monitors || []);
|
||||
const attached = new Set(attachedRows.map(m => m.monitor_id));
|
||||
// monitor_id → existing display_name override (or empty)
|
||||
// monitor_id → existing per-page overrides
|
||||
const displayNames = {};
|
||||
for (const r of attachedRows) displayNames[r.monitor_id] = r.display_name || '';
|
||||
const displayModes = {};
|
||||
for (const r of attachedRows) {
|
||||
displayNames[r.monitor_id] = r.display_name || '';
|
||||
displayModes[r.monitor_id] = r.display_mode || '';
|
||||
}
|
||||
// Render order: attached monitors first in their saved order, then any
|
||||
// unattached ones (alphabetical) so the user can drag them in.
|
||||
const attachedOrder = attachedRows
|
||||
.map(r => allMonitors.find(m => m.id === r.monitor_id))
|
||||
.filter(Boolean);
|
||||
const unattached = allMonitors
|
||||
.filter(m => !attached.has(m.id))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const orderedMonitors = [...attachedOrder, ...unattached];
|
||||
%>
|
||||
|
||||
<main class="max-w-3xl mx-auto px-8 py-10">
|
||||
@@ -68,25 +81,62 @@
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Monitors</label>
|
||||
<p class="text-xs text-gray-600 mb-2">Optional "Show as" field overrides the monitor name on this status page only. Leave blank to use the monitor's real name.</p>
|
||||
<p class="text-xs text-gray-600 mb-2">Tick to attach. Drag to reorder. "Show as" overrides the name on this page only. "Mode" picks compact or expanded for that one monitor (or leave blank to use the page default).</p>
|
||||
<% if (allMonitors.length === 0) { %>
|
||||
<p class="text-xs text-gray-600">No monitors yet. <a href="/dashboard/monitors/new" class="text-blue-400 hover:text-blue-300">Create one</a> first.</p>
|
||||
<% } else { %>
|
||||
<div class="space-y-2">
|
||||
<% allMonitors.forEach(function(m) {
|
||||
<div id="monitor-list" class="space-y-2">
|
||||
<% orderedMonitors.forEach(function(m) {
|
||||
const isAttached = attached.has(m.id);
|
||||
const displayName = displayNames[m.id] || '';
|
||||
const displayMode = displayModes[m.id] || '';
|
||||
%>
|
||||
<div class="flex items-center gap-3 bg-gray-900 border border-gray-800 rounded-lg px-3 py-2">
|
||||
<div class="monitor-edit-row flex items-center gap-3 bg-gray-900 border border-gray-800 rounded-lg px-3 py-2" draggable="true" data-monitor-id="<%= m.id %>">
|
||||
<span class="drag-handle text-gray-600 cursor-grab select-none px-1" title="Drag to reorder">⋮⋮</span>
|
||||
<input type="hidden" name="monitor_order" value="<%= m.id %>">
|
||||
<label class="flex items-center gap-2 cursor-pointer min-w-0 flex-1">
|
||||
<input type="checkbox" name="monitor_ids" value="<%= m.id %>" class="accent-blue-500" <%= isAttached ? 'checked' : '' %>>
|
||||
<span class="text-sm text-gray-300 truncate"><%= m.name %></span>
|
||||
</label>
|
||||
<select name="display_mode[<%= m.id %>]"
|
||||
class="text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-200 focus:outline-none focus:border-blue-500 shrink-0">
|
||||
<option value="" <%= displayMode === '' ? 'selected' : '' %>>Default</option>
|
||||
<option value="expanded" <%= displayMode === 'expanded' ? 'selected' : '' %>>Expanded</option>
|
||||
<option value="compact" <%= displayMode === 'compact' ? 'selected' : '' %>>Compact</option>
|
||||
</select>
|
||||
<input type="text" name="display_name[<%= m.id %>]" value="<%= displayName %>" placeholder="Show as (optional)"
|
||||
class="text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500 w-48 shrink-0">
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
<script>
|
||||
// Tiny vanilla drag-and-drop reorder. The DOM order at submit time is
|
||||
// the canonical order — each row carries a hidden "monitor_order" input
|
||||
// that gets posted in DOM order naturally.
|
||||
(function() {
|
||||
const list = document.getElementById('monitor-list');
|
||||
if (!list) return;
|
||||
let dragging = null;
|
||||
list.querySelectorAll('.monitor-edit-row').forEach((row) => {
|
||||
row.addEventListener('dragstart', (e) => {
|
||||
dragging = row;
|
||||
row.style.opacity = '0.4';
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
row.addEventListener('dragend', () => {
|
||||
row.style.opacity = '';
|
||||
dragging = null;
|
||||
});
|
||||
row.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || dragging === row) return;
|
||||
const rect = row.getBoundingClientRect();
|
||||
const after = (e.clientY - rect.top) > rect.height / 2;
|
||||
row.parentNode.insertBefore(dragging, after ? row.nextSibling : row);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user