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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ and [C#](https://docs.tilt.dev/example_csharp.html).
**Optimizing a Tiltfile?** Search for the function you need in our
[complete API reference](https://docs.tilt.dev/api.html).

**Custom UI Actions?** `v1alpha1.ui_button()` now always shows its inputs in a modal dialog for clearer input collection (replaces the old dropdown).

## Community & Contributions

**Questions:** Join [the Kubernetes slack](http://slack.k8s.io) and
Expand Down
44 changes: 43 additions & 1 deletion web/src/ApiButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import styled from "styled-components"
import { ApiButton } from "./ApiButton"
import { OverviewButtonMixin } from "./OverviewButton"
import { TiltSnackbarProvider } from "./Snackbar"
import { oneUIButton, textFieldForUIButton } from "./testdata"
import {
oneUIButton,
textFieldForUIButton,
boolFieldForUIButton,
} from "./testdata"
import { UIInputSpec } from "./types"

export default {
Expand Down Expand Up @@ -53,3 +57,41 @@ export const TextInputOptions = () => {
})
return <StyledButton uiButton={button} />
}

export const ButtonWithModal = () => {
const button = oneUIButton({
buttonText: "Deploy with Modal",
inputSpecs: [
textFieldForUIButton("environment", "dev", "dev, staging, prod"),
textFieldForUIButton("replicas", "1", "1-10"),
],
})
return <StyledButton uiButton={button} />
}

export const ModalWithManyInputs = () => {
const button = oneUIButton({
buttonText: "Deploy Complex App",
inputSpecs: [
textFieldForUIButton(
"environment",
"dev",
"Environment (dev/staging/prod)"
),
textFieldForUIButton("replicas", "3", "Number of replicas"),
textFieldForUIButton("version", "latest", "Image version"),
textFieldForUIButton("namespace", "default", "Kubernetes namespace"),
boolFieldForUIButton("enable_debug", false),
],
})
return <StyledButton uiButton={button} />
}

export const ModalWithConfirmation = () => {
const button = oneUIButton({
buttonText: "Delete Resources",
requiresConfirmation: true,
inputSpecs: [textFieldForUIButton("reason", "", "Reason for deletion")],
})
return <StyledButton uiButton={button} />
}
69 changes: 36 additions & 33 deletions web/src/ApiButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,29 +135,29 @@ describe("ApiButton", () => {
customRender(<ApiButton uiButton={uibutton} />).rerender
})

it("renders an options button", () => {
it("renders the button with inputs", () => {
expect(
screen.getByLabelText(`Open ${uibutton.spec!.text!} options`)
screen.getByLabelText(`Trigger ${uibutton.spec!.text!}`)
).toBeInTheDocument()
})

it("shows the options form with inputs when the options button is clicked", () => {
const optionButton = screen.getByLabelText(
`Open ${uibutton.spec!.text!} options`
it("shows the modal with inputs when the button is clicked", () => {
const button = screen.getByLabelText(
`Trigger ${uibutton.spec!.text!}`
)
userEvent.click(optionButton)
userEvent.click(button)

expect(
screen.getByText(`Options for ${uibutton.spec!.text!}`)
screen.getByText(`Configure ${uibutton.spec!.text!}`)
).toBeInTheDocument()
})

it("only shows inputs for visible inputs", () => {
// Open the options dialog first
const optionButton = screen.getByLabelText(
`Open ${uibutton.spec!.text!} options`
// Open the modal by clicking the button
const button = screen.getByLabelText(
`Trigger ${uibutton.spec!.text!}`
)
userEvent.click(optionButton)
userEvent.click(button)

inputSpecs.forEach((spec) => {
if (!spec.hidden) {
Expand All @@ -167,11 +167,11 @@ describe("ApiButton", () => {
})

it("allows an empty text string when there's a default value", async () => {
// Open the options dialog first
const optionButton = screen.getByLabelText(
`Open ${uibutton.spec!.text!} options`
// Open the modal by clicking the button
const button = screen.getByLabelText(
`Trigger ${uibutton.spec!.text!}`
)
userEvent.click(optionButton)
userEvent.click(button)

// Get the input element with the hardcoded default text
const inputWithDefault = screen.getByDisplayValue("default text")
Expand All @@ -182,11 +182,11 @@ describe("ApiButton", () => {
})

it("submits the current options when the submit button is clicked", async () => {
// Open the options dialog first
const optionButton = screen.getByLabelText(
`Open ${uibutton.spec!.text!} options`
// Open the modal by clicking the button
const button = screen.getByLabelText(
`Trigger ${uibutton.spec!.text!}`
)
userEvent.click(optionButton)
userEvent.click(button)

// Make a couple changes to the inputs
userEvent.type(screen.getByLabelText("text_field"), "new_value")
Expand All @@ -196,8 +196,8 @@ describe("ApiButton", () => {
userEvent.click(screen.getByText("choice1"))
userEvent.click(screen.getByText("choice3"))

// Click the submit button
userEvent.click(screen.getByLabelText(`Trigger ${uibutton.spec!.text!}`))
// Click the confirm button in modal
userEvent.click(screen.getByText("Confirm & Execute"))

// Wait for the button to be enabled again,
// which signals successful trigger button response
Expand Down Expand Up @@ -259,16 +259,16 @@ describe("ApiButton", () => {
})

it("submits default options when the submit button is clicked", async () => {
// The testing setup already includes a field with default text,
// so we can go ahead and click the submit button
// Open the modal
userEvent.click(screen.getByLabelText(`Trigger ${uibutton.spec!.text!}`))

// Click confirm in modal
userEvent.click(screen.getByText("Confirm & Execute"))

// Wait for the button to be enabled again,
// which signals successful trigger button response
// Wait for the modal to close and API call to complete
await waitFor(
() =>
expect(screen.getByLabelText(`Trigger ${uibutton.spec!.text!}`)).not
.toBeDisabled
expect(screen.queryByText("Confirm & Execute")).not.toBeInTheDocument()
)

const calls = fetchMock.calls()
Expand Down Expand Up @@ -341,19 +341,19 @@ describe("ApiButton", () => {
})

it("are read from local storage", () => {
// Open the options dialog
// Open the modal
userEvent.click(
screen.getByLabelText(`Open ${uibutton.spec!.text!} options`)
screen.getByLabelText(`Trigger ${uibutton.spec!.text!}`)
)

expect(screen.getByLabelText("text1")).toHaveValue("text value")
expect(screen.getByLabelText("bool1")).toBeChecked()
})

it("are written to local storage when edited", () => {
// Open the options dialog
it("are written to local storage when modal is confirmed", () => {
// Open the modal
userEvent.click(
screen.getByLabelText(`Open ${uibutton.spec!.text!} options`)
screen.getByLabelText(`Trigger ${uibutton.spec!.text!}`)
)

// Type a new value in the text field
Expand All @@ -363,8 +363,11 @@ describe("ApiButton", () => {

// Uncheck the boolean field
userEvent.click(screen.getByLabelText("bool1"))

// Confirm the modal to persist values
userEvent.click(screen.getByText("Confirm & Execute"))

// Expect local storage values are updated
// Expect local storage values are updated after confirmation
expect(buttonInputsAccessor.get()).toEqual({
text1: "new value!",
bool1: false,
Expand Down
Loading