feat: Pre-fill param inputs with query string values (#5758)

This commit is contained in:
Bruno Quaresma
2023-01-17 19:56:29 -03:00
committed by GitHub
parent 28b2bbd095
commit a13614e93d
6 changed files with 97 additions and 50 deletions

View File

@ -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)
}} }}

View File

@ -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)
})
}) })

View File

@ -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

View File

@ -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",

View File

@ -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,

View File

@ -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,
}
}