Skip to content
Merged
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
36 changes: 36 additions & 0 deletions components/web-console/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,22 @@
"defaultMessage": "Copy the create-sandbox command",
"description": "Accessible label for copying the create-sandbox command."
},
"app.gateway.connection.copySandboxConnectCommand": {
"defaultMessage": "Copy the connect-sandbox command",
"description": "Accessible label for the button that copies the sandbox connect command."
},
"app.gateway.connection.copySetupCommand": {
"defaultMessage": "Copy the one-time setup commands",
"description": "Accessible label for copying the consolidated one-time setup script."
},
"app.gateway.connection.editEditor": {
"defaultMessage": "Editor",
"description": "Accessible label for the editor selector in the connect-sandbox command."
},
"app.gateway.connection.editExistingSandboxName": {
"defaultMessage": "Existing sandbox name (editable)",
"description": "Accessible label for the inline-editable sandbox name in the connect-sandbox command."
},
"app.gateway.connection.editModel": {
"defaultMessage": "Model (editable)",
"description": "Accessible label for the inline-editable model name in the setup command."
Expand All @@ -139,6 +151,22 @@
"defaultMessage": "Sandbox name (editable)",
"description": "Accessible label for the inline-editable sandbox name in the create-sandbox command."
},
"app.gateway.connection.editorOptions": {
"defaultMessage": "Supported interactive editors are <code>vscode</code> and <code>cursor</code>.",
"description": "Info note listing the supported editor values for the connect-sandbox command. Editor names are wrapped in <code> tags."
},
"app.gateway.connection.editorOptionsLink": {
"defaultMessage": "OpenShell connect documentation",
"description": "Link text pointing to the OpenShell sandbox connect CLI documentation."
},
"app.gateway.connection.editorOptionsLinkNewTab": {
"defaultMessage": "Sandbox connect reference (opens in a new tab)",
"description": "Accessible name for the sandbox connect docs link, including that it opens in a new tab."
},
"app.gateway.connection.editorOptionsTitle": {
"defaultMessage": "Editor options",
"description": "Title for the editor options info alert."
},
"app.gateway.connection.installLink": {
"defaultMessage": "Install the OpenShell CLI",
"description": "Link text pointing to the NVIDIA OpenShell installation documentation."
Expand Down Expand Up @@ -167,6 +195,14 @@
"defaultMessage": "Create a sandbox",
"description": "Title for the create-sandbox connection step."
},
"app.gateway.connection.sandboxConnect.description": {
"defaultMessage": "Disconnecting does not stop the sandbox process. Connect attaches to the same process instance and replays recent output.",
"description": "Supporting text for the connect-to-sandbox connection step."
},
"app.gateway.connection.sandboxConnect.title": {
"defaultMessage": "Connect to a sandbox",
"description": "Title for the connect-to-sandbox connection step."
},
"app.gateway.connection.setup.description": {
"defaultMessage": "Run these once to log in, add the Claude on Vertex AI provider, and select the model.",
"description": "Supporting text for the one-time setup connection step."
Expand Down
79 changes: 61 additions & 18 deletions packages/gateway-management-ui/src/gateways/editable-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,44 @@ function EditableField({
);
}

/**
* A copyable command block whose marked value slots are editable in place.
*
* `templateCommand` carries the edit markers and is highlighted once; `copyText`
* is the same command with the operator's current values resolved and drives
* both the copy button and (identically) a whole-block text selection. Editing a
* field calls `onFieldChange(marker, value)`; a marker used twice in the command
* (the mirrored provider name) is kept in lockstep because both slots read the
* same entry in `values`.
*/
function SelectField({
colorClassName,
label,
onChange,
options,
value,
}: {
colorClassName: string;
label: string;
onChange: (value: string) => void;
options: readonly string[];
value: string;
}) {
return (
<select
aria-label={label}
className={[styles.selectField, colorClassName].filter(Boolean).join(" ")}
onChange={(event) => {
onChange(event.target.value);
}}
value={value}
>
{options.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
);
}

export function EditableCommand({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] Restore the component contract doc. The JSDoc that explained the marker/copyText/mirrored-slot behavior of EditableCommand was removed when SelectField was introduced and was not re-added. A short doc block here noting the new optional selectOptions prop (marker -> constrained choices renders a <select> instead of an editable field) would keep the contract discoverable. Confidence: Medium.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up for coordination (no change required in isolation): another in-flight PR renames this exported EditableCommand component to CommandBlock and refactors its call sites, while this PR extends EditableCommand in place with the new selectOptions/SelectField path. Whichever lands first, this extension will need to be re-applied onto the renamed API. See the Cross-PR coordination section in the review summary.

copyAriaLabel,
copyText,
labels,
markers,
onFieldChange,
selectOptions,
templateCommand,
values,
}: {
Expand All @@ -129,6 +151,7 @@ export function EditableCommand({
labels: Record<string, string>;
markers: readonly string[];
onFieldChange: (marker: string, value: string) => void;
selectOptions?: Record<string, readonly string[]>;
templateCommand: string;
values: Record<string, string>;
}) {
Expand Down Expand Up @@ -183,12 +206,32 @@ export function EditableCommand({
<div className={styles.highlighted}>
<pre className="shiki">
<code>
{parts.map((part, index) =>
part.kind === "text" ? (
<span className={part.className} key={index}>
{part.value}
</span>
) : (
{parts.map((part, index) => {
if (part.kind === "text") {
return (
<span className={part.className} key={index}>
{part.value}
</span>
);
}

const options = selectOptions?.[part.marker];
if (options) {
return (
<SelectField
colorClassName={part.className}
key={index}
label={labels[part.marker] ?? ""}
onChange={(value) => {
onFieldChange(part.marker, value);
}}
options={options}
value={values[part.marker] ?? ""}
/>
);
}

return (
<EditableField
colorClassName={part.className}
key={index}
Expand All @@ -198,8 +241,8 @@ export function EditableCommand({
}}
value={values[part.marker] ?? ""}
/>
),
)}
);
})}
</code>
</pre>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,26 @@
outline-offset: 2px;
}

.selectField {
appearance: auto;
background: transparent;
border: none;
border-radius: var(--pf-t--global--border--radius--small);
cursor: pointer;
font: inherit;
padding: 0;
}

.selectField:hover {
background-color: var(--pf-t--global--background--color--secondary--default);
}

.selectField:focus-visible {
background-color: var(--pf-t--global--background--color--secondary--default);
outline: 2px solid currentColor;
outline-offset: 2px;
}

.prereqAlert {
margin-block: var(--pf-t--global--spacer--sm);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";

import { GatewayConnectionSteps } from "./gateway-connection-steps";
import {
buildSandboxConnectCommand,
buildSandboxCreateCommand,
type GatewayConnection,
installDocsUrl,
Expand Down Expand Up @@ -32,7 +33,7 @@ function renderSteps(gateway: GatewayConnection) {
}

describe("GatewayConnectionSteps", () => {
it("renders the setup and sandbox steps", () => {
it("renders the setup, sandbox create, and sandbox connect steps", () => {
renderSteps(readyGateway);

expect(
Expand All @@ -41,6 +42,9 @@ describe("GatewayConnectionSteps", () => {
expect(
screen.getByRole("heading", { name: "Create a sandbox" }),
).toBeTruthy();
expect(
screen.getByRole("heading", { name: "Connect to a sandbox" }),
).toBeTruthy();
});

it("renders a prerequisite alert with an install docs link", () => {
Expand All @@ -62,11 +66,11 @@ describe("GatewayConnectionSteps", () => {
expect(link.textContent).toContain("Install the OpenShell CLI");
});

it("highlights both command blocks with Shiki once they resolve", async () => {
it("highlights all command blocks with Shiki once they resolve", async () => {
const { container } = renderSteps(readyGateway);

await waitFor(() => {
expect(container.querySelectorAll(".shiki").length).toBe(2);
expect(container.querySelectorAll(".shiki").length).toBe(3);
});
});

Expand All @@ -88,7 +92,7 @@ describe("GatewayConnectionSteps", () => {
const { container } = renderSteps(readyGateway);

await waitFor(() => {
expect(container.querySelectorAll(".shiki").length).toBe(2);
expect(container.querySelectorAll(".shiki").length).toBe(3);
});

const providerFields = screen.getAllByRole("textbox", {
Expand Down Expand Up @@ -134,6 +138,48 @@ describe("GatewayConnectionSteps", () => {
);
});

it("copies the raw sandbox connect command", async () => {
const user = userEvent.setup();
renderSteps(readyGateway);

await user.click(
screen.getByRole("button", {
name: "Copy the connect-sandbox command",
}),
);

expect(await navigator.clipboard.readText()).toBe(
buildSandboxConnectCommand(),
);
});

it("mirrors an edited sandbox name into both create and connect commands", async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] Missing interaction test for the editor selector. The new connect-command tests cover the sandbox-name mirroring and the raw copy, and gateway-connections.test.ts covers the --editor vscode variant at the builder level, but nothing exercises the inline <select> in the UI. Consider adding a test that changes the editor selector and asserts the copied command switches to --editor vscode, so the selectOptions/SelectField wiring in EditableCommand is regression-protected. Confidence: High.

const user = userEvent.setup();
renderSteps(readyGateway);

const createField = await screen.findByRole("textbox", {
name: "Sandbox name (editable)",
});
createField.textContent = "scratch";
fireEvent.input(createField);

await user.click(
screen.getByRole("button", { name: "Copy the create-sandbox command" }),
);
expect(await navigator.clipboard.readText()).toBe(
buildSandboxCreateCommand("scratch"),
);

await user.click(
screen.getByRole("button", {
name: "Copy the connect-sandbox command",
}),
);
expect(await navigator.clipboard.readText()).toBe(
buildSandboxConnectCommand("scratch"),
);
});

it("shows a pending placeholder until the gateway is ready", () => {
renderSteps({
...readyGateway,
Expand Down
Loading
Loading