Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
</header>
<main class="content">
<section id="config" class="config-wrap"></section>
<section id="config-editor" class="cfg-editor-wrap" hidden>
<div class="cfg-editor-head">
<span class="cfg-label">edit fleets.toml</span>
<span class="cfg-editor-path" id="cfg-editor-path"></span>
<span class="cfg-editor-spacer"></span>
<button class="cfg-btn" id="cfg-save" type="button">Save</button>
<button class="cfg-btn cfg-btn-ghost" id="cfg-cancel" type="button">Cancel</button>
</div>
<div id="cfg-editor-mount" class="cfg-editor-mount"></div>
<div class="cfg-editor-error" id="cfg-editor-error" hidden></div>
</section>
<section id="identity" class="identity-wrap"></section>
<section class="logs">
<nav class="tabs" id="tabs">
Expand Down
160 changes: 160 additions & 0 deletions console/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@
"typescript": "^5.6.3",
"vite": "^6.0.7",
"vitest": "^2.1.8"
},
"dependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/legacy-modes": "^6.5.3",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.8",
"codemirror": "^6.0.2"
}
}
15 changes: 15 additions & 0 deletions console/src/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,19 @@ export const FIXTURE_FLEET_CONFIG: FleetConfig = {
expected_principal: null,
},
],
text: `# OAB Studio fleet bindings — which credential manages which fleet.

[[fleet]]
name = "prod"
cluster = "oab"
region = "ap-east-2"
profile = "orca-prod"
expected_principal = "arn:aws:iam::504190915686:role/openab-orca-task-role"

[[fleet]]
name = "staging"
cluster = "oab-staging"
region = "ap-southeast-1"
profile = "orca-staging"
`,
};
78 changes: 76 additions & 2 deletions console/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { defaultSource } from "./source";
import { renderRoster, renderIdentity, renderFleetConfig } from "./render";
import type { FleetConfig } from "./types";
import { createPane, bindBackend, type Level } from "./log";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { StreamLanguage } from "@codemirror/language";
import { toml } from "@codemirror/legacy-modes/mode/toml";

const POLL_MS = 5000;
const DEFAULT_CLUSTER = "oab";
Expand All @@ -15,6 +19,12 @@ let fleetConfig: FleetConfig | null = null;
const roster = document.getElementById("roster");
const identityEl = document.getElementById("identity");
const configEl = document.getElementById("config");
const editorSection = document.getElementById("config-editor");
const editorMount = document.getElementById("cfg-editor-mount");
const editorError = document.getElementById("cfg-editor-error");
const editorPathEl = document.getElementById("cfg-editor-path");
const saveBtn = document.getElementById("cfg-save") as HTMLButtonElement | null;
const cancelBtn = document.getElementById("cfg-cancel") as HTMLButtonElement | null;
const clusterLabel = document.getElementById("cluster-label");
const pollStatus = document.getElementById("poll-status");
const logEl = document.getElementById("log");
Expand Down Expand Up @@ -138,10 +148,74 @@ function selectCluster(cluster: string): void {
void tick();
}

// One delegated listener: a click on any fleet button switches to its cluster.
// ---- fleets.toml editor (ADR #19 slice C: the "edit" side) -------------------
// A CodeMirror TOML editor over the raw config file. Kept imperative (CM owns
// real DOM) and separate from the re-rendered config panel, so switching fleets
// never wipes an open editor.
let editorView: EditorView | null = null;

function showEditorError(msg: string | null): void {
if (!editorError) return;
editorError.textContent = msg ?? "";
editorError.hidden = !msg;
}

function openEditor(): void {
if (!editorSection || !editorMount) return;
showEditorError(null);
if (editorPathEl) editorPathEl.textContent = fleetConfig?.path ?? "";
editorView?.destroy();
editorView = new EditorView({
parent: editorMount,
state: EditorState.create({
doc: fleetConfig?.text ?? "",
extensions: [basicSetup, StreamLanguage.define(toml)],
}),
});
editorSection.hidden = false;
editorView.focus();
}

function closeEditor(): void {
editorView?.destroy();
editorView = null;
if (editorSection) editorSection.hidden = true;
showEditorError(null);
}

async function saveEditor(): Promise<void> {
if (!editorView || !saveBtn) return;
const text = editorView.state.doc.toString();
saveBtn.disabled = true;
showEditorError(null);
try {
// The backend validates the TOML and rejects (without writing) on error.
fleetConfig = await source.writeFleetConfig(text);
if (configEl) renderFleetConfig(configEl, fleetConfig, activeCluster);
note("info", "fleet config saved");
closeEditor();
// A binding change may alter the active fleet's credential — re-observe.
void refreshIdentity();
} catch (e) {
showEditorError(`save failed — ${errText(e)}`);
} finally {
saveBtn.disabled = false;
}
}

saveBtn?.addEventListener("click", () => void saveEditor());
cancelBtn?.addEventListener("click", () => closeEditor());

// One delegated listener on the config panel: "Edit config" opens the editor;
// a click on any fleet button switches to its cluster.
if (configEl) {
configEl.addEventListener("click", (ev) => {
const btn = (ev.target as HTMLElement).closest<HTMLElement>("[data-cluster]");
const target = ev.target as HTMLElement;
if (target.closest('[data-action="edit-config"]')) {
openEditor();
return;
}
const btn = target.closest<HTMLElement>("[data-cluster]");
if (btn?.dataset.cluster) selectCluster(btn.dataset.cluster);
});
}
Expand Down
13 changes: 12 additions & 1 deletion console/src/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,25 @@ describe("fleetConfigHtml", () => {

it("renders an empty state with the config path when no fleets", () => {
const html = fleetConfigHtml(
{ path: "~/.config/oab-studio/fleets.toml", default_cluster: "oab", fleets: [] },
{
path: "~/.config/oab-studio/fleets.toml",
default_cluster: "oab",
fleets: [],
text: "",
},
"oab",
);
expect(html).toContain("No fleets configured");
expect(html).toContain("fleets.toml");
expect(html).not.toContain("cfg-fleet");
});

it("always offers the Edit config action (even with fleets)", () => {
expect(fleetConfigHtml(FIXTURE_FLEET_CONFIG, "oab")).toContain(
'data-action="edit-config"',
);
});

it("renders an unavailable state for null", () => {
expect(fleetConfigHtml(null, "oab")).toContain("fleet config unavailable");
});
Expand Down
1 change: 1 addition & 0 deletions console/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export function fleetConfigHtml(
<div class="cfg-head">
<span class="cfg-label">fleets</span>
${path}
<button class="cfg-edit" type="button" data-action="edit-config">Edit config</button>
</div>
${body}
</div>`;
Expand Down
Loading
Loading