Skip to content

Commit adf5f67

Browse files
author
PoeppingT
committed
Fix up validation to respect tombstones and stop validating on every single change for performance reasons.
1 parent 15a60d9 commit adf5f67

File tree

2 files changed

+109
-90
lines changed

2 files changed

+109
-90
lines changed

client/web/src/settings/ApplicationComponent.js

Lines changed: 106 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ export function ApplicationComponent(props) {
107107
var tierName = tiers[i]
108108
// min, max, computeSize, cpu/memory/instanceType (not in form), filesystem, database
109109
let thisTier = thisService?.tiers[tierName] || {
110-
min: 0,
111-
max: 0,
110+
min: 1,
111+
max: 1,
112112
computeSize: '',
113113
}
114114
const filesystem = {
@@ -183,120 +183,137 @@ export function ApplicationComponent(props) {
183183
}
184184

185185
// min, max, computeSize, cpu/memory/instanceType (not in form), filesystem, database
186-
const singleTierValidationSpec = Yup.object({
187-
min: Yup.number()
188-
.required('Minimum count is a required field.')
189-
.integer('Minimum count must be an integer value')
190-
.min(1, 'Minimum count must be at least ${min}'),
191-
max: Yup.number()
192-
.required('Maximum count is a required field.')
193-
.integer('Maximum count must be an integer value')
194-
.max(10, 'Maximum count can be no larger than ${max}')
195-
.test('match', 'Max cannot be smaller than min', function (max) {
196-
return max >= this.parent.min
197-
}),
198-
computeSize: Yup.string().required('Compute size is a required field.'),
199-
database: Yup.object().when('provisionDb', {
200-
is: true,
201-
then: Yup.object({
202-
engine: Yup.string().required('Engine is required'),
203-
version: Yup.string().required('Version is required'),
204-
instance: Yup.string().required('Instance is required'),
205-
username: Yup.string()
206-
.matches('^[a-zA-Z]+[a-zA-Z0-9_$]*$', 'Username is not valid')
207-
.required('Username is required'),
208-
password: Yup.string()
209-
.matches('^[a-zA-Z0-9/@"\' ]{8,}$', 'Password is not valid')
210-
.required('Password is required'),
211-
database: Yup.string(),
186+
const singleTierValidationSpec = (tombstone) => {
187+
return Yup.object({
188+
min: requiredIfNotTombstoned(tombstone, Yup.number()
189+
.integer('Minimum count must be an integer value')
190+
.min(1, 'Minimum count must be at least ${min}'), 'Minimum count is a required field.'),
191+
max: Yup.number()
192+
.required('Maximum count is a required field.')
193+
.integer('Maximum count must be an integer value')
194+
.max(10, 'Maximum count can be no larger than ${max}')
195+
.test('match', 'Max cannot be smaller than min', function (max) {
196+
return max >= this.parent.min
212197
}),
213-
otherwise: Yup.object(),
214-
}),
215-
filesystem: Yup.object().when('provisionFS', {
216-
is: true,
217-
then: Yup.object({
218-
mountPoint: Yup.string().when('fileSystemType', {
219-
is: EFS,
220-
then: Yup.string()
221-
.matches(/^(\/[a-zA-Z._-]+)*$/, 'Invalid path. Ex: /mnt')
222-
.max(100, "The full path can't exceed 100 characters in length")
223-
.test(
224-
'subdirectories',
225-
'The path can only include up to four subdirectories',
226-
(val) => (val?.match(/\//g) || []).length <= 4,
227-
)
228-
.required(),
229-
otherwise: Yup.string()
230-
.matches(
231-
/^[a-zA-Z]:\\(((?![<>:"/\\|?*]).)+((?<![ .])\\)?)*$/,
232-
'Invalid path. Ex: C:\\data',
233-
)
234-
.required(),
235-
}),
236-
fsx: Yup.object().when('fileSystemType', {
237-
is: FSX,
238-
then: Yup.object({
239-
storageGb: Yup.number()
240-
.required()
241-
.min(32, 'Storage minimum is 32 GB')
242-
.max(1048, 'Storage maximum is 1048 GB'),
243-
throughputMbs: Yup.number()
244-
.required()
245-
.min(8, 'Throughput minimum is 8 MB/s')
246-
.max(2048, 'Throughput maximum is 2048 MB/s'),
247-
backupRetentionDays: Yup.number()
248-
.required()
249-
.min(7, 'Minimum retention time is 7 days')
250-
.max(35, 'Maximum retention time is 35 days'),
251-
dailyBackupTime: Yup.string().required('Daily backup time is required'),
252-
weeklyMaintenanceTime: Yup.string().required('Weekly maintenance time is required'),
253-
windowsMountDrive: Yup.string().required('Windows mount drive is required'),
198+
computeSize: requiredIfNotTombstoned(tombstone, Yup.string(), 'Compute size is a required field.'),
199+
database: Yup.object().when('provisionDb', {
200+
is: true,
201+
then: Yup.object({
202+
engine: Yup.string().required('Engine is required'),
203+
version: Yup.string().required('Version is required'),
204+
instance: Yup.string().required('Instance is required'),
205+
username: Yup.string()
206+
.matches('^[a-zA-Z]+[a-zA-Z0-9_$]*$', 'Username is not valid')
207+
.required('Username is required'),
208+
password: Yup.string()
209+
.matches('^[a-zA-Z0-9/@"\' ]{8,}$', 'Password is not valid')
210+
.required('Password is required'),
211+
database: Yup.string(),
254212
}),
255-
otherwise: Yup.object().nullable(),
213+
otherwise: Yup.object(),
256214
}),
257-
efs: Yup.object().when('fileSystemType', {
258-
is: EFS,
259-
then: Yup.object({
260-
encryptAtRest: Yup.bool(),
261-
lifecycle: Yup.number().required('Lifecycle is required'),
262-
filesystemLifecycle: Yup.string(),
215+
filesystem: Yup.object().when('provisionFS', {
216+
is: true,
217+
then: Yup.object({
218+
mountPoint: Yup.string().when('fileSystemType', {
219+
is: EFS,
220+
then: Yup.string()
221+
.matches(/^(\/[a-zA-Z._-]+)*$/, 'Invalid path. Ex: /mnt')
222+
.max(100, "The full path can't exceed 100 characters in length")
223+
.test(
224+
'subdirectories',
225+
'The path can only include up to four subdirectories',
226+
(val) => (val?.match(/\//g) || []).length <= 4,
227+
)
228+
.required(),
229+
otherwise: Yup.string()
230+
.matches(
231+
/^[a-zA-Z]:\\(((?![<>:"/\\|?*]).)+((?<![ .])\\)?)*$/,
232+
'Invalid path. Ex: C:\\data',
233+
)
234+
.required(),
235+
}),
236+
fsx: Yup.object().when('fileSystemType', {
237+
is: FSX,
238+
then: Yup.object({
239+
storageGb: Yup.number()
240+
.required()
241+
.min(32, 'Storage minimum is 32 GB')
242+
.max(1048, 'Storage maximum is 1048 GB'),
243+
throughputMbs: Yup.number()
244+
.required()
245+
.min(8, 'Throughput minimum is 8 MB/s')
246+
.max(2048, 'Throughput maximum is 2048 MB/s'),
247+
backupRetentionDays: Yup.number()
248+
.required()
249+
.min(7, 'Minimum retention time is 7 days')
250+
.max(35, 'Maximum retention time is 35 days'),
251+
dailyBackupTime: Yup.string().required('Daily backup time is required'),
252+
weeklyMaintenanceTime: Yup.string().required('Weekly maintenance time is required'),
253+
windowsMountDrive: Yup.string().required('Windows mount drive is required'),
254+
}),
255+
otherwise: Yup.object().nullable(),
256+
}),
257+
efs: Yup.object().when('fileSystemType', {
258+
is: EFS,
259+
then: Yup.object({
260+
encryptAtRest: Yup.bool(),
261+
lifecycle: Yup.number().required('Lifecycle is required'),
262+
filesystemLifecycle: Yup.string(),
263+
}),
264+
otherwise: Yup.object().nullable(),
263265
}),
264-
otherwise: Yup.object().nullable(),
265266
}),
267+
otherwise: Yup.object(),
266268
}),
267-
otherwise: Yup.object(),
268-
}),
269-
})
269+
})
270+
}
270271

271-
const allTiersValidationSpec = {}
272-
for (var i = 0; i < tiers.length; i++) {
273-
var tierName = tiers[i]
274-
allTiersValidationSpec[tierName] = singleTierValidationSpec
272+
const allTiersValidationSpec = (tombstone) => {
273+
let allTiers = {}
274+
for (var i = 0; i < tiers.length; i++) {
275+
var tierName = tiers[i]
276+
allTiers[tierName] = singleTierValidationSpec(tombstone)
277+
}
278+
return Yup.object(allTiers)
279+
}
280+
281+
const requiredIfNotTombstoned = (tombstone, schema, requiredMessage) => {
282+
return tombstone ? schema : schema.required(message)
275283
}
276284

277285
const validationSpecs = Yup.object({
278286
name: Yup.string().required('Name is a required field.'),
279287
services: Yup.array(Yup.object({
280288
public: Yup.boolean(),
281-
name: Yup.string().required('Service Name is a required field.'),
289+
name: Yup.string().when('tombstone', (tombstone, schema) => {
290+
return requiredIfNotTombstoned(tombstone, schema, 'Service Name is a required field.')
291+
}),
282292
description: Yup.string(),
283-
path: Yup.string().matches(/^.+$/, 'error message',).required(),
293+
path: Yup.string().matches(/^.+$/, 'error message',).when('tombstone', (tombstone, schema) => {
294+
return requiredIfNotTombstoned(tombstone, schema, 'Path Name is a required field.')
295+
}),
284296
containerPort: Yup.number()
285297
.integer('Container port must be an integer value.')
286298
.required('Container port is a required field.'),
287299
containerTag: Yup.string().required('Container Tag is a required field.'),
288300
healthCheckUrl: Yup.string()
289301
.required('Health Check URL is a required field')
290302
.matches(/^\//, 'Health Check must start with forward slash (/)'),
291-
operatingSystem: Yup.string().required('Container OS is a required field.'),
303+
operatingSystem: Yup.string().when('tombstone', (tombstone, schema) => {
304+
return requiredIfNotTombstoned(tombstone, schema, 'Container OS is a required field.')
305+
}),
292306
windowsVersion: Yup.string().when('operatingSystem', {
293307
is: (containerOs) => containerOs && containerOs === WINDOWS,
294308
then: Yup.string().required('Windows version is a required field'),
295309
otherwise: Yup.string().nullable(),
296310
}),
297311
provisionDb: Yup.boolean(),
298312
provisionFS: Yup.boolean(),
299-
tiers: Yup.object(allTiersValidationSpec),
313+
tiers: Yup.object().when('tombstone', (tombstone, schema) => {
314+
return allTiersValidationSpec(tombstone)
315+
}),
316+
tombstone: Yup.boolean(),
300317
})).min(1, 'Application must have at least ${min} service(s).'),
301318
provisionBilling: Yup.boolean(),
302319
})
@@ -343,6 +360,7 @@ export function ApplicationComponent(props) {
343360
<Formik
344361
initialValues={initialValues}
345362
validationSchema={validationSpecs}
363+
validateOnChange={false}
346364
onSubmit={updateConfig}
347365
enableReinitialize={true}
348366
>

client/web/src/settings/ServicesComponent.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ const ServicesComponent = (props) => {
4848
}
4949

5050
const deleteService = (index) => {
51-
console.log('delService ' + index)
52-
console.log(formik.values.services)
51+
// we can't just remove this service from the list because it'll mess with our indices
5352
formik.values.services[index].tombstone = true
5453
setServices(formik.values.services)
54+
// kick off validation so the schema recognizes the tombstone and clears any pending errors
55+
formik.validateForm()
5556
}
5657

5758
const nextServiceIndex = services.length

0 commit comments

Comments
 (0)