From ba67f4be3b89cb0cceabe2654264cea958a8e8d9 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 29 Oct 2023 11:01:47 +0200 Subject: [PATCH] fix(custom-templates): render template --- ...-create-custom-template-view.controller.js | 5 +- ...be-edit-custom-template-view.controller.js | 5 +- .../views/deploy/deployController.js | 10 +- .../components/custom-templates/index.ts | 1 + .../createCustomTemplateViewController.js | 5 +- .../customTemplatesViewController.js | 8 +- .../editCustomTemplateViewController.js | 5 +- .../CustomTemplatesVariablesField.stories.tsx | 8 +- .../CustomTemplatesVariablesField.tsx | 125 +++++++++++++----- .../getDefaultValues.ts | 10 ++ .../CustomTemplatesVariablesField/index.ts | 8 +- .../custom-templates/components/utils.ts | 15 ++- 12 files changed, 143 insertions(+), 62 deletions(-) create mode 100644 app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/getDefaultValues.ts 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 ac65f2d04..92d58f218 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 @@ -1,6 +1,5 @@ import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel'; -import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils'; import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods'; import { confirmWebEditorDiscard } from '@@/modals/confirm'; import { KUBE_TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants'; @@ -13,7 +12,7 @@ class KubeCreateCustomTemplateViewController { this.methodOptions = [editor, upload, git]; this.templates = null; - this.isTemplateVariablesEnabled = isBE; + this.isTemplateVariablesEnabled = isTemplateVariablesEnabled; this.state = { method: 'editor', 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 a5ef19ae9..3d74acb4b 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 @@ -1,7 +1,6 @@ import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; -import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils'; +import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils'; import { confirmWebEditorDiscard } from '@@/modals/confirm'; import { getFilePreview } from '@/react/portainer/gitops/gitops.service'; import { KUBE_TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants'; @@ -11,7 +10,7 @@ class KubeEditCustomTemplateViewController { constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) { Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService }); - this.isTemplateVariablesEnabled = isBE; + this.isTemplateVariablesEnabled = isTemplateVariablesEnabled; this.formValues = { Variables: [], diff --git a/app/kubernetes/views/deploy/deployController.js b/app/kubernetes/views/deploy/deployController.js index 65eaf729c..3cb282745 100644 --- a/app/kubernetes/views/deploy/deployController.js +++ b/app/kubernetes/views/deploy/deployController.js @@ -4,14 +4,14 @@ import stripAnsi from 'strip-ansi'; import PortainerError from '@/portainer/error'; import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy'; -import { renderTemplate } from '@/react/portainer/custom-templates/components/utils'; +import { isTemplateVariablesEnabled, renderTemplate } from '@/react/portainer/custom-templates/components/utils'; import { getDeploymentOptions } from '@/react/portainer/environments/environment.service'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods'; import { editor, git, customTemplate, url, helm } from '@@/BoxSelector/common-options/build-methods'; import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils'; import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper'; import { confirmWebEditorDiscard } from '@@/modals/confirm'; +import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField'; class KubernetesDeployController { /* @ngInject */ @@ -25,7 +25,7 @@ class KubernetesDeployController { this.StackService = StackService; this.CustomTemplateService = CustomTemplateService; - this.isTemplateVariablesEnabled = isBE; + this.isTemplateVariablesEnabled = isTemplateVariablesEnabled; this.deployOptions = [{ ...kubernetes, value: KubernetesDeployManifestTypes.KUBERNETES }]; @@ -72,7 +72,7 @@ class KubernetesDeployController { RepositoryPassword: '', AdditionalFiles: [], ComposeFilePathInRepository: '', - Variables: {}, + Variables: [], AutoUpdate: parseAutoUpdateResponse(), TLSSkipVerify: false, Name: '', @@ -220,7 +220,7 @@ class KubernetesDeployController { } if (template.Variables && template.Variables.length > 0) { - const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, ''])); + const variables = getVariablesFieldDefaultValues(template.Variables); this.onChangeTemplateVariables(variables); } } catch (err) { diff --git a/app/portainer/react/components/custom-templates/index.ts b/app/portainer/react/components/custom-templates/index.ts index 333cfe768..7b1b8aedc 100644 --- a/app/portainer/react/components/custom-templates/index.ts +++ b/app/portainer/react/components/custom-templates/index.ts @@ -26,6 +26,7 @@ export const ngModule = angular 'value', 'onChange', 'definitions', + 'errors', ]) ) .component('customTemplatesVariablesField', VariablesFieldAngular) 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 9d4d76182..164321d52 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 @@ -1,8 +1,7 @@ import _ from 'lodash'; import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants'; -import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils'; import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods'; import { confirmWebEditorDiscard } from '@@/modals/confirm'; @@ -25,7 +24,7 @@ class CreateCustomTemplateViewController { this.buildMethods = [editor, upload, git]; - this.isTemplateVariablesEnabled = isBE; + this.isTemplateVariablesEnabled = isTemplateVariablesEnabled; this.formValues = { Title: '', diff --git a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js index 6a6ece2dc..d7b731a18 100644 --- a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js +++ b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js @@ -1,9 +1,9 @@ import _ from 'lodash-es'; import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants'; -import { renderTemplate } from '@/react/portainer/custom-templates/components/utils'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import { isTemplateVariablesEnabled, renderTemplate } from '@/react/portainer/custom-templates/components/utils'; import { confirmDelete } from '@@/modals/confirm'; +import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField'; class CustomTemplatesViewController { /* @ngInject */ @@ -34,7 +34,7 @@ class CustomTemplatesViewController { this.StateManager = StateManager; this.StackService = StackService; - this.isTemplateVariablesEnabled = isBE; + this.isTemplateVariablesEnabled = isTemplateVariablesEnabled; this.DOCKER_STANDALONE = 'DOCKER_STANDALONE'; this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE'; @@ -221,7 +221,7 @@ class CustomTemplatesViewController { this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type); if (template.Variables && template.Variables.length > 0) { - const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, ''])); + const variables = getVariablesFieldDefaultValues(template.Variables); this.onChangeTemplateVariables(variables); } } 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 67171afc8..ed9d7b543 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 @@ -4,8 +4,7 @@ import { ResourceControlViewModel } from '@/react/portainer/access-control/model import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants'; import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; -import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils'; import { confirmWebEditorDiscard } from '@@/modals/confirm'; class EditCustomTemplateViewController { @@ -13,7 +12,7 @@ class EditCustomTemplateViewController { constructor($async, $state, $window, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) { Object.assign(this, { $async, $state, $window, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService }); - this.isTemplateVariablesEnabled = isBE; + this.isTemplateVariablesEnabled = isTemplateVariablesEnabled; this.formValues = { Variables: [], diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.stories.tsx b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.stories.tsx index 4e8364437..df6ce8b73 100644 --- a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.stories.tsx +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.stories.tsx @@ -4,7 +4,7 @@ import { VariableDefinition } from '../CustomTemplatesVariablesDefinitionField/C import { CustomTemplatesVariablesField, - Variables, + Values, } from './CustomTemplatesVariablesField'; export default { @@ -34,10 +34,8 @@ const definitions: VariableDefinition[] = [ ]; function Template() { - const [value, setValue] = useState( - Object.fromEntries( - definitions.map((def) => [def.name, def.defaultValue || '']) - ) + const [value, setValue] = useState( + definitions.map((def) => ({ key: def.name, value: def.defaultValue || '' })) ); return ( diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.tsx b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.tsx index 70fe54477..d27f06573 100644 --- a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.tsx +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.tsx @@ -1,18 +1,24 @@ +import { SchemaOf, array, object, string } from 'yup'; + import { FormControl } from '@@/form-components/FormControl'; import { FormSection } from '@@/form-components/FormSection/FormSection'; import { Input } from '@@/form-components/Input'; +import { ArrayError } from '@@/form-components/InputList/InputList'; +import { FormError } from '@@/form-components/FormError'; import { VariableDefinition } from '../CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField'; -export type Variables = Record; +export type Values = Array<{ key: string; value?: string }>; interface Props { - value: Variables; - definitions?: VariableDefinition[]; - onChange: (value: Variables) => void; + errors?: ArrayError; + value: Values; + definitions: VariableDefinition[] | undefined; + onChange: (value: Values) => void; } export function CustomTemplatesVariablesField({ + errors, value, definitions, onChange, @@ -23,32 +29,91 @@ export function CustomTemplatesVariablesField({ return ( - {definitions.map((def) => { - const inputId = `${def.name}-input`; - const variable = value[def.name] || ''; - return ( - - - onChange({ - ...value, - [def.name]: e.target.value, - }) - } - /> - - ); - })} + {definitions.map((definition, index) => ( + v.key === definition.name)?.value || ''} + error={getError(errors, index)} + onChange={(fieldValue) => { + onChange( + value.map((v) => + v.key === definition.name ? { ...v, value: fieldValue } : v + ) + ); + }} + /> + ))} + + {typeof errors === 'string' && {errors}} ); } + +function VariableFieldItem({ + definition, + value, + error, + onChange, +}: { + definition: VariableDefinition; + value: string; + error?: string; + onChange: (value: string) => void; +}) { + const inputId = `${definition.name}-input`; + + return ( + + onChange(e.target.value)} + /> + + ); +} + +function getError(errors: ArrayError | undefined, index: number) { + if (!errors || typeof errors !== 'object') { + return undefined; + } + + const error = errors[index]; + if (!error) { + return undefined; + } + + return typeof error === 'object' ? error.value : error; +} + +export function validation( + definitions: VariableDefinition[] +): SchemaOf { + return array( + object({ + key: string().required(), + value: string().default(''), + }).test('required-if-no-default-value', 'This field is required', (obj) => { + const definition = definitions.find((d) => d.name === obj.key); + if (!definition) { + return true; + } + + if (!definition.defaultValue && !obj.value) { + return false; + } + + return true; + }) + ); +} diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/getDefaultValues.ts b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/getDefaultValues.ts new file mode 100644 index 000000000..088950b5d --- /dev/null +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/getDefaultValues.ts @@ -0,0 +1,10 @@ +import { VariableDefinition } from '../CustomTemplatesVariablesDefinitionField'; + +import { Values } from './CustomTemplatesVariablesField'; + +export function getDefaultValues(definitions: VariableDefinition[]): Values { + return definitions.map((v) => ({ + key: v.name, + value: v.defaultValue, + })); +} diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/index.ts b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/index.ts index 433d971d0..014903d90 100644 --- a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/index.ts +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/index.ts @@ -1 +1,7 @@ -export { CustomTemplatesVariablesField } from './CustomTemplatesVariablesField'; +export { + CustomTemplatesVariablesField, + type Values as VariablesFieldValue, + validation as variablesFieldValidation, +} from './CustomTemplatesVariablesField'; + +export { getDefaultValues as getVariablesFieldDefaultValues } from './getDefaultValues'; diff --git a/app/react/portainer/custom-templates/components/utils.ts b/app/react/portainer/custom-templates/components/utils.ts index 6fca12f39..df08adcb4 100644 --- a/app/react/portainer/custom-templates/components/utils.ts +++ b/app/react/portainer/custom-templates/components/utils.ts @@ -1,7 +1,12 @@ import _ from 'lodash'; import Mustache from 'mustache'; +import { isBE } from '../../feature-flags/feature-flags.service'; + import { VariableDefinition } from './CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField'; +import { VariablesFieldValue } from './CustomTemplatesVariablesField'; + +export const isTemplateVariablesEnabled = isBE; export function getTemplateVariables(templateStr: string) { const [template, error] = validateAndParse(templateStr); @@ -60,22 +65,22 @@ export function intersectVariables( export function renderTemplate( template: string, - variables: Record, + variables: VariablesFieldValue, definitions: VariableDefinition[] ) { const state = Object.fromEntries( _.compact( - Object.entries(variables).map(([name, value]) => { + variables.map(({ key, value }) => { if (value) { - return [name, value]; + return [key, value]; } - const definition = definitions.find((def) => def.name === name); + const definition = definitions.find((def) => def.name === key); if (!definition) { return null; } - return [name, definition.defaultValue || `{{ ${definition.name} }}`]; + return [key, definition.defaultValue || `{{ ${definition.name} }}`]; }) ) );