-
Notifications
You must be signed in to change notification settings - Fork 742
UI rework polish #2666
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
Merged
Merged
UI rework polish #2666
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
addc276
fix(ui): give the substrate tables one hover and one sort header
toreysoloio e2289ca
fix(ui): mark the fields these forms refuse to submit without
toreysoloio db0b844
fix(ui): stop the MCP form inventing a server name
toreysoloio 82907bf
fix(ui): say what actually sorts the substrate tables, and tighten th…
toreysoloio f06b4ec
fix(ui): give the segmented control an edge on the dark theme
toreysoloio 709a199
Merge branch 'main' into ui-rework-polish
toreysoloio 94a47f0
Merge branch 'main' into ui-rework-polish
Charlesthebird 0a5d1f3
Merge branch 'main' into ui-rework-polish
Charlesthebird File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import type { Locator, Page } from "@playwright/test"; | ||
| import { test, expect } from "../../fixtures/test"; | ||
| import { expectSettled, loadPage, routes } from "../../helpers/app"; | ||
|
|
||
| /** The fixture configuration the edit form opens on. */ | ||
| const modelEdit = "/models/kagent/default-model-config/edit"; | ||
|
|
||
| /** The fixture template the details page renders read-only. */ | ||
| const templateDetail = "/agent-templates/kagent/k8s-agent-7f3a91c"; | ||
|
|
||
| /** | ||
| * The asterisk on a field the form will not submit without. | ||
| * | ||
| * antd draws it from `required` on a `Form.Item`, and every authoring surface here | ||
| * gates its own submit in code — `draftProblems`, `modelDraftIssues`, | ||
| * `validateMcpServerForm` — rather than through antd's rules. So the mark and the | ||
| * gate are two separate statements about the same field, and nothing but a test | ||
| * keeps them agreeing. They had already come apart: the whole agent-template form | ||
| * carried no mark at all while refusing to save without a model configuration, and | ||
| * the model form's API key was required to create with nothing on screen to say so. | ||
| * | ||
| * Asserted as a pair each time — what is marked *and* what is not. A test that only | ||
| * checked the marks would pass just as well on a form that marked every field, which | ||
| * tells a reader nothing about which ones matter. | ||
| */ | ||
|
|
||
| /** One field's label, by the text a reader sees on it. */ | ||
| function fieldLabel(page: Page, text: string): Locator { | ||
| // Exact, because these labels are prefixes of each other: "Name" would otherwise | ||
| // match "Namespace" too, and match it first. | ||
| return page | ||
| .locator(".ant-form-item-label label") | ||
| .filter({ has: page.getByText(text, { exact: true }) }); | ||
| } | ||
|
|
||
| /** | ||
| * The labels that carry the mark, and the ones that must not. | ||
| * | ||
| * Both lists in one helper so a call site reads as the claim it is making. | ||
| */ | ||
| async function expectRequired( | ||
| page: Page, | ||
| { marked, unmarked }: { marked: string[]; unmarked: string[] }, | ||
| ): Promise<void> { | ||
| for (const text of marked) { | ||
| const label = fieldLabel(page, text); | ||
| await expect(label, `"${text}" is required, so it must be marked`).toHaveCount(1); | ||
| await expect(label).toHaveClass(/ant-form-item-required/); | ||
| } | ||
| for (const text of unmarked) { | ||
| const label = fieldLabel(page, text); | ||
| await expect(label, `"${text}" is optional, so it must not be marked`).toHaveCount(1); | ||
| await expect(label).not.toHaveClass(/ant-form-item-required/); | ||
| } | ||
| } | ||
|
|
||
| test("forms: a field the form refuses to submit without is marked required", async ({ | ||
| page, | ||
| }) => { | ||
| await test.step("1. the agent template form", async () => { | ||
| await loadPage(page, routes.agentTemplateNew, { title: "New agent template" }); | ||
| await expectSettled(page); | ||
|
|
||
| // Name and the model configuration are what `draftProblems` refuses to submit | ||
| // without. Everything else on this form is genuinely optional — including the | ||
| // system prompt, which a template may take from its harness instead. | ||
| await expectRequired(page, { | ||
| marked: ["Name", "Model configuration"], | ||
| unmarked: ["Description", "System prompt"], | ||
| }); | ||
| }); | ||
|
|
||
| await test.step("2. the model form, where the API key is required only to create", async () => { | ||
| await loadPage(page, routes.modelNew, { title: "New model" }); | ||
| await expectSettled(page); | ||
|
|
||
| await expectRequired(page, { | ||
| marked: ["Provider", "Model", "Name", "Namespace", "API key"], | ||
| // A radio group that arrives with a choice already made cannot be missing one. | ||
| unmarked: ["Authentication"], | ||
| }); | ||
| }); | ||
|
|
||
| await test.step("3. and not on an edit, which keeps the key it already has", async () => { | ||
| await loadPage(page, `${modelEdit}?mock=ok`); | ||
| await expectSettled(page); | ||
|
|
||
| // The fixture holds its credential in a Secret, so the inline-key field is not | ||
| // on screen until that is what the reader is choosing. | ||
| await page | ||
| .getByTestId("model-auth-type") | ||
| .getByText("API key", { exact: true }) | ||
| .click(); | ||
|
|
||
| // The label says the same thing in words; the mark has to agree with it, or the | ||
| // form is asking for a credential the cluster already holds. | ||
| await expectRequired(page, { | ||
| marked: ["Name", "Namespace"], | ||
| unmarked: ["API key (leave blank to keep existing)"], | ||
| }); | ||
| }); | ||
|
|
||
| await test.step("4. the harness form", async () => { | ||
| await loadPage(page, routes.harnessNew, { title: "New harness" }); | ||
| await expectSettled(page); | ||
|
|
||
| await expectRequired(page, { | ||
| marked: [ | ||
| "Namespace", | ||
| "Name", | ||
| "Runtime adapter", | ||
| "Workload image", | ||
| "Worker pool", | ||
| "Snapshot location", | ||
| ], | ||
| // Optional in the CRD's sense and warned about on screen instead: a harness | ||
| // with no selector is created and admits nothing. | ||
| unmarked: ["Admits agent templates labelled"], | ||
| }); | ||
| }); | ||
|
|
||
| await test.step("5. the MCP server form, whose namespace really is optional", async () => { | ||
| await loadPage(page, routes.mcpServerNew, { title: "New MCP server" }); | ||
| await expectSettled(page); | ||
|
|
||
| // Unlike every other form here: `validateMcpServerForm` accepts a blank | ||
| // namespace and the controller defaults it, so a mark would be a lie. | ||
| await expectRequired(page, { | ||
| marked: ["Name", "Server URL"], | ||
| unmarked: ["Namespace", "TLS", "Headers"], | ||
| }); | ||
| }); | ||
|
|
||
| await test.step("6. the prompt library form", async () => { | ||
| await loadPage(page, routes.promptNew, { title: "New prompt library" }); | ||
| await expectSettled(page); | ||
|
|
||
| await expectRequired(page, { marked: ["Namespace", "Name"], unmarked: [] }); | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * Read-only is not a form, so it asks for nothing. | ||
| * | ||
| * The template details page renders the same component with `readOnly`, and an | ||
| * asterisk there would be asking a reader to supply something they are only looking | ||
| * at — on a template that already has it. | ||
| */ | ||
| test("forms: the read-only template view marks nothing as required", async ({ page }) => { | ||
| await loadPage(page, templateDetail); | ||
| await expectSettled(page); | ||
|
|
||
| await expect(page.locator(".ant-form-item-label label").first()).toBeVisible(); | ||
| await expect(page.locator(".ant-form-item-label label.ant-form-item-required")).toHaveCount( | ||
| 0, | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.