mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat!: Validate monotonic numbers for rich parameters (#6046)
* Database changes * protobuf * Fix: docs * workspaces_test * Validation in coderd * Fix: resources * omitempty * UI changes * UI tests * fix
This commit is contained in:
@@ -758,10 +758,11 @@ export interface TemplateVersionParameter {
|
||||
readonly default_value: string
|
||||
readonly icon: string
|
||||
readonly options: TemplateVersionParameterOption[]
|
||||
readonly validation_error: string
|
||||
readonly validation_regex: string
|
||||
readonly validation_min: number
|
||||
readonly validation_max: number
|
||||
readonly validation_error?: string
|
||||
readonly validation_regex?: string
|
||||
readonly validation_min?: number
|
||||
readonly validation_max?: number
|
||||
readonly validation_monotonic?: ValidationMonotonicOrder
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
@@ -1209,6 +1210,13 @@ export const TemplateRoles: TemplateRole[] = ["", "admin", "use"]
|
||||
export type UserStatus = "active" | "suspended"
|
||||
export const UserStatuses: UserStatus[] = ["active", "suspended"]
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
export type ValidationMonotonicOrder = "decreasing" | "increasing"
|
||||
export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = [
|
||||
"decreasing",
|
||||
"increasing",
|
||||
]
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export type WorkspaceAgentLifecycle =
|
||||
| "created"
|
||||
|
||||
@@ -29,6 +29,7 @@ const createTemplateVersionParameter = (
|
||||
validation_regex: "",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
validation_monotonic: "",
|
||||
|
||||
...partial,
|
||||
}
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
"validationRequiredParameter": "Parameter is required.",
|
||||
"validationNumberNotInRange": "Value must be between {{min}} and {{max}}.",
|
||||
"validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}}).",
|
||||
"updateWorkspace": "Update workspace"
|
||||
"updateWorkspace": "Update workspace",
|
||||
"validationNumberNotIncreasing": "The value must be equal or greater than the previous one {{last}}.",
|
||||
"validationNumberNotDecreasing": "The value must be equal or lower than the previous one {{last}}."
|
||||
}
|
||||
|
||||
@@ -493,6 +493,7 @@ export const workspaceBuildParameterValue = (
|
||||
export const ValidationSchemaForRichParameters = (
|
||||
ns: string,
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[],
|
||||
lastBuildParameters?: TypesGen.WorkspaceBuildParameter[],
|
||||
): Yup.AnySchema => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
@@ -515,28 +516,62 @@ export const ValidationSchemaForRichParameters = (
|
||||
switch (templateParameter.type) {
|
||||
case "number":
|
||||
if (
|
||||
templateParameter.validation_min === 0 &&
|
||||
templateParameter.validation_max === 0
|
||||
templateParameter.validation_min &&
|
||||
templateParameter.validation_max
|
||||
) {
|
||||
return true
|
||||
if (
|
||||
Number(val) < templateParameter.validation_min ||
|
||||
templateParameter.validation_max < Number(val)
|
||||
) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotInRange", {
|
||||
min: templateParameter.validation_min,
|
||||
max: templateParameter.validation_max,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
Number(val) < templateParameter.validation_min ||
|
||||
templateParameter.validation_max < Number(val)
|
||||
templateParameter.validation_monotonic &&
|
||||
lastBuildParameters
|
||||
) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotInRange", {
|
||||
min: templateParameter.validation_min,
|
||||
max: templateParameter.validation_max,
|
||||
}),
|
||||
})
|
||||
const lastBuildParameter = lastBuildParameters.find(
|
||||
(last) => last.name === name,
|
||||
)
|
||||
if (lastBuildParameter) {
|
||||
switch (templateParameter.validation_monotonic) {
|
||||
case "increasing":
|
||||
if (Number(lastBuildParameter.value) > Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotIncreasing", {
|
||||
last: lastBuildParameter.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
case "decreasing":
|
||||
if (Number(lastBuildParameter.value) < Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotDecreasing", {
|
||||
last: lastBuildParameter.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case "string":
|
||||
{
|
||||
if (templateParameter.validation_regex.length === 0) {
|
||||
if (
|
||||
!templateParameter.validation_regex ||
|
||||
templateParameter.validation_regex.length === 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { fireEvent, screen } from "@testing-library/react"
|
||||
import {
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter5,
|
||||
MockWorkspace,
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
MockWorkspaceBuildParameter5,
|
||||
renderWithAuth,
|
||||
} from "testHelpers/renderHelpers"
|
||||
import * as API from "api/api"
|
||||
@@ -19,6 +21,14 @@ const validationNumberNotInRangeText = t("validationNumberNotInRange", {
|
||||
min: "1",
|
||||
max: "3",
|
||||
})
|
||||
const validationNumberNotIncreasing = t("validationNumberNotIncreasing", {
|
||||
ns: "workspaceBuildParametersPage",
|
||||
last: "3",
|
||||
})
|
||||
const validationNumberNotDecreasing = t("validationNumberNotDecreasing", {
|
||||
ns: "workspaceBuildParametersPage",
|
||||
last: "5",
|
||||
})
|
||||
|
||||
const renderWorkspaceBuildParametersPage = () => {
|
||||
return renderWithAuth(<WorkspaceBuildParametersPage />, {
|
||||
@@ -116,4 +126,80 @@ describe("WorkspaceBuildParametersPage", () => {
|
||||
)
|
||||
expect(validationError).toBeDefined()
|
||||
})
|
||||
|
||||
it("rich parameter: number is not monotonically increasing", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
])
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
])
|
||||
renderWorkspaceBuildParametersPage()
|
||||
|
||||
const element = await screen.findByText(pageTitleText)
|
||||
expect(element).toBeDefined()
|
||||
const secondParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter2.description,
|
||||
)
|
||||
expect(secondParameter).toBeDefined()
|
||||
|
||||
const secondParameterField = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter2.name,
|
||||
)
|
||||
expect(secondParameterField).toBeDefined()
|
||||
|
||||
fireEvent.change(secondParameterField, {
|
||||
target: { value: "1" },
|
||||
})
|
||||
fireEvent.submit(secondParameter)
|
||||
|
||||
const validationError = await screen.findByText(
|
||||
validationNumberNotIncreasing,
|
||||
)
|
||||
expect(validationError).toBeDefined()
|
||||
})
|
||||
|
||||
it("rich parameter: number is not monotonically decreasing", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter5,
|
||||
])
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter5,
|
||||
])
|
||||
renderWorkspaceBuildParametersPage()
|
||||
|
||||
const element = await screen.findByText(pageTitleText)
|
||||
expect(element).toBeDefined()
|
||||
const secondParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter5.description,
|
||||
)
|
||||
expect(secondParameter).toBeDefined()
|
||||
|
||||
const secondParameterField = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter5.name,
|
||||
)
|
||||
expect(secondParameterField).toBeDefined()
|
||||
|
||||
fireEvent.change(secondParameterField, {
|
||||
target: { value: "6" },
|
||||
})
|
||||
fireEvent.submit(secondParameter)
|
||||
|
||||
const validationError = await screen.findByText(
|
||||
validationNumberNotDecreasing,
|
||||
)
|
||||
expect(validationError).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,6 +64,7 @@ export const WorkspaceBuildParametersPageView: FC<
|
||||
rich_parameter_values: ValidationSchemaForRichParameters(
|
||||
"workspaceBuildParametersPage",
|
||||
props.templateParameters,
|
||||
initialRichParameterValues,
|
||||
),
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
|
||||
@@ -672,10 +672,6 @@ export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter =
|
||||
mutable: true,
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter =
|
||||
@@ -687,10 +683,9 @@ export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter =
|
||||
mutable: true,
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 1,
|
||||
validation_max: 3,
|
||||
validation_monotonic: "increasing",
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter =
|
||||
@@ -704,8 +699,6 @@ export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter =
|
||||
options: [],
|
||||
validation_error: "No way!",
|
||||
validation_regex: "^[a-z]{3}$",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter =
|
||||
@@ -717,10 +710,20 @@ export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter =
|
||||
mutable: false,
|
||||
icon: "/icon/database.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter =
|
||||
{
|
||||
name: "fifth_parameter",
|
||||
type: "number",
|
||||
description: "This is fifth parameter",
|
||||
default_value: "5",
|
||||
mutable: true,
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
validation_min: 1,
|
||||
validation_max: 10,
|
||||
validation_monotonic: "decreasing",
|
||||
}
|
||||
|
||||
// requests the MockWorkspace
|
||||
@@ -1313,6 +1316,11 @@ export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = {
|
||||
value: "3",
|
||||
}
|
||||
|
||||
export const MockWorkspaceBuildParameter5: TypesGen.WorkspaceBuildParameter = {
|
||||
name: MockTemplateVersionParameter5.name,
|
||||
value: "5",
|
||||
}
|
||||
|
||||
export const mockParameterSchema = (
|
||||
partial: Partial<TypesGen.ParameterSchema>,
|
||||
): TypesGen.ParameterSchema => {
|
||||
|
||||
Reference in New Issue
Block a user