mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: Pre-fill param inputs with query string values (#5758)
This commit is contained in:
@ -35,11 +35,15 @@ export interface ParameterInputProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
schema: ParameterSchema
|
schema: ParameterSchema
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
|
defaultValue?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParameterInput: FC<
|
export const ParameterInput: FC<ParameterInputProps> = ({
|
||||||
React.PropsWithChildren<ParameterInputProps>
|
disabled,
|
||||||
> = ({ disabled, onChange, schema }) => {
|
onChange,
|
||||||
|
schema,
|
||||||
|
defaultValue,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -50,21 +54,25 @@ export const ParameterInput: FC<
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
|
defaultValue={defaultValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParameterField: React.FC<
|
const ParameterField: React.FC<ParameterInputProps> = ({
|
||||||
React.PropsWithChildren<ParameterInputProps>
|
disabled,
|
||||||
> = ({ disabled, onChange, schema }) => {
|
onChange,
|
||||||
|
schema,
|
||||||
|
defaultValue,
|
||||||
|
}) => {
|
||||||
if (schema.validation_contains && schema.validation_contains.length > 0) {
|
if (schema.validation_contains && schema.validation_contains.length > 0) {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
id={schema.name}
|
id={schema.name}
|
||||||
size="small"
|
size="small"
|
||||||
defaultValue={schema.default_source_value}
|
defaultValue={defaultValue ?? schema.default_source_value}
|
||||||
placeholder={schema.default_source_value}
|
placeholder={schema.default_source_value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@ -116,6 +124,7 @@ const ParameterField: React.FC<
|
|||||||
size="small"
|
size="small"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={schema.default_source_value}
|
placeholder={schema.default_source_value}
|
||||||
|
defaultValue={defaultValue ?? schema.default_source_value}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange(event.target.value)
|
onChange(event.target.value)
|
||||||
}}
|
}}
|
||||||
|
@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"
|
|||||||
import * as API from "api/api"
|
import * as API from "api/api"
|
||||||
import i18next from "i18next"
|
import i18next from "i18next"
|
||||||
import {
|
import {
|
||||||
|
mockParameterSchema,
|
||||||
MockTemplate,
|
MockTemplate,
|
||||||
MockUser,
|
MockUser,
|
||||||
MockWorkspace,
|
MockWorkspace,
|
||||||
@ -62,4 +63,23 @@ describe("CreateWorkspacePage", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("uses default param values passed from the URL", async () => {
|
||||||
|
const param = "dotfile_uri"
|
||||||
|
const paramValue = "localhost:3000"
|
||||||
|
jest.spyOn(API, "getTemplateVersionSchema").mockResolvedValueOnce([
|
||||||
|
mockParameterSchema({
|
||||||
|
name: param,
|
||||||
|
default_source_value: "",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
renderWithAuth(<CreateWorkspacePage />, {
|
||||||
|
route:
|
||||||
|
"/templates/" +
|
||||||
|
MockTemplate.name +
|
||||||
|
`/workspace?param.${param}=${paramValue}`,
|
||||||
|
path: "/templates/:template/workspace",
|
||||||
|
})
|
||||||
|
await screen.findByDisplayValue(paramValue)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,29 +1,26 @@
|
|||||||
import { useActor, useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react"
|
||||||
|
import { useMe } from "hooks/useMe"
|
||||||
import { useOrganizationId } from "hooks/useOrganizationId"
|
import { useOrganizationId } from "hooks/useOrganizationId"
|
||||||
import { FC, useContext } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { useNavigate, useParams } from "react-router-dom"
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom"
|
||||||
import { pageTitle } from "util/page"
|
import { pageTitle } from "util/page"
|
||||||
import { createWorkspaceMachine } from "xServices/createWorkspace/createWorkspaceXService"
|
import { createWorkspaceMachine } from "xServices/createWorkspace/createWorkspaceXService"
|
||||||
import { XServiceContext } from "xServices/StateContext"
|
|
||||||
import {
|
import {
|
||||||
CreateWorkspaceErrors,
|
CreateWorkspaceErrors,
|
||||||
CreateWorkspacePageView,
|
CreateWorkspacePageView,
|
||||||
} from "./CreateWorkspacePageView"
|
} from "./CreateWorkspacePageView"
|
||||||
|
|
||||||
const CreateWorkspacePage: FC = () => {
|
const CreateWorkspacePage: FC = () => {
|
||||||
const xServices = useContext(XServiceContext)
|
|
||||||
const organizationId = useOrganizationId()
|
const organizationId = useOrganizationId()
|
||||||
const { template } = useParams()
|
const { template: templateName } = useParams() as { template: string }
|
||||||
const templateName = template ? template : ""
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [authState] = useActor(xServices.authXService)
|
const me = useMe()
|
||||||
const { me } = authState.context
|
|
||||||
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
||||||
context: {
|
context: {
|
||||||
organizationId,
|
organizationId,
|
||||||
templateName,
|
templateName,
|
||||||
owner: me ?? null,
|
owner: me,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
onCreateWorkspace: (_, event) => {
|
onCreateWorkspace: (_, event) => {
|
||||||
@ -31,7 +28,6 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
templates,
|
templates,
|
||||||
templateSchema,
|
templateSchema,
|
||||||
@ -42,6 +38,8 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
permissions,
|
permissions,
|
||||||
owner,
|
owner,
|
||||||
} = createWorkspaceState.context
|
} = createWorkspaceState.context
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const defaultParameterValues = getDefaultParameterValues(searchParams)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -49,6 +47,7 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
<title>{pageTitle("Create Workspace")}</title>
|
<title>{pageTitle("Create Workspace")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<CreateWorkspacePageView
|
<CreateWorkspacePageView
|
||||||
|
defaultParameterValues={defaultParameterValues}
|
||||||
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
|
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
|
||||||
loadingTemplateSchema={createWorkspaceState.matches(
|
loadingTemplateSchema={createWorkspaceState.matches(
|
||||||
"gettingTemplateSchema",
|
"gettingTemplateSchema",
|
||||||
@ -89,4 +88,18 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDefaultParameterValues = (
|
||||||
|
urlSearchParams: URLSearchParams,
|
||||||
|
): Record<string, string> => {
|
||||||
|
const paramValues: Record<string, string> = {}
|
||||||
|
Array.from(urlSearchParams.keys())
|
||||||
|
.filter((key) => key.startsWith("param."))
|
||||||
|
.forEach((key) => {
|
||||||
|
const paramName = key.replace("param.", "")
|
||||||
|
const paramValue = urlSearchParams.get(key)
|
||||||
|
paramValues[paramName] = paramValue ?? ""
|
||||||
|
})
|
||||||
|
return paramValues
|
||||||
|
}
|
||||||
|
|
||||||
export default CreateWorkspacePage
|
export default CreateWorkspacePage
|
||||||
|
@ -1,37 +1,15 @@
|
|||||||
import { ComponentMeta, Story } from "@storybook/react"
|
import { ComponentMeta, Story } from "@storybook/react"
|
||||||
import { ParameterSchema } from "../../api/typesGenerated"
|
import {
|
||||||
import { makeMockApiError, MockTemplate } from "../../testHelpers/entities"
|
makeMockApiError,
|
||||||
|
mockParameterSchema,
|
||||||
|
MockTemplate,
|
||||||
|
} from "../../testHelpers/entities"
|
||||||
import {
|
import {
|
||||||
CreateWorkspaceErrors,
|
CreateWorkspaceErrors,
|
||||||
CreateWorkspacePageView,
|
CreateWorkspacePageView,
|
||||||
CreateWorkspacePageViewProps,
|
CreateWorkspacePageViewProps,
|
||||||
} from "./CreateWorkspacePageView"
|
} from "./CreateWorkspacePageView"
|
||||||
|
|
||||||
const createParameterSchema = (
|
|
||||||
partial: Partial<ParameterSchema>,
|
|
||||||
): ParameterSchema => {
|
|
||||||
return {
|
|
||||||
id: "000000",
|
|
||||||
job_id: "000000",
|
|
||||||
allow_override_destination: false,
|
|
||||||
allow_override_source: true,
|
|
||||||
created_at: "",
|
|
||||||
default_destination_scheme: "none",
|
|
||||||
default_refresh: "",
|
|
||||||
default_source_scheme: "data",
|
|
||||||
default_source_value: "default-value",
|
|
||||||
name: "parameter name",
|
|
||||||
description: "Some description!",
|
|
||||||
redisplay_value: false,
|
|
||||||
validation_condition: "",
|
|
||||||
validation_contains: [],
|
|
||||||
validation_error: "",
|
|
||||||
validation_type_system: "",
|
|
||||||
validation_value_type: "",
|
|
||||||
...partial,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "pages/CreateWorkspacePageView",
|
title: "pages/CreateWorkspacePageView",
|
||||||
component: CreateWorkspacePageView,
|
component: CreateWorkspacePageView,
|
||||||
@ -54,7 +32,7 @@ Parameters.args = {
|
|||||||
templates: [MockTemplate],
|
templates: [MockTemplate],
|
||||||
selectedTemplate: MockTemplate,
|
selectedTemplate: MockTemplate,
|
||||||
templateSchema: [
|
templateSchema: [
|
||||||
createParameterSchema({
|
mockParameterSchema({
|
||||||
name: "region",
|
name: "region",
|
||||||
default_source_value: "🏈 US Central",
|
default_source_value: "🏈 US Central",
|
||||||
description: "Where would you like your workspace to live?",
|
description: "Where would you like your workspace to live?",
|
||||||
@ -65,19 +43,19 @@ Parameters.args = {
|
|||||||
"🦘 Australia South",
|
"🦘 Australia South",
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
createParameterSchema({
|
mockParameterSchema({
|
||||||
name: "instance_size",
|
name: "instance_size",
|
||||||
default_source_value: "Big",
|
default_source_value: "Big",
|
||||||
description: "How large should you instance be?",
|
description: "How large should you instance be?",
|
||||||
validation_contains: ["Small", "Medium", "Big"],
|
validation_contains: ["Small", "Medium", "Big"],
|
||||||
}),
|
}),
|
||||||
createParameterSchema({
|
mockParameterSchema({
|
||||||
name: "instance_size",
|
name: "instance_size",
|
||||||
default_source_value: "Big",
|
default_source_value: "Big",
|
||||||
description: "How large should your instance be?",
|
description: "How large should your instance be?",
|
||||||
validation_contains: ["Small", "Medium", "Big"],
|
validation_contains: ["Small", "Medium", "Big"],
|
||||||
}),
|
}),
|
||||||
createParameterSchema({
|
mockParameterSchema({
|
||||||
name: "disable_docker",
|
name: "disable_docker",
|
||||||
description: "Disable Docker?",
|
description: "Disable Docker?",
|
||||||
validation_value_type: "bool",
|
validation_value_type: "bool",
|
||||||
|
@ -39,6 +39,7 @@ export interface CreateWorkspacePageViewProps {
|
|||||||
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
|
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
|
||||||
// initialTouched is only used for testing the error state of the form.
|
// initialTouched is only used for testing the error state of the form.
|
||||||
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
|
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
|
||||||
|
defaultParameterValues?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = i18n
|
const { t } = i18n
|
||||||
@ -55,7 +56,7 @@ export const CreateWorkspacePageView: FC<
|
|||||||
const formFooterStyles = useFormFooterStyles()
|
const formFooterStyles = useFormFooterStyles()
|
||||||
const [parameterValues, setParameterValues] = useState<
|
const [parameterValues, setParameterValues] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
>({})
|
>(props.defaultParameterValues ?? {})
|
||||||
|
|
||||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
||||||
useFormik<TypesGen.CreateWorkspaceRequest>({
|
useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||||
@ -234,6 +235,7 @@ export const CreateWorkspacePageView: FC<
|
|||||||
<ParameterInput
|
<ParameterInput
|
||||||
disabled={form.isSubmitting}
|
disabled={form.isSubmitting}
|
||||||
key={schema.id}
|
key={schema.id}
|
||||||
|
defaultValue={parameterValues[schema.name]}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setParameterValues({
|
setParameterValues({
|
||||||
...parameterValues,
|
...parameterValues,
|
||||||
|
@ -1130,3 +1130,28 @@ export const MockAppearance: TypesGen.AppearanceConfig = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mockParameterSchema = (
|
||||||
|
partial: Partial<TypesGen.ParameterSchema>,
|
||||||
|
): TypesGen.ParameterSchema => {
|
||||||
|
return {
|
||||||
|
id: "000000",
|
||||||
|
job_id: "000000",
|
||||||
|
allow_override_destination: false,
|
||||||
|
allow_override_source: true,
|
||||||
|
created_at: "",
|
||||||
|
default_destination_scheme: "none",
|
||||||
|
default_refresh: "",
|
||||||
|
default_source_scheme: "data",
|
||||||
|
default_source_value: "default-value",
|
||||||
|
name: "parameter name",
|
||||||
|
description: "Some description!",
|
||||||
|
redisplay_value: false,
|
||||||
|
validation_condition: "",
|
||||||
|
validation_contains: [],
|
||||||
|
validation_error: "",
|
||||||
|
validation_type_system: "",
|
||||||
|
validation_value_type: "",
|
||||||
|
...partial,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user