-
Notifications
You must be signed in to change notification settings - Fork 10
[HYPERSHELL-129] feat(web-console): Adding section to gateway UI with instructions for sandbox connecting #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2e2e6fa
6afc6b3
0096a6e
33b5df7
4464914
55eecc5
d203fad
28815a9
4ca4e28
741b714
798e676
98a7e54
b2e51fd
3c46a82
7190a9e
b5c9973
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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({ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| copyAriaLabel, | ||
| copyText, | ||
| labels, | ||
| markers, | ||
| onFieldChange, | ||
| selectOptions, | ||
| templateCommand, | ||
| values, | ||
| }: { | ||
|
|
@@ -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>; | ||
| }) { | ||
|
|
@@ -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} | ||
|
|
@@ -198,8 +241,8 @@ export function EditableCommand({ | |
| }} | ||
| value={values[part.marker] ?? ""} | ||
| /> | ||
| ), | ||
| )} | ||
| ); | ||
| })} | ||
| </code> | ||
| </pre> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; | |
|
|
||
| import { GatewayConnectionSteps } from "./gateway-connection-steps"; | ||
| import { | ||
| buildSandboxConnectCommand, | ||
| buildSandboxCreateCommand, | ||
| type GatewayConnection, | ||
| installDocsUrl, | ||
|
|
@@ -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( | ||
|
|
@@ -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", () => { | ||
|
|
@@ -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); | ||
| }); | ||
| }); | ||
|
|
||
|
|
@@ -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", { | ||
|
|
@@ -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 () => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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, | ||
|
|
||
There was a problem hiding this comment.
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 ofEditableCommandwas removed whenSelectFieldwas introduced and was not re-added. A short doc block here noting the new optionalselectOptionsprop (marker -> constrained choices renders a<select>instead of an editable field) would keep the contract discoverable. Confidence: Medium.