Skip to content
Draft
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ pnpm build
```

The console is built into `apps/console/dist/`.

## Test

```sh
pnpm test
```

Runs the workspace test suites (vitest + jsdom for the console).
98 changes: 98 additions & 0 deletions apps/console/src/components/AdditionalPropertiesField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, it, expect } from "vitest"
import { render, screen } from "@testing-library/react"
import { SchemaForm } from "./SchemaForm.tsx"
import { IMMUTABLE_HELP_TEXT } from "../lib/immutable-paths.ts"

const schemaWithMap = JSON.stringify({
type: "object",
properties: {
labels: {
type: "object",
additionalProperties: { type: "string" },
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "labels are immutable" },
],
},
},
})

const noop = () => {}

describe("AdditionalPropertiesField immutable", () => {
it("renders Add/Remove controls when not immutable", () => {
render(
<SchemaForm
openAPISchema={schemaWithMap}
formData={{ labels: { env: "prod" } }}
onChange={noop}
/>,
)
expect(screen.getByPlaceholderText("Enter key name...")).toBeInTheDocument()
expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument()
expect(screen.getByRole("button", { name: /remove/i })).toBeInTheDocument()
})

it("treats nested *-not-last on additionalProperties as whole-map disabled in the UI; overlay matches", () => {
// Per-value-immutable schema (additionalProperties carries a nested
// CEL rule). The UI freezes the whole map and the overlay freezes the
// whole map (see overlayImmutable's additionalProperties branch).
// Both sides therefore agree: YAML-editor bypasses that add or rename
// keys are caught by the overlay before the PUT lands.
const nestedImmutableSchema = JSON.stringify({
type: "object",
properties: {
labels: {
type: "object",
additionalProperties: {
type: "object",
properties: {
value: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "value is immutable" },
],
},
},
},
},
},
})
render(
<SchemaForm
openAPISchema={nestedImmutableSchema}
formData={{ labels: { env: { value: "prod" } } }}
onChange={noop}
immutableMode="enforce"
/>,
)
expect(
screen.queryByPlaceholderText("Enter key name..."),
).not.toBeInTheDocument()
expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})

it("hides Add/Remove and disables inner fields when the map is immutable in enforce mode", () => {
render(
<SchemaForm
openAPISchema={schemaWithMap}
formData={{ labels: { env: "prod" } }}
onChange={noop}
immutableMode="enforce"
/>,
)
expect(
screen.queryByPlaceholderText("Enter key name..."),
).not.toBeInTheDocument()
expect(
screen.queryByRole("button", { name: /add/i }),
).not.toBeInTheDocument()
expect(
screen.queryByRole("button", { name: /remove/i }),
).not.toBeInTheDocument()

const innerInput = screen.getByDisplayValue("prod") as HTMLInputElement
expect(innerInput).toBeDisabled()

expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})
})
140 changes: 140 additions & 0 deletions apps/console/src/components/SchemaForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { describe, it, expect } from "vitest"
import { render, screen } from "@testing-library/react"
import { SchemaForm } from "./SchemaForm.tsx"
import { IMMUTABLE_HELP_TEXT } from "../lib/immutable-paths.ts"

const schema = JSON.stringify({
type: "object",
properties: {
version: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "version is immutable" },
],
},
description: {
type: "string",
},
},
})

const noop = () => {}

describe("SchemaForm immutableMode", () => {
it("renders fields editable and without help text when immutableMode is omitted", () => {
render(
<SchemaForm
openAPISchema={schema}
formData={{ version: "1.0", description: "hi" }}
onChange={noop}
/>,
)
const versionInput = screen.getByLabelText("version") as HTMLInputElement
expect(versionInput).not.toBeDisabled()
expect(screen.queryByText(IMMUTABLE_HELP_TEXT)).not.toBeInTheDocument()
})

it("greys out immutable fields and shows help text when immutableMode is enforce", () => {
render(
<SchemaForm
openAPISchema={schema}
formData={{ version: "1.0", description: "hi" }}
onChange={noop}
immutableMode="enforce"
/>,
)
const versionInput = screen.getByLabelText("version") as HTMLInputElement
expect(versionInput).toBeDisabled()
const descriptionInput = screen.getByLabelText(
"description",
) as HTMLInputElement
expect(descriptionInput).not.toBeDisabled()
expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})

it("treats immutableMode='off' the same as omitting the prop", () => {
render(
<SchemaForm
openAPISchema={schema}
formData={{ version: "1.0", description: "hi" }}
onChange={noop}
immutableMode="off"
/>,
)
const versionInput = screen.getByLabelText("version") as HTMLInputElement
expect(versionInput).not.toBeDisabled()
expect(screen.queryByText(IMMUTABLE_HELP_TEXT)).not.toBeInTheDocument()
})

it("greys out every element of a whole-array immutable field", () => {
// *-as-last: the items schema itself carries the rule, so the entire
// array is immutable. RJSF receives ui:disabled on `items` and
// disables each element's input.
const wholeArraySchema = JSON.stringify({
type: "object",
properties: {
tags: {
type: "array",
items: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "tags is immutable" },
],
},
},
},
})
render(
<SchemaForm
openAPISchema={wholeArraySchema}
formData={{ tags: ["alpha", "beta"] }}
onChange={noop}
immutableMode="enforce"
/>,
)
expect(screen.getByDisplayValue("alpha")).toBeDisabled()
expect(screen.getByDisplayValue("beta")).toBeDisabled()
expect(screen.getAllByText(IMMUTABLE_HELP_TEXT).length).toBeGreaterThan(0)
// Mirroring the AdditionalPropertiesField map case: the array
// wrapper is disabled, so RJSF disables Add (and per-element
// Remove) too. Otherwise the user could append an entry that the
// overlay would silently drop on save.
expect(screen.getByRole("button", { name: /add/i })).toBeDisabled()
})

it("greys out immutable nested fields inside array items", () => {
const arraySchema = JSON.stringify({
type: "object",
properties: {
volumes: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "name immutable" },
],
},
size: { type: "string" },
},
},
},
},
})
render(
<SchemaForm
openAPISchema={arraySchema}
formData={{ volumes: [{ name: "disk1", size: "10Gi" }] }}
onChange={noop}
immutableMode="enforce"
/>,
)
const nameInput = screen.getByDisplayValue("disk1") as HTMLInputElement
const sizeInput = screen.getByDisplayValue("10Gi") as HTMLInputElement
expect(nameInput).toBeDisabled()
expect(sizeInput).not.toBeDisabled()
expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})
})
Loading
Loading