From be9d3285e1781f8d281cab3eba2f77abd8076e3a Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Mon, 17 Jun 2024 09:24:54 +1200 Subject: [PATCH] fix(custom-templates): add stack validation, remove custom template validation [EE-7102] (#11938) Co-authored-by: testa113 --- app/kubernetes/react/components/index.ts | 9 +++++- .../create/createApplication.html | 2 ++ .../create/createApplicationController.js | 16 ++++++++-- app/kubernetes/views/deploy/deploy.html | 7 ++++- .../views/deploy/deployController.js | 17 ++++++++-- .../common/stacks/CreateView/NameField.tsx | 3 ++ app/react/common/stacks/common/form-texts.tsx | 2 +- app/react/components/CollapseExpandButton.tsx | 2 +- .../DeployView/StackName/StackName.tsx | 12 +++++-- .../DeployView/StackName/constants.ts | 4 +++ .../components/CommonFields.tsx | 31 +++---------------- .../shared/MoreSettingsSection.tsx | 2 +- .../CreateView/useValidation.tsx | 1 - .../EditView/useValidation.tsx | 1 - .../DeployForm.tsx | 3 +- 15 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 app/react/kubernetes/DeployView/StackName/constants.ts diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index 654c3dfb3..7280f5af0 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -151,7 +151,14 @@ export const ngModule = angular ), { stackName: 'setStackName' } ), - ['setStackName', 'stackName', 'stacks', 'inputClassName', 'textTip'] + [ + 'setStackName', + 'stackName', + 'stacks', + 'inputClassName', + 'textTip', + 'error', + ] ) ) .component( diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 23ca602cf..0dffb4380 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -172,6 +172,7 @@ text-tip="'Enter or select a \'stack\' name to group multiple deployments together, or else leave empty to ignore.'" stacks="ctrl.stacks" input-class-name="'col-lg-10 col-sm-9'" + error="ctrl.state.stackNameError" > @@ -234,6 +235,7 @@ text-tip="'Enter or select a \'stack\' name to group multiple deployments together, or else leave empty to ignore.'" stacks="ctrl.stacks" input-class-name="'col-lg-10 col-sm-9'" + error="ctrl.state.stackNameError" > diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index eaeecf921..5103aad29 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -28,6 +28,7 @@ import { confirmUpdateAppIngress } from '@/react/kubernetes/applications/CreateV import { confirm, confirmUpdate, confirmWebEditorDiscard } from '@@/modals/confirm'; import { buildConfirmButton } from '@@/modals/utils'; import { ModalType } from '@@/modals'; +import { KUBE_STACK_NAME_VALIDATION_REGEX } from '@/react/kubernetes/DeployView/StackName/constants'; class KubernetesCreateApplicationController { /* #region CONSTRUCTOR */ @@ -127,6 +128,7 @@ class KubernetesCreateApplicationController { // a validation message will be shown. isExistingCPUReservationUnchanged and isExistingMemoryReservationUnchanged (with available resources being exceeded) is used to decide whether to show the message or not. isExistingCPUReservationUnchanged: false, isExistingMemoryReservationUnchanged: false, + stackNameError: '', }; this.isAdmin = this.Authentication.isAdmin(); @@ -186,9 +188,16 @@ class KubernetesCreateApplicationController { } /* #endregion */ - onChangeStackName(stackName) { + onChangeStackName(name) { return this.$async(async () => { - this.formValues.StackName = stackName; + if (KUBE_STACK_NAME_VALIDATION_REGEX.test(name) || name === '') { + this.state.stackNameError = ''; + } else { + this.state.stackNameError = + "Stack must consist of alphanumeric characters, '-', '_' or '.', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123')."; + } + + this.formValues.StackName = name; }); } @@ -649,7 +658,8 @@ class KubernetesCreateApplicationController { const invalid = !this.isValid(); const hasNoChanges = this.isEditAndNoChangesMade(); const nonScalable = this.isNonScalable(); - return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable; + const stackNameInvalid = this.state.stackNameError !== ''; + return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || stackNameInvalid; } isUpdateApplicationViaWebEditorButtonDisabled() { diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html index 8b94cb4bb..5fdc53f06 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -92,7 +92,12 @@
- + diff --git a/app/kubernetes/views/deploy/deployController.js b/app/kubernetes/views/deploy/deployController.js index f214e27fe..50c923d58 100644 --- a/app/kubernetes/views/deploy/deployController.js +++ b/app/kubernetes/views/deploy/deployController.js @@ -12,6 +12,7 @@ import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/p import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper'; import { confirmWebEditorDiscard } from '@@/modals/confirm'; import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField'; +import { KUBE_STACK_NAME_VALIDATION_REGEX } from '@/react/kubernetes/DeployView/StackName/constants'; class KubernetesDeployController { /* @ngInject */ @@ -57,6 +58,7 @@ class KubernetesDeployController { templateLoadFailed: false, isEditorReadOnly: false, selectedHelmChart: '', + stackNameError: '', }; this.currentUser = { @@ -117,7 +119,16 @@ class KubernetesDeployController { } setStackName(name) { - this.formValues.StackName = name; + return this.$async(async () => { + if (KUBE_STACK_NAME_VALIDATION_REGEX.test(name) || name === '') { + this.state.stackNameError = ''; + } else { + this.state.stackNameError = + "Stack must consist of alphanumeric characters, '-', '_' or '.', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123')."; + } + + this.formValues.StackName = name; + }); } renderTemplate() { @@ -197,9 +208,9 @@ class KubernetesDeployController { const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent); const isURLFormInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.URL && _.isEmpty(this.formValues.ManifestURL); const isCustomTemplateInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.CUSTOM_TEMPLATE && _.isEmpty(this.formValues.EditorContent); - const isNamespaceInvalid = _.isEmpty(this.formValues.Namespace); - return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this.state.actionInProgress || isNamespaceInvalid; + const isStackNameInvalid = this.state.stackNameError !== ''; + return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this.state.actionInProgress || isNamespaceInvalid || isStackNameInvalid; } onChangeFormValues(newValues) { diff --git a/app/react/common/stacks/CreateView/NameField.tsx b/app/react/common/stacks/CreateView/NameField.tsx index 59f197220..262e6cbdb 100644 --- a/app/react/common/stacks/CreateView/NameField.tsx +++ b/app/react/common/stacks/CreateView/NameField.tsx @@ -14,10 +14,12 @@ export function NameField({ onChange, value, errors, + placeholder, }: { onChange(value: string): void; value: string; errors?: FormikErrors; + placeholder?: string; }) { return ( @@ -25,6 +27,7 @@ export function NameField({ id="name-input" onChange={(e) => onChange(e.target.value)} value={value} + placeholder={placeholder} required data-cy="stack-name-input" /> diff --git a/app/react/common/stacks/common/form-texts.tsx b/app/react/common/stacks/common/form-texts.tsx index 6423092c7..3ccf82973 100644 --- a/app/react/common/stacks/common/form-texts.tsx +++ b/app/react/common/stacks/common/form-texts.tsx @@ -33,7 +33,7 @@ export const textByType = { (Deployment, Secret, ConfigMap...)

- You can get more information about Kubernetes file format in the + You can get more information about Kubernetes file format in the{' '} diff --git a/app/react/kubernetes/DeployView/StackName/StackName.tsx b/app/react/kubernetes/DeployView/StackName/StackName.tsx index 1980a9d11..c6930a1e4 100644 --- a/app/react/kubernetes/DeployView/StackName/StackName.tsx +++ b/app/react/kubernetes/DeployView/StackName/StackName.tsx @@ -6,6 +6,7 @@ import { Link } from '@@/Link'; import { TextTip } from '@@/Tip/TextTip'; import { Tooltip } from '@@/Tip/Tooltip'; import { AutocompleteSelect } from '@@/form-components/AutocompleteSelect'; +import { FormError } from '@@/form-components/FormError'; type Props = { stackName: string; @@ -13,6 +14,7 @@ type Props = { stacks?: string[]; inputClassName?: string; textTip?: string; + error?: string; }; export function StackName({ @@ -21,6 +23,7 @@ export function StackName({ stacks = [], inputClassName, textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.", + error = '', }: Props) { const isAdminQuery = useIsEdgeAdmin(); const stackResults = useMemo( @@ -54,9 +57,11 @@ export function StackName({ return ( <> - - {textTip} - + {textTip ? ( + + {textTip} + + ) : null}

diff --git a/app/react/kubernetes/DeployView/StackName/constants.ts b/app/react/kubernetes/DeployView/StackName/constants.ts new file mode 100644 index 000000000..5c2a01801 --- /dev/null +++ b/app/react/kubernetes/DeployView/StackName/constants.ts @@ -0,0 +1,4 @@ +// this regex is to satisfy k8s label validation rules +// alphanumeric, lowercase, uppercase, can contain dashes, dots and underscores, max 63 characters +export const KUBE_STACK_NAME_VALIDATION_REGEX = + /^(([a-zA-Z0-9](?:(?:[-a-zA-Z0-9_.]){0,61}[a-zA-Z0-9])?))$/; diff --git a/app/react/portainer/custom-templates/components/CommonFields.tsx b/app/react/portainer/custom-templates/components/CommonFields.tsx index ae834f4a0..4b44589c9 100644 --- a/app/react/portainer/custom-templates/components/CommonFields.tsx +++ b/app/react/portainer/custom-templates/components/CommonFields.tsx @@ -95,14 +95,10 @@ export function CommonFields({ export function validation({ currentTemplateId, templates = [], - viewType = 'docker', }: { currentTemplateId?: CustomTemplate['Id']; templates?: Array; - viewType?: 'kube' | 'docker' | 'edge'; } = {}): SchemaOf { - const titlePattern = titlePatternValidation(viewType); - return object({ Title: string() .required('Title is required.') @@ -116,31 +112,12 @@ export function validation({ template.Title === value && template.Id !== currentTemplateId ) ) - .matches(titlePattern.pattern, titlePattern.error), + .max( + 200, + 'Custom template title must be less than or equal to 200 characters' + ), Description: string().required('Description is required.'), Note: string().default(''), Logo: string().default(''), }); } - -export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; - -const KUBE_TEMPLATE_NAME_VALIDATION_REGEX = - '^(([a-z0-9](?:(?:[-a-z0-9_.]){0,61}[a-z0-9])?))$'; // alphanumeric, lowercase, can contain dashes, dots and underscores, max 63 characters - -function titlePatternValidation(type: 'kube' | 'docker' | 'edge') { - switch (type) { - case 'kube': - return { - pattern: new RegExp(KUBE_TEMPLATE_NAME_VALIDATION_REGEX), - error: - "This field must consist of lower-case alphanumeric characters, '.', '_' or '-', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').", - }; - default: - return { - pattern: new RegExp(TEMPLATE_NAME_VALIDATION_REGEX), - error: - "This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').", - }; - } -} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx index 1e1427c7e..089d37672 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MoreSettingsSection.tsx @@ -6,7 +6,7 @@ import { MetadataFieldset } from './MetadataFieldset'; export function MoreSettingsSection({ children }: PropsWithChildren) { return ( - +
{children} diff --git a/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx b/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx index 6b9bab0fb..56b73c627 100644 --- a/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx +++ b/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx @@ -72,7 +72,6 @@ export function useValidation({ }).concat( commonFieldsValidation({ templates: customTemplatesQuery.data, - viewType, }) ), [ diff --git a/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx b/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx index dc6a7f461..5aa012e81 100644 --- a/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx +++ b/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx @@ -62,7 +62,6 @@ export function useValidation({ commonFieldsValidation({ templates: customTemplatesQuery.data, currentTemplateId: templateId, - viewType, }) ), [ diff --git a/app/react/portainer/templates/custom-templates/ListView/StackFromCustomTemplateFormWidget/DeployForm.tsx b/app/react/portainer/templates/custom-templates/ListView/StackFromCustomTemplateFormWidget/DeployForm.tsx index 51ec1748e..983a8cf19 100644 --- a/app/react/portainer/templates/custom-templates/ListView/StackFromCustomTemplateFormWidget/DeployForm.tsx +++ b/app/react/portainer/templates/custom-templates/ListView/StackFromCustomTemplateFormWidget/DeployForm.tsx @@ -63,7 +63,7 @@ export function DeployForm({ const isGit = !!template.GitConfig; const initialValues: FormValues = { - name: template.Title || '', + name: '', variables: getVariablesFieldDefaultValues(template.Variables), accessControl: parseAccessControlFormData( isEdgeAdminQuery.isAdmin, @@ -86,6 +86,7 @@ export function DeployForm({ value={values.name} onChange={(v) => setFieldValue('name', v)} errors={errors.name} + placeholder="e.g. mystack" />