]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
import 15.2.0 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / forms / cd-validators.ts
1 import {
2 AbstractControl,
3 AsyncValidatorFn,
4 ValidationErrors,
5 ValidatorFn,
6 Validators
7 } from '@angular/forms';
8
9 import { I18n } from '@ngx-translate/i18n-polyfill';
10 import * as _ from 'lodash';
11 import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
12 import { map, switchMapTo, take } from 'rxjs/operators';
13
14 import { DimlessBinaryPipe } from '../pipes/dimless-binary.pipe';
15 import { FormatterService } from '../services/formatter.service';
16
17 export function isEmptyInputValue(value: any): boolean {
18 return value == null || value.length === 0;
19 }
20
21 export type existsServiceFn = (value: any) => Observable<boolean>;
22
23 export class CdValidators {
24 /**
25 * Validator that performs email validation. In contrast to the Angular
26 * email validator an empty email will not be handled as invalid.
27 */
28 static email(control: AbstractControl): ValidationErrors | null {
29 // Exit immediately if value is empty.
30 if (isEmptyInputValue(control.value)) {
31 return null;
32 }
33 return Validators.email(control);
34 }
35
36 /**
37 * Validator function in order to validate IP addresses.
38 * @param {number} version determines the protocol version. It needs to be set to 4 for IPv4 and
39 * to 6 for IPv6 validation. For any other number (it's also the default case) it will return a
40 * function to validate the input string against IPv4 OR IPv6.
41 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
42 * if the validation failed, otherwise `null`.
43 */
44 static ip(version: number = 0): ValidatorFn {
45 // prettier-ignore
46 const ipv4Rgx =
47 /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
48 const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
49
50 if (version === 4) {
51 return Validators.pattern(ipv4Rgx);
52 } else if (version === 6) {
53 return Validators.pattern(ipv6Rgx);
54 } else {
55 return Validators.pattern(new RegExp(ipv4Rgx.source + '|' + ipv6Rgx.source));
56 }
57 }
58
59 /**
60 * Validator function in order to validate numbers.
61 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
62 * if the validation failed, otherwise `null`.
63 */
64 static number(allowsNegative: boolean = true): ValidatorFn {
65 if (allowsNegative) {
66 return Validators.pattern(/^-?[0-9]+$/i);
67 } else {
68 return Validators.pattern(/^[0-9]+$/i);
69 }
70 }
71
72 /**
73 * Validator function in order to validate decimal numbers.
74 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
75 * if the validation failed, otherwise `null`.
76 */
77 static decimalNumber(allowsNegative: boolean = true): ValidatorFn {
78 if (allowsNegative) {
79 return Validators.pattern(/^-?[0-9]+(.[0-9]+)?$/i);
80 } else {
81 return Validators.pattern(/^[0-9]+(.[0-9]+)?$/i);
82 }
83 }
84
85 /**
86 * Validator that requires controls to fulfill the specified condition if
87 * the specified prerequisites matches. If the prerequisites are fulfilled,
88 * then the given function is executed and if it succeeds, the 'required'
89 * validation error will be returned, otherwise null.
90 * @param {Object} prerequisites An object containing the prerequisites.
91 * ### Example
92 * ```typescript
93 * {
94 * 'generate_key': true,
95 * 'username': 'Max Mustermann'
96 * }
97 * ```
98 * Only if all prerequisites are fulfilled, then the validation of the
99 * control will be triggered.
100 * @param {Function | undefined} condition The function to be executed when all
101 * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue}
102 * function will be used by default. The control's value is used as function
103 * argument. The function must return true to set the validation error.
104 * @return {ValidatorFn} Returns the validator function.
105 */
106 static requiredIf(prerequisites: object, condition?: Function | undefined): ValidatorFn {
107 let isWatched = false;
108
109 return (control: AbstractControl): ValidationErrors | null => {
110 if (!isWatched && control.parent) {
111 Object.keys(prerequisites).forEach((key) => {
112 control.parent.get(key).valueChanges.subscribe(() => {
113 control.updateValueAndValidity({ emitEvent: false });
114 });
115 });
116
117 isWatched = true;
118 }
119
120 // Check if all prerequisites met.
121 if (
122 !Object.keys(prerequisites).every((key) => {
123 return control.parent && control.parent.get(key).value === prerequisites[key];
124 })
125 ) {
126 return null;
127 }
128 const success = _.isFunction(condition)
129 ? condition.call(condition, control.value)
130 : isEmptyInputValue(control.value);
131 return success ? { required: true } : null;
132 };
133 }
134
135 /**
136 * Compose multiple validators into a single function that returns the union of
137 * the individual error maps for the provided control when the given prerequisites
138 * are fulfilled.
139 *
140 * @param {Object} prerequisites An object containing the prerequisites as
141 * key/value pairs.
142 * ### Example
143 * ```typescript
144 * {
145 * 'generate_key': true,
146 * 'username': 'Max Mustermann'
147 * }
148 * ```
149 * @param {ValidatorFn[]} validators List of validators that should be taken
150 * into action when the prerequisites are met.
151 * @return {ValidatorFn} Returns the validator function.
152 */
153 static composeIf(prerequisites: object, validators: ValidatorFn[]): ValidatorFn {
154 let isWatched = false;
155 return (control: AbstractControl): ValidationErrors | null => {
156 if (!isWatched && control.parent) {
157 Object.keys(prerequisites).forEach((key) => {
158 control.parent.get(key).valueChanges.subscribe(() => {
159 control.updateValueAndValidity({ emitEvent: false });
160 });
161 });
162 isWatched = true;
163 }
164 // Check if all prerequisites are met.
165 if (
166 !Object.keys(prerequisites).every((key) => {
167 return control.parent && control.parent.get(key).value === prerequisites[key];
168 })
169 ) {
170 return null;
171 }
172 return Validators.compose(validators)(control);
173 };
174 }
175
176 /**
177 * Custom validation by passing a name for the error and a function as error condition.
178 *
179 * @param {string} error
180 * @param {Function} condition - a truthy return value will trigger the error
181 * @returns {ValidatorFn}
182 */
183 static custom(error: string, condition: Function): ValidatorFn {
184 return (control: AbstractControl): { [key: string]: any } => {
185 const value = condition.call(this, control.value);
186 if (value) {
187 return { [error]: value };
188 }
189 return null;
190 };
191 }
192
193 /**
194 * Validate form control if condition is true with validators.
195 *
196 * @param {AbstractControl} formControl
197 * @param {Function} condition
198 * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested
199 * when the condition is met
200 * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested
201 * @param {AbstractControl[]} watchControls List of controls that the condition depend on.
202 * Every time one of this controls value is updated, the validation will be triggered
203 */
204 static validateIf(
205 formControl: AbstractControl,
206 condition: Function,
207 conditionalValidators: ValidatorFn[],
208 permanentValidators: ValidatorFn[] = [],
209 watchControls: AbstractControl[] = []
210 ) {
211 conditionalValidators = conditionalValidators.concat(permanentValidators);
212
213 formControl.setValidators((control: AbstractControl): {
214 [key: string]: any;
215 } => {
216 const value = condition.call(this);
217 if (value) {
218 return Validators.compose(conditionalValidators)(control);
219 }
220 if (permanentValidators.length > 0) {
221 return Validators.compose(permanentValidators)(control);
222 }
223 return null;
224 });
225
226 watchControls.forEach((control: AbstractControl) => {
227 control.valueChanges.subscribe(() => {
228 formControl.updateValueAndValidity({ emitEvent: false });
229 });
230 });
231 }
232
233 /**
234 * Validator that requires that both specified controls have the same value.
235 * Error will be added to the `path2` control.
236 * @param {string} path1 A dot-delimited string that define the path to the control.
237 * @param {string} path2 A dot-delimited string that define the path to the control.
238 * @return {ValidatorFn} Returns a validator function that always returns `null`.
239 * If the validation fails an error map with the `match` property will be set
240 * on the `path2` control.
241 */
242 static match(path1: string, path2: string): ValidatorFn {
243 return (control: AbstractControl): { [key: string]: any } => {
244 const ctrl1 = control.get(path1);
245 const ctrl2 = control.get(path2);
246 if (!ctrl1 || !ctrl2) {
247 return null;
248 }
249 if (ctrl1.value !== ctrl2.value) {
250 ctrl2.setErrors({ match: true });
251 } else {
252 const hasError = ctrl2.hasError('match');
253 if (hasError) {
254 // Remove the 'match' error. If no more errors exists, then set
255 // the error value to 'null', otherwise the field is still marked
256 // as invalid.
257 const errors = ctrl2.errors;
258 _.unset(errors, 'match');
259 ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors);
260 }
261 }
262 return null;
263 };
264 }
265
266 /**
267 * Asynchronous validator that requires the control's value to be unique.
268 * The validation is only executed after the specified delay. Every
269 * keystroke during this delay will restart the timer.
270 * @param serviceFn {existsServiceFn} The service function that is
271 * called to check whether the given value exists. It must return
272 * boolean 'true' if the given value exists, otherwise 'false'.
273 * @param serviceFnThis {any} The object to be used as the 'this' object
274 * when calling the serviceFn function. Defaults to null.
275 * @param {number|Date} dueTime The delay time to wait before the
276 * serviceFn call is executed. This is useful to prevent calls on
277 * every keystroke. Defaults to 500.
278 * @return {AsyncValidatorFn} Returns an asynchronous validator function
279 * that returns an error map with the `notUnique` property if the
280 * validation check succeeds, otherwise `null`.
281 */
282 static unique(
283 serviceFn: existsServiceFn,
284 serviceFnThis: any = null,
285 dueTime = 500
286 ): AsyncValidatorFn {
287 return (control: AbstractControl): Observable<ValidationErrors | null> => {
288 // Exit immediately if user has not interacted with the control yet
289 // or the control value is empty.
290 if (control.pristine || isEmptyInputValue(control.value)) {
291 return observableOf(null);
292 }
293 // Forgot previous requests if a new one arrives within the specified
294 // delay time.
295 return observableTimer(dueTime).pipe(
296 switchMapTo(serviceFn.call(serviceFnThis, control.value)),
297 map((resp: boolean) => {
298 if (!resp) {
299 return null;
300 } else {
301 return { notUnique: true };
302 }
303 }),
304 take(1)
305 );
306 };
307 }
308
309 /**
310 * Validator function for UUIDs.
311 * @param required - Defines if it is mandatory to fill in the UUID
312 * @return Validator function that returns an error object containing `invalidUuid` if the
313 * validation failed, `null` otherwise.
314 */
315 static uuid(required = false): ValidatorFn {
316 const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
317 return (control: AbstractControl): { [key: string]: any } | null => {
318 if (control.pristine && control.untouched) {
319 return null;
320 } else if (!required && !control.value) {
321 return null;
322 } else if (uuidRe.test(control.value)) {
323 return null;
324 }
325 return { invalidUuid: 'This is not a valid UUID' };
326 };
327 }
328
329 /**
330 * A simple minimum validator vor cd-binary inputs.
331 *
332 * To use the validation message pass I18n into the function as it cannot
333 * be called in a static one.
334 */
335 static binaryMin(bytes: number): ValidatorFn {
336 return (control: AbstractControl): { [key: string]: (i18n: I18n) => string } | null => {
337 const formatterService = new FormatterService();
338 const currentBytes = new FormatterService().toBytes(control.value);
339 if (bytes <= currentBytes) {
340 return null;
341 }
342 const value = new DimlessBinaryPipe(formatterService).transform(bytes);
343 return {
344 binaryMin: (i18n: I18n) => i18n(`Size has to be at least {{value}} or more`, { value })
345 };
346 };
347 }
348
349 /**
350 * A simple maximum validator vor cd-binary inputs.
351 *
352 * To use the validation message pass I18n into the function as it cannot
353 * be called in a static one.
354 */
355 static binaryMax(bytes: number): ValidatorFn {
356 return (control: AbstractControl): { [key: string]: (i18n: I18n) => string } | null => {
357 const formatterService = new FormatterService();
358 const currentBytes = formatterService.toBytes(control.value);
359 if (bytes >= currentBytes) {
360 return null;
361 }
362 const value = new DimlessBinaryPipe(formatterService).transform(bytes);
363 return {
364 binaryMax: (i18n: I18n) => i18n(`Size has to be at most {{value}} or less`, { value })
365 };
366 };
367 }
368
369 /**
370 * Asynchronous validator that checks if the password meets the password
371 * policy.
372 * @param userServiceThis The object to be used as the 'this' object
373 * when calling the 'validatePassword' method of the 'UserService'.
374 * @param usernameFn Function to get the username that should be
375 * taken into account.
376 * @param callback Callback function that is called after the validation
377 * has been done.
378 * @return {AsyncValidatorFn} Returns an asynchronous validator function
379 * that returns an error map with the `passwordPolicy` property if the
380 * validation check fails, otherwise `null`.
381 */
382 static passwordPolicy(
383 userServiceThis: any,
384 usernameFn?: Function,
385 callback?: (valid: boolean, credits?: number, valuation?: string) => void
386 ): AsyncValidatorFn {
387 return (control: AbstractControl): Observable<ValidationErrors | null> => {
388 if (control.pristine || control.value === '') {
389 if (_.isFunction(callback)) {
390 callback(true, 0);
391 }
392 return observableOf(null);
393 }
394 let username;
395 if (_.isFunction(usernameFn)) {
396 username = usernameFn();
397 }
398 return observableTimer(500).pipe(
399 switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)),
400 map((resp: { valid: boolean; credits: number; valuation: string }) => {
401 if (_.isFunction(callback)) {
402 callback(resp.valid, resp.credits, resp.valuation);
403 }
404 if (resp.valid) {
405 return null;
406 } else {
407 return { passwordPolicy: true };
408 }
409 }),
410 take(1)
411 );
412 };
413 }
414 }