Skip to content
Open
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
44 changes: 34 additions & 10 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,41 @@
<input id="deploy-fleet-name" class="compose-input" type="text" placeholder="e.g. support-fleet" spellcheck="false" />
</label>
<label
>Region
<input id="deploy-region" class="compose-input" type="text" placeholder="e.g. ap-east-2" spellcheck="false" />
</label>
<label
>Credential profile
<input id="deploy-profile" class="compose-input" type="text" placeholder="e.g. oab-fleet" spellcheck="false" />
</label>
<label
>Principal (optional)
<input id="deploy-principal" class="compose-input" type="text" placeholder="arn:aws:iam::…" spellcheck="false" />
>Provider
<select id="deploy-provider" class="compose-select">
<option value="aws" selected>AWS (ECS)</option>
<option value="k8s">Kubernetes</option>
</select>
</label>
<div id="deploy-aws-fields">
<label
>Region
<input id="deploy-region" class="compose-input" type="text" placeholder="e.g. ap-east-2" spellcheck="false" />
</label>
<label
>Credential profile
<input id="deploy-profile" class="compose-input" type="text" placeholder="e.g. oab-fleet" spellcheck="false" />
</label>
<label
>Principal (optional)
<input id="deploy-principal" class="compose-input" type="text" placeholder="arn:aws:iam::…" spellcheck="false" />
</label>
</div>
<div id="deploy-k8s-fields" hidden>
<label
>Context
<select id="deploy-k8s-context" class="compose-select"></select>
</label>
<label
>Namespace
<input id="deploy-k8s-namespace" class="compose-input" type="text" list="deploy-k8s-namespace-options" placeholder="pick existing or type a new one" spellcheck="false" />
<datalist id="deploy-k8s-namespace-options"></datalist>
</label>
<label
>Service account (optional)
<input id="deploy-k8s-service-account" class="compose-input" type="text" placeholder="defaults to the namespace's default SA" spellcheck="false" />
</label>
</div>
<div class="compose-actions">
<button type="submit">Next: first instance &rarr;</button>
<span class="compose-status" id="deploy-identity-status"></span>
Expand Down
92 changes: 92 additions & 0 deletions console/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ function fillOptions(sel: HTMLSelectElement, names: string[], keepNoneFirst: boo
sel.innerHTML = opts.join("");
}

// list_k8s_contexts / list_namespaces response shapes (oab-mcp, studio#104) —
// kept minimal (just what this panel reads), not the tools' full contract.
interface K8sContextsResponse {
contexts: { name: string }[];
current_context: string | null;
}
interface K8sNamespacesResponse {
namespaces: string[];
}

export type DeployMode = { kind: "new-fleet" } | { kind: "add-instance"; fleetName: string };

// What the panel reports back once a deploy + fleets.toml write both succeed —
Expand Down Expand Up @@ -75,9 +85,18 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
const cancelBtn = document.getElementById("deploy-cancel") as HTMLButtonElement | null;
const identityForm = document.getElementById("deploy-identity-form") as HTMLFormElement | null;
const nameInput = document.getElementById("deploy-fleet-name") as HTMLInputElement | null;
const providerSel = document.getElementById("deploy-provider") as HTMLSelectElement | null;
const awsFieldsEl = document.getElementById("deploy-aws-fields");
const regionInput = document.getElementById("deploy-region") as HTMLInputElement | null;
const profileInput = document.getElementById("deploy-profile") as HTMLInputElement | null;
const principalInput = document.getElementById("deploy-principal") as HTMLInputElement | null;
const k8sFieldsEl = document.getElementById("deploy-k8s-fields");
const k8sContextSel = document.getElementById("deploy-k8s-context") as HTMLSelectElement | null;
// Namespace is a text input with a <datalist> of what already exists (not a
// <select>) — per studio#104's design, a brand-new namespace is a valid
// choice and a plain select can't express "not in this list yet".
const k8sNamespaceInput = document.getElementById("deploy-k8s-namespace") as HTMLInputElement | null;
const k8sNamespaceOptions = document.getElementById("deploy-k8s-namespace-options") as HTMLDataListElement | null;
const identityStatusEl = document.getElementById("deploy-identity-status");
const composeSection = document.getElementById("deploy-compose");
const composeHeading = document.getElementById("deploy-compose-heading");
Expand All @@ -97,9 +116,15 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
!cancelBtn ||
!identityForm ||
!nameInput ||
!providerSel ||
!awsFieldsEl ||
!regionInput ||
!profileInput ||
!principalInput ||
!k8sFieldsEl ||
!k8sContextSel ||
!k8sNamespaceInput ||
!k8sNamespaceOptions ||
!composeSection ||
!tmplSel ||
!ovlSel ||
Expand Down Expand Up @@ -130,8 +155,63 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
setStatus(identityStatusEl, "");
setStatus(previewStatusEl, "");
setStatus(deployStatusEl, "");
// identityForm.reset() puts <select id="deploy-provider"> back to its
// `selected` default ("aws"), but doesn't touch the field-group `hidden`
// attributes this panel manages by hand — sync those too.
showProviderFields(providerSel.value);
};

