mirror of
https://github.com/vee1e/krply.git
synced 2026-09-01 09:49:51 +00:00
feat(web): prefill demo defaults so views load instantly
- timeline picks the first cluster, stream, and a real observed object - diff defaults to the first cluster and busiest namespace - plans default to the first cluster, latest snapshot, and a source/target namespace pair, bootstrapping one plan when none exist
This commit is contained in:
parent
fbd7880d87
commit
0baf78ddb7
4 changed files with 151 additions and 29 deletions
46
web/src/defaults.js
Normal file
46
web/src/defaults.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// defaults.js - demo-friendly defaults so views work with one click (or none).
|
||||
// Each picker falls back to "" so the form still validates when data is absent.
|
||||
|
||||
export function pickFirst(items) {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
return list.length ? list[0] : null;
|
||||
}
|
||||
|
||||
// pickLatestSnapshot returns the snapshot with the newest observed time.
|
||||
export function pickLatestSnapshot(snaps) {
|
||||
let best = null;
|
||||
for (const s of Array.isArray(snaps) ? snaps : []) {
|
||||
const t = s.at ? new Date(s.at).getTime() : 0;
|
||||
if (!best || t > new Date(best.at).getTime()) best = s;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// pickNamespace returns the most common namespace across streams.
|
||||
export function pickNamespace(streams) {
|
||||
const counts = new Map();
|
||||
for (const st of Array.isArray(streams) ? streams : []) {
|
||||
if (!st.namespace) continue;
|
||||
counts.set(st.namespace, (counts.get(st.namespace) || 0) + 1);
|
||||
}
|
||||
let best = '';
|
||||
let bestCount = 0;
|
||||
for (const [ns, c] of counts) {
|
||||
if (c > bestCount) { best = ns; bestCount = c; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// pickObject returns the first observed object {namespace, name}.
|
||||
export function pickObject(events) {
|
||||
for (const ev of Array.isArray(events) ? events : []) {
|
||||
if (ev.namespace && ev.name) return { namespace: ev.namespace, name: ev.name };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// setSelect assigns a value and fires change so dependent options rebuild.
|
||||
export function setSelect(sel, value) {
|
||||
sel.value = value || '';
|
||||
sel.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
// list every path with added/removed/modified badges.
|
||||
import { api } from '../api.js';
|
||||
import { el, asList, badge, fmtTime, field, formCard } from '../util.js';
|
||||
import { pickFirst, pickNamespace, setSelect } from '../defaults.js';
|
||||
|
||||
const toLocalInput = (d) => {
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
|
|
@ -12,7 +13,9 @@ const toLocalInput = (d) => {
|
|||
|
||||
export async function viewDiff(mount) {
|
||||
mount.appendChild(el('h1', { textContent: 'JSON diff' }));
|
||||
const clusters = asList(await api.clusters().catch(() => []));
|
||||
const [c, s] = await Promise.all([api.clusters().catch(() => []), api.streams().catch(() => [])]);
|
||||
const clusters = asList(c);
|
||||
const streams = asList(s);
|
||||
|
||||
const clusterSel = el('select');
|
||||
clusterSel.appendChild(el('option', { value: '', textContent: '— cluster —' }));
|
||||
|
|
@ -22,13 +25,14 @@ export async function viewDiff(mount) {
|
|||
const sinceInput = el('input', { type: 'datetime-local', step: '1', value: toLocalInput(new Date(Date.now() - 3600e3)) });
|
||||
const untilInput = el('input', { type: 'datetime-local', step: '1', value: toLocalInput(new Date()) });
|
||||
|
||||
const defCluster = pickFirst(clusters);
|
||||
const defNs = pickNamespace(streams);
|
||||
if (defCluster) setSelect(clusterSel, defCluster.id);
|
||||
if (defNs) namespaceInput.value = defNs;
|
||||
|
||||
const results = el('div');
|
||||
mount.appendChild(formCard([
|
||||
field('cluster', clusterSel),
|
||||
field('namespace', namespaceInput),
|
||||
field('before (observed)', sinceInput),
|
||||
field('after (observed)', untilInput),
|
||||
], 'show diff', async () => {
|
||||
|
||||
async function run() {
|
||||
results.textContent = '';
|
||||
if (!clusterSel.value || !namespaceInput.value.trim() || !sinceInput.value || !untilInput.value) {
|
||||
results.appendChild(el('div', { className: 'notice warn', textContent: 'cluster, namespace, before and after are required.' }));
|
||||
|
|
@ -49,8 +53,17 @@ export async function viewDiff(mount) {
|
|||
results.textContent = '';
|
||||
results.appendChild(el('div', { className: 'notice error', textContent: err.message || String(err) }));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
mount.appendChild(formCard([
|
||||
field('cluster', clusterSel),
|
||||
field('namespace', namespaceInput),
|
||||
field('before (observed)', sinceInput),
|
||||
field('after (observed)', untilInput),
|
||||
], 'show diff', run));
|
||||
mount.appendChild(results);
|
||||
|
||||
run();
|
||||
}
|
||||
|
||||
function renderDiff(results, data) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
// Replay plans: create (POST /v1/replay-plans), review objects/exclusions, dry-run.
|
||||
import { api } from '../api.js';
|
||||
import { el, asList, simpleTable, badge, fmtTime, field, formCard } from '../util.js';
|
||||
import { pickFirst, pickLatestSnapshot, pickNamespace, setSelect } from '../defaults.js';
|
||||
|
||||
export async function viewPlans(mount) {
|
||||
mount.appendChild(el('h1', { textContent: 'Replay plans' }));
|
||||
|
||||
const [clusters, snaps] = await Promise.all([api.clusters().catch(() => []), api.snapshots().catch(() => [])]);
|
||||
const [clusters, snaps, streams] = await Promise.all([
|
||||
api.clusters().catch(() => []),
|
||||
api.snapshots().catch(() => []),
|
||||
api.streams().catch(() => []),
|
||||
]);
|
||||
const clusterList = asList(clusters);
|
||||
const snapshotList = asList(snaps);
|
||||
const streamList = asList(streams);
|
||||
|
||||
const clusterSel = el('select');
|
||||
clusterSel.appendChild(el('option', { value: '', textContent: '— cluster —' }));
|
||||
|
|
@ -29,13 +35,39 @@ export async function viewPlans(mount) {
|
|||
const sourceInput = el('input', { type: 'text', placeholder: 'source namespace' });
|
||||
const targetInput = el('input', { type: 'text', placeholder: 'target namespace' });
|
||||
const list = el('div', { className: 'plan-list' });
|
||||
let bootstrapped = false;
|
||||
|
||||
const defCluster = pickFirst(clusterList);
|
||||
const defSnapshot = pickLatestSnapshot(snapshotList);
|
||||
const defNs = pickNamespace(streamList);
|
||||
if (defCluster) setSelect(clusterSel, defCluster.id);
|
||||
fillSnapshots();
|
||||
if (defSnapshot && (!clusterSel.value || defSnapshot.cluster_id === clusterSel.value)) snapshotSel.value = defSnapshot.id;
|
||||
if (defNs) {
|
||||
sourceInput.value = defNs;
|
||||
targetInput.value = defNs ? `${defNs}-copy` : '';
|
||||
}
|
||||
|
||||
async function create(body) {
|
||||
list.textContent = '';
|
||||
list.appendChild(el('p', { className: 'muted', textContent: 'creating plan…' }));
|
||||
try {
|
||||
const plan = await api.createPlan(body);
|
||||
list.textContent = '';
|
||||
if (plan && plan.id) list.appendChild(planCard(plan));
|
||||
await load();
|
||||
} catch (err) {
|
||||
list.textContent = '';
|
||||
list.appendChild(el('div', { className: 'notice error', textContent: err.message || String(err) }));
|
||||
}
|
||||
}
|
||||
|
||||
mount.appendChild(formCard([
|
||||
field('cluster', clusterSel),
|
||||
field('snapshot', snapshotSel),
|
||||
field('source namespace', sourceInput),
|
||||
field('target namespace', targetInput),
|
||||
], 'create plan', async () => {
|
||||
], 'create plan', () => {
|
||||
list.textContent = '';
|
||||
const body = {
|
||||
cluster_id: clusterSel.value,
|
||||
|
|
@ -47,16 +79,7 @@ export async function viewPlans(mount) {
|
|||
list.appendChild(el('div', { className: 'notice warn', textContent: 'cluster, snapshot, source and target namespace are required.' }));
|
||||
return;
|
||||
}
|
||||
list.appendChild(el('p', { className: 'muted', textContent: 'creating plan…' }));
|
||||
try {
|
||||
const plan = await api.createPlan(body);
|
||||
list.textContent = '';
|
||||
if (plan && plan.id) list.appendChild(planCard(plan));
|
||||
await load();
|
||||
} catch (err) {
|
||||
list.textContent = '';
|
||||
list.appendChild(el('div', { className: 'notice error', textContent: err.message || String(err) }));
|
||||
}
|
||||
create(body);
|
||||
}));
|
||||
mount.appendChild(list);
|
||||
|
||||
|
|
@ -68,6 +91,7 @@ export async function viewPlans(mount) {
|
|||
list.textContent = '';
|
||||
if (!plans.length) {
|
||||
list.appendChild(el('p', { className: 'muted', textContent: 'No plans yet — create one above.' }));
|
||||
bootstrapIfEmpty();
|
||||
return;
|
||||
}
|
||||
for (const p of plans) list.appendChild(planCard(p));
|
||||
|
|
@ -77,6 +101,23 @@ export async function viewPlans(mount) {
|
|||
}
|
||||
}
|
||||
await load();
|
||||
|
||||
// Demo bootstrap: with a real cluster + snapshot, auto-create one plan so a
|
||||
// visitor sees a populated page without filling the form.
|
||||
async function bootstrapIfEmpty() {
|
||||
if (bootstrapped) return;
|
||||
bootstrapped = true;
|
||||
const body = {
|
||||
cluster_id: clusterSel.value,
|
||||
snapshot_id: snapshotSel.value,
|
||||
source_namespace: sourceInput.value.trim(),
|
||||
target_namespace: targetInput.value.trim(),
|
||||
};
|
||||
if (!body.cluster_id || !body.snapshot_id || !body.source_namespace || !body.target_namespace) return;
|
||||
const plans = asList(await api.plans().catch(() => []));
|
||||
if (plans.length) return;
|
||||
create(body);
|
||||
}
|
||||
}
|
||||
|
||||
const itemName = (x) => (x.namespace ? `${x.namespace}/${x.name}` : x.name);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
// Object timeline: observed events for one object, with GAP rows shown distinctly.
|
||||
import { api } from '../api.js';
|
||||
import { el, asList, badge, watchBadge, fmtTime, streamLabel, streamTitle, field, formCard } from '../util.js';
|
||||
import { pickFirst, pickObject, setSelect } from '../defaults.js';
|
||||
|
||||
export async function viewTimeline(mount) {
|
||||
mount.appendChild(el('h1', { textContent: 'Object timeline' }));
|
||||
const [clusters, streams] = await Promise.all([api.clusters().catch(() => []), api.streams().catch(() => [])]);
|
||||
const [clusters, streams, evs] = await Promise.all([
|
||||
api.clusters().catch(() => []),
|
||||
api.streams().catch(() => []),
|
||||
api.events({ limit: 30 }).catch(() => []),
|
||||
]);
|
||||
const clusterList = asList(clusters);
|
||||
const streamList = asList(streams);
|
||||
const events = asList(evs);
|
||||
|
||||
const clusterSel = el('select');
|
||||
clusterSel.appendChild(el('option', { value: '', textContent: '— cluster —' }));
|
||||
|
|
@ -29,14 +35,20 @@ export async function viewTimeline(mount) {
|
|||
const nameInput = el('input', { type: 'text', placeholder: 'name' });
|
||||
const sinceInput = el('input', { type: 'number', min: '1', max: '8760', value: '24' });
|
||||
|
||||
const defCluster = pickFirst(clusterList);
|
||||
const defObject = pickObject(events);
|
||||
if (defCluster) setSelect(clusterSel, defCluster.id);
|
||||
fillStreams();
|
||||
const defStream = streamList.find((st) => st.cluster_id === (defCluster && defCluster.id)) || pickFirst(streamList);
|
||||
if (defStream) streamSel.value = defStream.id;
|
||||
if (defObject) {
|
||||
namespaceInput.value = defObject.namespace;
|
||||
nameInput.value = defObject.name;
|
||||
}
|
||||
|
||||
const results = el('div');
|
||||
mount.appendChild(formCard([
|
||||
field('cluster', clusterSel),
|
||||
field('stream', streamSel),
|
||||
field('namespace', namespaceInput),
|
||||
field('name', nameInput),
|
||||
field('since (hours)', sinceInput),
|
||||
], 'show timeline', async () => {
|
||||
|
||||
async function run() {
|
||||
results.textContent = '';
|
||||
const cluster = clusterSel.value;
|
||||
const stream = streamSel.value;
|
||||
|
|
@ -57,8 +69,18 @@ export async function viewTimeline(mount) {
|
|||
results.textContent = '';
|
||||
results.appendChild(el('div', { className: 'notice error', textContent: err.message || String(err) }));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
mount.appendChild(formCard([
|
||||
field('cluster', clusterSel),
|
||||
field('stream', streamSel),
|
||||
field('namespace', namespaceInput),
|
||||
field('name', nameInput),
|
||||
field('since (hours)', sinceInput),
|
||||
], 'show timeline', run));
|
||||
mount.appendChild(results);
|
||||
|
||||
run();
|
||||
}
|
||||
|
||||
function renderHistory(results, data) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue