mirror of https://github.com/portainer/portainer
refactor(r2aform): remove validationData [EE-5559] (#9045)
* refactor(r2aform): remove validationData [EE-5559] * update doc --------- Co-authored-by: testa113 <testa113>pull/9064/head
parent
834ab7c158
commit
4a331b71e1
|
@ -1150,7 +1150,7 @@
|
||||||
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
|
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
|
||||||
app-name="ctrl.formValues.Name"
|
app-name="ctrl.formValues.Name"
|
||||||
selector="ctrl.formValues.Selector"
|
selector="ctrl.formValues.Selector"
|
||||||
validation-context="{nodePortServices: ctrl.state.nodePortServices}"
|
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services}"
|
||||||
is-edit-mode="ctrl.state.isEdit"
|
is-edit-mode="ctrl.state.isEdit"
|
||||||
></kube-services-form>
|
></kube-services-form>
|
||||||
<!-- kubernetes services options -->
|
<!-- kubernetes services options -->
|
||||||
|
@ -1200,7 +1200,7 @@
|
||||||
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
|
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
|
||||||
app-name="ctrl.formValues.Name"
|
app-name="ctrl.formValues.Name"
|
||||||
selector="ctrl.formValues.Selector"
|
selector="ctrl.formValues.Selector"
|
||||||
validation-context="{nodePortServices: ctrl.state.nodePortServices}"
|
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services}"
|
||||||
is-edit-mode="ctrl.state.isEdit"
|
is-edit-mode="ctrl.state.isEdit"
|
||||||
></kube-services-form>
|
></kube-services-form>
|
||||||
<!-- kubernetes services options -->
|
<!-- kubernetes services options -->
|
||||||
|
|
|
@ -13,38 +13,41 @@ interface FormFieldProps<TValue> {
|
||||||
onChange(values: TValue): void;
|
onChange(values: TValue): void;
|
||||||
values: TValue;
|
values: TValue;
|
||||||
errors?: FormikErrors<TValue> | ArrayError<TValue>;
|
errors?: FormikErrors<TValue> | ArrayError<TValue>;
|
||||||
validationContext?: object; // optional context to pass to yup validation, for example, external data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WithFormFieldProps<TProps, TValue> = TProps & FormFieldProps<TValue>;
|
type WithFormFieldProps<TProps, TValue> = TProps & FormFieldProps<TValue>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function to use for wrapping react components with form validation
|
* This utility function is used for wrapping React components with form validation.
|
||||||
* when used inside an angular form, it will set the form to invalid if the component values are invalid.
|
* When used inside an Angular form, it sets the form to invalid if the component values are invalid.
|
||||||
|
* This function registers two AngularJS components:
|
||||||
|
* 1. The React component with r2a wrapping:
|
||||||
|
* - `onChange` and `values` must be manually passed to the React component from an Angular view.
|
||||||
|
* - `errors` will be automatically passed to the React component and updated by the validation component.
|
||||||
|
* 2. An AngularJS component that handles form validation, based on a yup validation schema:
|
||||||
|
* - `validationData` can optionally be passed to the React component from an Angular view, which can be used in validation.
|
||||||
*
|
*
|
||||||
* this registers two angularjs components:
|
* @example
|
||||||
* 1. the react component with r2a wrapping
|
* // Usage in Angular view
|
||||||
* 2. an angularjs component that handles form validation
|
* <component
|
||||||
|
* values="ctrl.values"
|
||||||
|
* on-change="ctrl.handleChange"
|
||||||
|
* validation-data="ctrl.validationData">
|
||||||
|
* </component>
|
||||||
*/
|
*/
|
||||||
export function withFormValidation<TProps, TValue, TData = never>(
|
export function withFormValidation<TProps, TValue, TData = never>(
|
||||||
ngModule: IModule,
|
ngModule: IModule,
|
||||||
Component: ComponentType<WithFormFieldProps<TProps, TValue>>,
|
Component: ComponentType<WithFormFieldProps<TProps, TValue>>,
|
||||||
componentName: string,
|
componentName: string,
|
||||||
propNames: PropNames<TProps>[],
|
propNames: PropNames<TProps>[],
|
||||||
schemaBuilder: (data?: TData) => SchemaOf<TValue>
|
schemaBuilder: (validationData?: TData) => SchemaOf<TValue>
|
||||||
) {
|
) {
|
||||||
const reactComponentName = `react${_.upperFirst(componentName)}`;
|
const reactComponentName = `react${_.upperFirst(componentName)}`;
|
||||||
|
|
||||||
ngModule
|
ngModule
|
||||||
.component(
|
.component(
|
||||||
reactComponentName,
|
reactComponentName,
|
||||||
r2a(Component, [
|
r2a(Component, ['errors', 'onChange', 'values', ...propNames])
|
||||||
'errors',
|
|
||||||
'onChange',
|
|
||||||
'values',
|
|
||||||
'validationContext',
|
|
||||||
...propNames,
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
componentName,
|
componentName,
|
||||||
|
@ -58,11 +61,11 @@ export function withFormValidation<TProps, TValue, TData = never>(
|
||||||
|
|
||||||
export function createFormValidationComponent<TFormModel, TData = never>(
|
export function createFormValidationComponent<TFormModel, TData = never>(
|
||||||
componentName: string,
|
componentName: string,
|
||||||
props: Array<string>,
|
propNames: Array<string>,
|
||||||
schemaBuilder: (data?: TData) => SchemaOf<TFormModel>
|
schemaBuilder: (validationData?: TData) => SchemaOf<TFormModel>
|
||||||
): IComponentOptions {
|
): IComponentOptions {
|
||||||
const kebabName = _.kebabCase(componentName);
|
const kebabName = _.kebabCase(componentName);
|
||||||
const propsWithErrors = [...props, 'errors', 'values'];
|
const propsWithErrors = [...propNames, 'errors', 'values'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
template: `<ng-form name="$ctrl.form">
|
template: `<ng-form name="$ctrl.form">
|
||||||
|
@ -75,18 +78,13 @@ export function createFormValidationComponent<TFormModel, TData = never>(
|
||||||
</ng-form>`,
|
</ng-form>`,
|
||||||
controller: createFormValidatorController(schemaBuilder),
|
controller: createFormValidatorController(schemaBuilder),
|
||||||
bindings: Object.fromEntries(
|
bindings: Object.fromEntries(
|
||||||
[
|
[...propsWithErrors, 'validationData', 'onChange'].map((p) => [p, '<'])
|
||||||
...propsWithErrors,
|
|
||||||
'validationData',
|
|
||||||
'onChange',
|
|
||||||
'validationContext',
|
|
||||||
].map((p) => [p, '<'])
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFormValidatorController<TFormModel, TData = never>(
|
function createFormValidatorController<TFormModel, TData = never>(
|
||||||
schemaBuilder: (data?: TData) => SchemaOf<TFormModel>
|
schemaBuilder: (validationData?: TData) => SchemaOf<TFormModel>
|
||||||
) {
|
) {
|
||||||
return class FormValidatorController {
|
return class FormValidatorController {
|
||||||
errors?: FormikErrors<TFormModel> = {};
|
errors?: FormikErrors<TFormModel> = {};
|
||||||
|
@ -99,8 +97,6 @@ export function createFormValidatorController<TFormModel, TData = never>(
|
||||||
|
|
||||||
validationData?: TData;
|
validationData?: TData;
|
||||||
|
|
||||||
validationContext?: object;
|
|
||||||
|
|
||||||
onChange?: (value: TFormModel) => void;
|
onChange?: (value: TFormModel) => void;
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -114,18 +110,17 @@ export function createFormValidatorController<TFormModel, TData = never>(
|
||||||
async handleChange(newValues: TFormModel) {
|
async handleChange(newValues: TFormModel) {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
this.onChange?.(newValues);
|
this.onChange?.(newValues);
|
||||||
await this.runValidation(newValues, this.validationContext);
|
await this.runValidation(newValues);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runValidation(value: TFormModel, validationContext?: object) {
|
async runValidation(value: TFormModel) {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
this.form?.$setValidity('form', true, this.form);
|
this.form?.$setValidity('form', true, this.form);
|
||||||
|
|
||||||
this.errors = await validateForm<TFormModel>(
|
this.errors = await validateForm<TFormModel>(
|
||||||
() => schemaBuilder(this.validationData),
|
() => schemaBuilder(this.validationData),
|
||||||
value,
|
value
|
||||||
validationContext
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.errors && Object.keys(this.errors).length > 0) {
|
if (this.errors && Object.keys(this.errors).length > 0) {
|
||||||
|
@ -136,10 +131,7 @@ export function createFormValidatorController<TFormModel, TData = never>(
|
||||||
|
|
||||||
async $onChanges(changes: { values?: { currentValue: TFormModel } }) {
|
async $onChanges(changes: { values?: { currentValue: TFormModel } }) {
|
||||||
if (changes.values) {
|
if (changes.values) {
|
||||||
await this.runValidation(
|
await this.runValidation(changes.values.currentValue);
|
||||||
changes.values.currentValue,
|
|
||||||
this.validationContext
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { SchemaOf } from 'yup';
|
||||||
|
|
||||||
export async function validateForm<T>(
|
export async function validateForm<T>(
|
||||||
schemaBuilder: () => SchemaOf<T>,
|
schemaBuilder: () => SchemaOf<T>,
|
||||||
formValues: T,
|
formValues: T
|
||||||
validationContext?: object
|
|
||||||
) {
|
) {
|
||||||
const validationSchema = schemaBuilder();
|
const validationSchema = schemaBuilder();
|
||||||
|
|
||||||
|
@ -12,8 +11,6 @@ export async function validateForm<T>(
|
||||||
await validationSchema.validate(formValues, {
|
await validationSchema.validate(formValues, {
|
||||||
strict: true,
|
strict: true,
|
||||||
abortEarly: false,
|
abortEarly: false,
|
||||||
// workaround to access all parents for nested fields. See clusterIpFormValidation for a use case.
|
|
||||||
context: { formValues, validationContext },
|
|
||||||
});
|
});
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -340,7 +340,9 @@ type NodePortValidationContext = {
|
||||||
formServices: ServiceFormValues[];
|
formServices: ServiceFormValues[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
|
export function kubeServicesValidation(
|
||||||
|
validationData?: NodePortValidationContext
|
||||||
|
): SchemaOf<ServiceFormValues[]> {
|
||||||
return array(
|
return array(
|
||||||
object({
|
object({
|
||||||
Headless: boolean().required(),
|
Headless: boolean().required(),
|
||||||
|
@ -367,20 +369,20 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
|
||||||
.test(
|
.test(
|
||||||
'service-port-is-unique',
|
'service-port-is-unique',
|
||||||
'Service port number must be unique.',
|
'Service port number must be unique.',
|
||||||
// eslint-disable-next-line func-names
|
(servicePort, context) => {
|
||||||
function (servicePort, context) {
|
|
||||||
// test for duplicate service ports within this service.
|
// test for duplicate service ports within this service.
|
||||||
// yup gives access to context.parent which gives one ServicePort object.
|
// yup gives access to context.parent which gives one ServicePort object.
|
||||||
// yup also gives access to all form values through this.options.context.
|
// yup also gives access to all form values through this.options.context.
|
||||||
// Unfortunately, it doesn't give direct access to all Ports within the current service.
|
// Unfortunately, it doesn't give direct access to all Ports within the current service.
|
||||||
// To find all ports in the service for validation, I'm filtering the services by the service name,
|
// To find all ports in the service for validation, I'm filtering the services by the service name,
|
||||||
// that's stored in the ServicePort object, then getting all Ports in the service.
|
// that's stored in the ServicePort object, then getting all Ports in the service.
|
||||||
if (servicePort === undefined) {
|
if (servicePort === undefined || validationData === undefined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const { formServices } = validationData;
|
||||||
const matchingService = getServiceForPort(
|
const matchingService = getServiceForPort(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
this.options.context?.formValues as ServiceFormValues[]
|
formServices
|
||||||
);
|
);
|
||||||
if (matchingService === undefined) {
|
if (matchingService === undefined) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -406,14 +408,14 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
|
||||||
.test(
|
.test(
|
||||||
'node-port-is-unique-in-service',
|
'node-port-is-unique-in-service',
|
||||||
'Node port is already used in this service.',
|
'Node port is already used in this service.',
|
||||||
// eslint-disable-next-line func-names
|
(nodePort, context) => {
|
||||||
function (nodePort, context) {
|
if (nodePort === undefined || validationData === undefined) {
|
||||||
if (nodePort === undefined) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const { formServices } = validationData;
|
||||||
const matchingService = getServiceForPort(
|
const matchingService = getServiceForPort(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
this.options.context?.formValues as ServiceFormValues[]
|
formServices
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
matchingService === undefined ||
|
matchingService === undefined ||
|
||||||
|
@ -432,15 +434,11 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
|
||||||
.test(
|
.test(
|
||||||
'node-port-is-unique-in-cluster',
|
'node-port-is-unique-in-cluster',
|
||||||
'Node port is already used.',
|
'Node port is already used.',
|
||||||
// eslint-disable-next-line func-names
|
(nodePort, context) => {
|
||||||
function (nodePort, context) {
|
if (nodePort === undefined || validationData === undefined) {
|
||||||
if (nodePort === undefined) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const { nodePortServices } = this.options.context
|
const { formServices, nodePortServices } = validationData;
|
||||||
?.validationContext as NodePortValidationContext;
|
|
||||||
const formServices = this.options.context
|
|
||||||
?.formValues as ServiceFormValues[];
|
|
||||||
const matchingService = getServiceForPort(
|
const matchingService = getServiceForPort(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
formServices
|
formServices
|
||||||
|
@ -476,21 +474,21 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
|
||||||
.map((formServicePorts) => formServicePorts.nodePort);
|
.map((formServicePorts) => formServicePorts.nodePort);
|
||||||
return (
|
return (
|
||||||
!clusterNodePortsWithoutFormServices.includes(nodePort) && // node port is not in the cluster services that aren't in the application form
|
!clusterNodePortsWithoutFormServices.includes(nodePort) && // node port is not in the cluster services that aren't in the application form
|
||||||
!formNodePortsWithoutCurrentService.includes(nodePort) // node port is not in the current form, excluding the current service
|
!formNodePortsWithoutCurrentService.includes(nodePort) // and the node port is not in the current form, excluding the current service
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.test(
|
.test(
|
||||||
'node-port-minimum',
|
'node-port-minimum',
|
||||||
'Nodeport number must be inside the range 30000-32767 or blank for system allocated.',
|
'Nodeport number must be inside the range 30000-32767 or blank for system allocated.',
|
||||||
// eslint-disable-next-line func-names
|
(nodePort, context) => {
|
||||||
function (nodePort, context) {
|
if (nodePort === undefined || validationData === undefined) {
|
||||||
if (nodePort === undefined) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const { formServices } = validationData;
|
||||||
const matchingService = getServiceForPort(
|
const matchingService = getServiceForPort(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
this.options.context?.formValues as ServiceFormValues[]
|
formServices
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!matchingService ||
|
!matchingService ||
|
||||||
|
@ -505,14 +503,14 @@ export function kubeServicesValidation(): SchemaOf<ServiceFormValues[]> {
|
||||||
.test(
|
.test(
|
||||||
'node-port-maximum',
|
'node-port-maximum',
|
||||||
'Nodeport number must be inside the range 30000-32767 or blank for system allocated.',
|
'Nodeport number must be inside the range 30000-32767 or blank for system allocated.',
|
||||||
// eslint-disable-next-line func-names
|
(nodePort, context) => {
|
||||||
function (nodePort, context) {
|
if (nodePort === undefined || validationData === undefined) {
|
||||||
if (nodePort === undefined) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const { formServices } = validationData;
|
||||||
const matchingService = getServiceForPort(
|
const matchingService = getServiceForPort(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
this.options.context?.formValues as ServiceFormValues[]
|
formServices
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!matchingService ||
|
!matchingService ||
|
||||||
|
|
Loading…
Reference in New Issue