// Toggle the AWS/k8s field groups per studio#104's design: switching
// providers resets which group is visible; field *values* aren't cleared
// here (identityForm.reset() already did that on open/close) since the
// two groups have no overlapping semantics to accidentally carry over.
const showProviderFields = (provider: string): void => {
awsFieldsEl.hidden = provider !== "aws";
k8sFieldsEl.hidden = provider !== "k8s";
};

const loadK8sNamespaces = async (): Promise<void> => {
const invoke = tauriInvoke();
if (!invoke) return;
const context = k8sContextSel.value || undefined;
try {
const res = await invoke<K8sNamespacesResponse>(
"list_namespaces",
context ? { context } : {},
);
k8sNamespaceOptions.innerHTML = res.namespaces
.map((n) => `<option value="${escapeHtml(n)}"></option>`)
.join("");
} catch (e) {
// Non-fatal: the namespace field is a free-text input either way (list_
// namespaces failing just means no autocomplete suggestions).
setStatus(identityStatusEl, `namespace list unavailable: ${errText(e)}`, "err");
}
};

const loadK8sContexts = async (): Promise<void> => {
const invoke = tauriInvoke();
if (!invoke) return;
try {
const res = await invoke<K8sContextsResponse>("list_k8s_contexts");
const opts = ['<option value="">— kubeconfig current-context —</option>'];
for (const c of res.contexts) {
const label = c.name === res.current_context ? `${c.name} (current)` : c.name;
opts.push(`<option value="${escapeHtml(c.name)}">${escapeHtml(label)}</option>`);
}
k8sContextSel.innerHTML = opts.join("");
} catch (e) {
setStatus(identityStatusEl, `k8s context list unavailable: ${errText(e)}`, "err");
}
void loadK8sNamespaces();
};

providerSel.addEventListener("change", () => {
showProviderFields(providerSel.value);
if (providerSel.value === "k8s") void loadK8sContexts();
});
k8sContextSel.addEventListener("change", () => void loadK8sNamespaces());

const loadLibraryAndPickers = async (): Promise<void> => {
const invoke = tauriInvoke();
if (!invoke) {
Expand Down Expand Up @@ -184,6 +264,18 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
setStatus(identityStatusEl, "fleet name is required", "err");
return;
}
// k8s provisioning isn't wired end-to-end yet (deploy_provision has no
// k8s dispatch — see openabdev/studio#104's discussion of why this isn't
// just "swap the driver"). Block here rather than let the wizard proceed
// into a Compose step that would fail at the final deploy_provision call.
if (providerSel.value === "k8s") {
setStatus(
identityStatusEl,
"Kubernetes provisioning isn't available yet — tracked in openabdev/studio#104",
"err",
);
return;
}
identityForm.hidden = true;
composeSection.hidden = false;
if (composeHeading) composeHeading.textContent = "Step 2 — first instance";
Expand Down
Loading