diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js index 3e6632b9b..2535d1ae3 100644 --- a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js +++ b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js @@ -21,7 +21,6 @@ class KubeCreateCustomTemplateViewController { formValidationError: '', isEditorDirty: false, isTemplateValid: true, - templateNameRegex: KUBE_TEMPLATE_NAME_VALIDATION_REGEX, }; this.formValues = { @@ -42,12 +41,30 @@ class KubeCreateCustomTemplateViewController { ComposeFilePathInRepository: 'manifest.yml', }; + this.validationData = { + title: { + pattern: 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').", + }, + }; + this.onChangeFile = this.onChangeFile.bind(this); this.onChangeFileContent = this.onChangeFileContent.bind(this); this.onChangeMethod = this.onChangeMethod.bind(this); this.onBeforeOnload = this.onBeforeOnload.bind(this); this.handleChange = this.handleChange.bind(this); this.onVariablesChange = this.onVariablesChange.bind(this); + this.onChangePlatform = this.onChangePlatform.bind(this); + this.onChangeType = this.onChangeType.bind(this); + } + + onChangePlatform(value) { + this.handleChange({ Platform: value }); + } + + onChangeType(value) { + this.handleChange({ Type: value }); } onChangeMethod(method) { diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html index 2e25b8ead..fdfaab333 100644 --- a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html +++ b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html @@ -5,11 +5,7 @@
- +
Build method
diff --git a/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js b/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js index d889abb68..100e16dc5 100644 --- a/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js +++ b/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.controller.js @@ -16,6 +16,10 @@ class KubeEditCustomTemplateViewController { this.formValues = { Variables: [], TLSSkipVerify: false, + Title: '', + Description: '', + Note: '', + Logo: '', }; this.state = { formValidationError: '', @@ -25,10 +29,17 @@ class KubeEditCustomTemplateViewController { templateLoadFailed: false, templatePreviewFailed: false, templatePreviewError: '', - templateNameRegex: KUBE_TEMPLATE_NAME_VALIDATION_REGEX, }; this.templates = []; + this.validationData = { + title: { + pattern: 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').", + }, + }; + this.getTemplate = this.getTemplate.bind(this); this.submitAction = this.submitAction.bind(this); this.onChangeFileContent = this.onChangeFileContent.bind(this); @@ -36,6 +47,16 @@ class KubeEditCustomTemplateViewController { this.handleChange = this.handleChange.bind(this); this.onVariablesChange = this.onVariablesChange.bind(this); this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this); + this.onChangePlatform = this.onChangePlatform.bind(this); + this.onChangeType = this.onChangeType.bind(this); + } + + onChangePlatform(value) { + this.handleChange({ Platform: value }); + } + + onChangeType(value) { + this.handleChange({ Type: value }); } getTemplate() { diff --git a/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.html b/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.html index 353cc3f2e..3a5c68d24 100644 --- a/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.html +++ b/app/kubernetes/custom-templates/kube-edit-custom-template-view/kube-edit-custom-template-view.html @@ -5,11 +5,7 @@ - + diff --git a/app/portainer/components/custom-template-common-fields/customTemplateCommonFields.html b/app/portainer/components/custom-template-common-fields/customTemplateCommonFields.html deleted file mode 100644 index f21ce56a9..000000000 --- a/app/portainer/components/custom-template-common-fields/customTemplateCommonFields.html +++ /dev/null @@ -1,87 +0,0 @@ - - -
- -
- - -
-
-
-

Title is required.

-

- - {{ $ctrl.nameRegexError }} -

-
-
-
-
-
-
- - - -
- -
- - -
-
-
-

Description is required.

-
-
-
-
-
-
- - - -
- -
- -
-
- - - -
- -
- -
-
- - - -
- -
- -
-
- - - -
- -
- -
-
- -
diff --git a/app/portainer/components/custom-template-common-fields/customTemplateCommonFieldsController.js b/app/portainer/components/custom-template-common-fields/customTemplateCommonFieldsController.js deleted file mode 100644 index cf5aee110..000000000 --- a/app/portainer/components/custom-template-common-fields/customTemplateCommonFieldsController.js +++ /dev/null @@ -1,16 +0,0 @@ -class CustomTemplateCommonFieldsController { - /* @ngInject */ - constructor() { - this.platformTypes = [ - { label: 'Linux', value: 1 }, - { label: 'Windows', value: 2 }, - ]; - - this.templateTypes = [ - { label: 'Swarm', value: 1 }, - { label: 'Standalone', value: 2 }, - ]; - } -} - -export default CustomTemplateCommonFieldsController; diff --git a/app/portainer/components/custom-template-common-fields/index.js b/app/portainer/components/custom-template-common-fields/index.js deleted file mode 100644 index a1acca346..000000000 --- a/app/portainer/components/custom-template-common-fields/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import CustomTemplateCommonFieldsController from './customTemplateCommonFieldsController.js'; - -angular.module('portainer.app').component('customTemplateCommonFields', { - templateUrl: './customTemplateCommonFields.html', - controller: CustomTemplateCommonFieldsController, - bindings: { - formValues: '=', - showPlatformField: '<', - showTypeField: '<', - nameRegex: '<', - nameRegexError: '<', - }, -}); diff --git a/app/portainer/react/components/custom-templates/index.ts b/app/portainer/react/components/custom-templates/index.ts index 18c1cdfcb..488bf752d 100644 --- a/app/portainer/react/components/custom-templates/index.ts +++ b/app/portainer/react/components/custom-templates/index.ts @@ -8,10 +8,17 @@ import { CustomTemplatesListItem } from '@/react/portainer/templates/custom-temp import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { withUIRouter } from '@/react-tools/withUIRouter'; import { AppTemplatesListItem } from '@/react/portainer/templates/app-templates/AppTemplatesListItem'; +import { + CommonFields, + validation as commonFieldsValidation, +} from '@/react/portainer/custom-templates/components/CommonFields'; +import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector'; +import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector'; +import { withFormValidation } from '@/react-tools/withFormValidation'; import { VariablesFieldAngular } from './variables-field'; -export const customTemplatesModule = angular +export const ngModule = angular .module('portainer.app.react.components.custom-templates', []) .component( 'customTemplatesVariablesFieldReact', @@ -48,4 +55,22 @@ export const customTemplatesModule = angular 'isSelected', 'onDuplicate', ]) - ).name; + ) + .component( + 'customTemplatesPlatformSelector', + r2a(PlatformField, ['onChange', 'value']) + ) + .component( + 'customTemplatesTypeSelector', + r2a(TemplateTypeSelector, ['onChange', 'value']) + ); + +withFormValidation( + ngModule, + withControlledInput(CommonFields, { values: 'onChange' }), + 'customTemplatesCommonFields', + [], + commonFieldsValidation +); + +export const customTemplatesModule = ngModule.name; diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html index 5c631880c..c1a61e82e 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html @@ -5,13 +5,11 @@ - + + + + +
diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js index 8a39edbe7..068491039 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js @@ -54,10 +54,16 @@ class CreateCustomTemplateViewController { fromStack: false, loading: true, isEditorDirty: false, - templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX, isTemplateValid: true, }; + this.validationData = { + title: { + pattern: TEMPLATE_NAME_VALIDATION_REGEX, + error: "This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').", + }, + }; + this.templates = []; this.createCustomTemplate = this.createCustomTemplate.bind(this); @@ -71,12 +77,22 @@ class CreateCustomTemplateViewController { this.onChangeMethod = this.onChangeMethod.bind(this); this.onVariablesChange = this.onVariablesChange.bind(this); this.handleChange = this.handleChange.bind(this); + this.onChangePlatform = this.onChangePlatform.bind(this); + this.onChangeType = this.onChangeType.bind(this); } onVariablesChange(value) { this.handleChange({ Variables: value }); } + onChangePlatform(value) { + this.handleChange({ Platform: value }); + } + + onChangeType(value) { + this.handleChange({ Type: value }); + } + handleChange(values) { return this.$async(async () => { this.formValues = { diff --git a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html index 39cfc3a00..ada199ecd 100644 --- a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html +++ b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html @@ -5,13 +5,11 @@ - + + + + + diff --git a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js index a7a27b60f..e19731df2 100644 --- a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js +++ b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js @@ -18,7 +18,12 @@ class EditCustomTemplateViewController { this.formValues = { Variables: [], TLSSkipVerify: false, + Title: '', + Description: '', + Note: '', + Logo: '', }; + this.state = { formValidationError: '', isEditorDirty: false, @@ -27,8 +32,15 @@ class EditCustomTemplateViewController { templateLoadFailed: false, templatePreviewFailed: false, templatePreviewError: '', - templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX, }; + + this.validationData = { + title: { + pattern: TEMPLATE_NAME_VALIDATION_REGEX, + error: "This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').", + }, + }; + this.templates = []; this.getTemplate = this.getTemplate.bind(this); @@ -39,6 +51,16 @@ class EditCustomTemplateViewController { this.onVariablesChange = this.onVariablesChange.bind(this); this.handleChange = this.handleChange.bind(this); this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this); + this.onChangePlatform = this.onChangePlatform.bind(this); + this.onChangeType = this.onChangeType.bind(this); + } + + onChangePlatform(value) { + this.handleChange({ Platform: value }); + } + + onChangeType(value) { + this.handleChange({ Type: value }); } getTemplate() { diff --git a/app/react/portainer/custom-templates/components/CommonFields.tsx b/app/react/portainer/custom-templates/components/CommonFields.tsx new file mode 100644 index 000000000..e1d0e5d81 --- /dev/null +++ b/app/react/portainer/custom-templates/components/CommonFields.tsx @@ -0,0 +1,106 @@ +import { SchemaOf, object, string } from 'yup'; +import { FormikErrors } from 'formik'; + +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; + +interface Values { + Title: string; + Description: string; + Note: string; + Logo: string; +} + +export function CommonFields({ + values, + onChange, + errors, +}: { + values: Values; + onChange: (values: Values) => void; + errors?: FormikErrors; +}) { + return ( + <> + + { + handleChange({ Title: e.target.value }); + }} + /> + + + + { + handleChange({ Description: e.target.value }); + }} + /> + + + + { + handleChange({ Note: e.target.value }); + }} + /> + + + + { + handleChange({ Logo: e.target.value }); + }} + /> + + + ); + + function handleChange(change: Partial) { + onChange({ ...values, ...change }); + } +} + +export function validation({ + title, +}: { + title?: { pattern: string; error: string }; +} = {}): SchemaOf { + let titleSchema = string().required('Title is required.'); + if (title?.pattern) { + const pattern = new RegExp(title.pattern); + titleSchema = titleSchema.matches(pattern, title.error); + } + + return object({ + Title: titleSchema, + Description: string().required('Description is required.'), + Note: string().default(''), + Logo: string().default(''), + }); +} diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/index.ts b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/index.ts index 436e59600..22d6a0afc 100644 --- a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/index.ts +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/index.ts @@ -1 +1,2 @@ export { CustomTemplatesVariablesDefinitionField } from './CustomTemplatesVariablesDefinitionField'; +export type { VariableDefinition } from './CustomTemplatesVariablesDefinitionField'; diff --git a/app/react/portainer/custom-templates/components/PlatformSelector.tsx b/app/react/portainer/custom-templates/components/PlatformSelector.tsx new file mode 100644 index 000000000..f08addba2 --- /dev/null +++ b/app/react/portainer/custom-templates/components/PlatformSelector.tsx @@ -0,0 +1,30 @@ +import { FormControl } from '@@/form-components/FormControl'; +import { Select } from '@@/form-components/Input'; + +import { Platform } from '../types'; + +const platformOptions = [ + { label: 'Linux', value: Platform.LINUX }, + { label: 'Windows', value: Platform.WINDOWS }, +]; + +export function PlatformField({ + onChange, + value, +}: { + onChange: (platform: Platform) => void; + value: Platform; +}) { + return ( + + onChange(parseInt(e.target.value, 10))} + /> + + ); +} diff --git a/app/react/portainer/custom-templates/types.ts b/app/react/portainer/custom-templates/types.ts index 788604443..6316ebcd3 100644 --- a/app/react/portainer/custom-templates/types.ts +++ b/app/react/portainer/custom-templates/types.ts @@ -4,19 +4,14 @@ import { StackType } from '@/react/common/stacks/types'; import { ResourceControlResponse } from '../access-control/types'; import { RepoConfigResponse } from '../gitops/types'; +import { VariableDefinition } from './components/CustomTemplatesVariablesDefinitionField'; + export enum Platform { LINUX = 1, WINDOWS, } -export /** - * CustomTemplate represents a custom template. - */ -interface CustomTemplate { - /** - * CustomTemplate Identifier. - * @example 1 - */ +export type CustomTemplate = { Id: number; /** @@ -82,6 +77,8 @@ interface CustomTemplate { */ ResourceControl?: ResourceControlResponse; + Variables: VariableDefinition[]; + /** * GitConfig for the template. */ @@ -92,7 +89,7 @@ interface CustomTemplate { * @example false */ IsComposeFormat: boolean; -} +}; export type CustomTemplateFileContent = { FileContent: string;