diff --git a/package.json b/package.json index 4ee27e39..c28a0f14 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "build:mac": "npm run build && electron-builder --mac --config", "build:linux": "npm run build && electron-builder --linux --config", "test:main": "standard | snazzy && vitest ./test/main --no-threads", - "test": "npm run test:main", + "test:renderer": "standard | snazzy && vitest ./test/renderer --no-threads", + "test": "npm run test:main && npm run test:renderer", "clean": "rm -Rf ./out && rm -Rf ./dist && rm -Rf ./coverage", "release:win": "electron-builder -p always --win --config", "release:linux": "electron-builder -p always --linux --config", diff --git a/src/renderer/src/components/plugins/PluginEnvVarsForm.jsx b/src/renderer/src/components/plugins/PluginEnvVarsForm.jsx index e693ab66..7fdfc307 100644 --- a/src/renderer/src/components/plugins/PluginEnvVarsForm.jsx +++ b/src/renderer/src/components/plugins/PluginEnvVarsForm.jsx @@ -1,67 +1,28 @@ 'use strict' -import React, { useState, useEffect } from 'react' +import React from 'react' import PropTypes from 'prop-types' import { BorderedBox, Forms } from '@platformatic/ui-components' import commonStyles from '~/styles/CommonStyles.module.css' import typographyStyles from '~/styles/Typography.module.css' import { OPACITY_30, TRANSPARENT, WHITE } from '@platformatic/ui-components/src/components/constants' -function PluginEnvVarsForm ({ service, plugin }) { - const [form, setForm] = useState(null) - const [validations, setValidations] = useState({ }) - // eslint-disable-next-line no-unused-vars - const [validForm, setValidForm] = useState(false) - - useEffect(() => { - if (plugin) { - const tmp = {} - const validations = {} - const formErrors = {} - - let envName - plugin.envVars.forEach(envVar => { - envName = envVar.name - tmp[envName] = '' - validations[`${envName}Valid`] = false - formErrors[envName] = '' - }) - setForm({ ...tmp }) - setValidations({ ...validations, formErrors }) - } - }, [plugin]) - - function handleChange (event) { - const value = event.target.value - validateField(event.target.name, value, setForm(form => ({ ...form, [event.target.name]: value }))) - } - - function validateField (fieldName, fieldValue, callback = () => {}) { - let tmpValid = validations[`${fieldName}Valid`] - const formErrors = { ...validations.formErrors } - switch (fieldName) { - default: - tmpValid = fieldValue.length > 0 && /^\S+$/g.test(fieldValue) - formErrors[fieldName] = fieldValue.length > 0 ? (tmpValid ? '' : 'The field is not valid, make sure you are using regular characters') : '' - break - } - const nextValidation = { ...validations, formErrors } - nextValidation[`${fieldName}Valid`] = tmpValid - setValidations(nextValidation) - validateForm(nextValidation, callback()) - } - - function validateForm (validations, callback = () => {}) { - // eslint-disable-next-line no-unused-vars - const { _formErrors, ...restValidations } = validations - const valid = Object.keys(restValidations).findIndex(element => restValidations[element] === false) === -1 - setValidForm(valid) - return callback - } +function PluginEnvVarsForm ({ + configuredServices, + onChange, + templateName, + serviceName, + pluginName +}) { + const configuredServiceFound = configuredServices.find(configuredService => configuredService.template === templateName && configuredService.name === serviceName) + const pluginFound = configuredServiceFound.plugins.find(plugin => plugin.name === pluginName) || {} function renderForm () { - return Object.keys(form).map((element) => ( + if (Object.keys(pluginFound.form).length === 0) { + return <> + } + return Object.keys(pluginFound.form).map((element) => ( env.name === element)?.path} + title={pluginFound.form[element].path} titleColor={WHITE} key={element} > @@ -69,9 +30,9 @@ function PluginEnvVarsForm ({ service, plugin }) { placeholder='Env variable example' name={element} borderColor={WHITE} - value={form[element]} - onChange={handleChange} - errorMessage={validations.formErrors[element]} + value={pluginFound.form[element].value} + onChange={onChange} + errorMessage={pluginFound.validations.formErrors[element]} backgroundTransparent inputTextClassName={`${typographyStyles.desktopBody} ${typographyStyles.textWhite}`} verticalPaddingClassName={commonStyles.noVerticalPadding} @@ -82,6 +43,13 @@ function PluginEnvVarsForm ({ service, plugin }) { )) } + function renderVariablesText () { + if (Object.keys(pluginFound.form).length === 0) { + return This plugin has no configurable variables. + } + return Variables + } + return ( -

{plugin.name} Variables

+

{pluginName} {renderVariablesText()}

- {form && renderForm()} + {pluginFound && renderForm()}
) @@ -99,11 +67,33 @@ function PluginEnvVarsForm ({ service, plugin }) { PluginEnvVarsForm.propTypes = { /** - * plugin + * configuredServices + */ + configuredServices: PropTypes.array.isRequired, + /** + * templateName + */ + templateName: PropTypes.string, + /** + * serviceName */ - plugin: PropTypes.object.isRequired + serviceName: PropTypes.string, + /** + * onChange + */ + onChange: PropTypes.func, + /** + * pluginName + */ + pluginName: PropTypes.string + } -// ConfigureEnvVarsPlugins.defaultProps = {} +PluginEnvVarsForm.defaultProps = { + templateName: '', + serviceName: '', + onChange: () => {}, + pluginName: '' +} export default PluginEnvVarsForm diff --git a/src/renderer/src/components/steps/configure-services/ConfigureEnvVarsTemplateAndPlugins.jsx b/src/renderer/src/components/steps/configure-services/ConfigureEnvVarsTemplateAndPlugins.jsx index 482bd4dc..464d660f 100644 --- a/src/renderer/src/components/steps/configure-services/ConfigureEnvVarsTemplateAndPlugins.jsx +++ b/src/renderer/src/components/steps/configure-services/ConfigureEnvVarsTemplateAndPlugins.jsx @@ -11,7 +11,8 @@ import '~/components/component.animation.css' function ConfigureEnvVarsTemplateAndPlugins ({ configuredServices, - handleChangeTemplateForm + handleChangeTemplateForm, + handleChangePluginForm }) { const [serviceSelected, setServiceSelected] = useState(null) const [pluginSelected, setPluginSelected] = useState(null) @@ -27,10 +28,22 @@ function ConfigureEnvVarsTemplateAndPlugins ({ return handleChangeTemplateForm(event, serviceSelected.template, serviceSelected.name) } + function handleChangePluginEnvVars (event) { + return handleChangePluginForm(event, serviceSelected.template, serviceSelected.name, pluginSelected.name) + } + useEffect(() => { if (serviceSelected) { if (pluginSelected) { - setCurrentComponent() + setCurrentComponent( + ) } else { setCurrentComponent( {} + handleChangeTemplateForm: () => {}, + handleChangePluginForm: () => {} } export default ConfigureEnvVarsTemplateAndPlugins diff --git a/src/renderer/src/components/steps/configure-services/ConfigureServices.jsx b/src/renderer/src/components/steps/configure-services/ConfigureServices.jsx index 5d0a1bcb..e9ea106e 100644 --- a/src/renderer/src/components/steps/configure-services/ConfigureServices.jsx +++ b/src/renderer/src/components/steps/configure-services/ConfigureServices.jsx @@ -10,6 +10,7 @@ import useStackablesStore from '~/useStackablesStore' import Title from '~/components/ui/Title' import '~/components/component.animation.css' import ConfigureEnvVarsTemplateAndPlugins from './ConfigureEnvVarsTemplateAndPlugins' +import { generateForm, preapareFormForCreateApplication } from '../../../utils' const ConfigureServices = React.forwardRef(({ onNext, onBack }, ref) => { const globalState = useStackablesStore() @@ -19,51 +20,7 @@ const ConfigureServices = React.forwardRef(({ onNext, onBack }, ref) => { useEffect(() => { if (services.length > 0) { - const tmpServices = [] - let tmpTemplateForms = {} - let tmpTemplateValidations = {} - let tmpTemplateValidForm = {} - let tmpObj = {} - services.forEach(service => { - tmpTemplateForms = {} - tmpTemplateValidations = {} - tmpTemplateValidForm = {} - tmpObj = {} - - tmpObj.name = service.name - tmpObj.template = service.template.name - let form - let validations - let formErrors - - if (service.template.envVars.length > 0) { - form = {} - validations = {} - formErrors = {} - service.template.envVars.forEach(envVar => { - const { var: envName, configValue, type, default: envDefault, label } = envVar - form[envName] = { - label, - var: envName, - value: envDefault || '', - configValue, - type - } - validations[`${envName}Valid`] = envDefault !== '' - formErrors[envName] = '' - }) - tmpTemplateForms = { ...form } - tmpTemplateValidations = { ...validations, formErrors } - tmpTemplateValidForm = Object.keys(validations).findIndex(element => validations[element] === false) === -1 - } - tmpObj.form = { ...tmpTemplateForms } - tmpObj.validations = { ...tmpTemplateValidations } - tmpObj.validForm = tmpTemplateValidForm - tmpObj.updatedAt = new Date().toISOString() - tmpObj.plugins = [] - tmpServices.push(tmpObj) - }) - setConfiguredServices(tmpServices) + setConfiguredServices(generateForm(services)) } }, [services]) @@ -74,16 +31,8 @@ const ConfigureServices = React.forwardRef(({ onNext, onBack }, ref) => { }, [configuredServices]) function onClickConfigureApplication () { - const services = configuredServices.map(({ name, template, form }) => ({ - name, - template, - fields: Object.keys(form).map(k => { - const { label, ...rest } = form[k] - return { ...rest } - }) - })) addFormData({ - configuredServices: { services } + configuredServices: { services: preapareFormForCreateApplication(configuredServices) } }) onNext() } @@ -92,32 +41,68 @@ const ConfigureServices = React.forwardRef(({ onNext, onBack }, ref) => { const fieldName = event.target.name const fieldValue = event.target.value const { form: newForm, validations: newValidations } = configuredServices.find(configuredService => configuredService.name === serviceName && configuredService.template === templateName) - + let tmpValid newForm[fieldName].value = fieldValue - - let tmpValid = newValidations[`${fieldName}Valid`] - const formErrors = { ...newValidations.formErrors } switch (fieldName) { default: tmpValid = fieldValue.length > 0 && /^\S+$/g.test(fieldValue) - formErrors[fieldName] = fieldValue.length > 0 ? (tmpValid ? '' : 'The field is not valid, make sure you are using regular characters') : '' + newValidations[`${fieldName}Valid`] = tmpValid + newValidations.formErrors[fieldName] = tmpValid ? '' : 'The field is not valid, make sure you are using regular characters' break } - const nextValidation = { ...newValidations, formErrors } - nextValidation[`${fieldName}Valid`] = tmpValid - - const newFormValid = Object.keys(nextValidation).findIndex(element => nextValidation[element] === false) === -1 - setConfiguredServices(configuredServices => { return [...configuredServices.map(configuredService => { if (configuredService.name === serviceName && configuredService.template === templateName) { const { form, validations, validForm, ...rest } = configuredService const newObject = { + ...rest, form: newForm, updatedAt: new Date().toISOString(), - validations: nextValidation, - validForm: newFormValid, - ...rest + validations: newValidations, + validForm: Object.keys(newValidations).findIndex(element => newValidations[element] === false) === -1 + } + return newObject + } else { + return configuredService + } + })] + }) + } + + function handleChangePluginForm (event, templateName, serviceName, pluginName) { + const fieldName = event.target.name + const fieldValue = event.target.value + const configuredServiceFound = configuredServices.find(configuredService => configuredService.name === serviceName && configuredService.template === templateName) + const { form: newForm, validations: newValidations } = configuredServiceFound.plugins.find(plugin => plugin.name === pluginName) + + newForm[fieldName].value = fieldValue + + const tmpValid = fieldValue.length > 0 && /^\S+$/g.test(fieldValue) + newValidations[`${fieldName}Valid`] = tmpValid + newValidations.formErrors[fieldName] = tmpValid ? '' : 'The field is not valid, make sure you are using regular characters' + + const newFormValid = Object.keys(newValidations).findIndex(element => newValidations[element] === false) === -1 + + setConfiguredServices(configuredServices => { + return [...configuredServices.map(configuredService => { + if (configuredService.name === serviceName && configuredService.template === templateName) { + const { plugins, ...rest } = configuredService + const newPlugins = plugins.map(plugin => { + if (plugin.name === pluginName) { + return { + name: pluginName, + form: newForm, + updatedAt: new Date().toISOString(), + validations: newValidations, + validForm: newFormValid + } + } else { + return plugin + } + }) + const newObject = { + ...rest, + plugins: newPlugins } return newObject } else { @@ -143,6 +128,7 @@ const ConfigureServices = React.forwardRef(({ onNext, onBack }, ref) => { diff --git a/src/renderer/src/components/templates/TemplateEnvVarsForm.jsx b/src/renderer/src/components/templates/TemplateEnvVarsForm.jsx index 564fedab..908e2c4a 100644 --- a/src/renderer/src/components/templates/TemplateEnvVarsForm.jsx +++ b/src/renderer/src/components/templates/TemplateEnvVarsForm.jsx @@ -16,7 +16,7 @@ function TemplateEnvVarsForm ({ function renderForm () { if (Object.keys(configuredServiceFound.form).length === 0) { - return

No variables for the template selected!

+ return <> } return Object.keys(configuredServiceFound.form).map((element) => ( This template has no configurable variables. + } + return Variables + } + return ( -

{templateName} Variables

+

{templateName} {renderVariablesText()}

{configuredServiceFound && renderForm()}
diff --git a/src/renderer/src/utils.js b/src/renderer/src/utils.js new file mode 100644 index 00000000..f97c7e55 --- /dev/null +++ b/src/renderer/src/utils.js @@ -0,0 +1,104 @@ +export const generateForm = (services, addUpdatedAt = true) => { + const tmpServices = [] + let tmpTemplateForms = {} + let tmpTemplateValidations = {} + let tmpTemplateValidForm = {} + let tmpObj = {} + + services.forEach(service => { + tmpTemplateForms = {} + tmpTemplateValidations = {} + tmpTemplateValidForm = {} + tmpObj = {} + + tmpObj.name = service.name + tmpObj.template = service.template.name + let form + let validations + let formErrors + + if (service.template.envVars.length > 0) { + form = {} + validations = {} + formErrors = {} + service.template.envVars.forEach(envVar => { + const { var: envName, configValue, type, default: envDefault, label } = envVar + const value = envDefault || '' + form[envName] = { + label, + var: envName, + value: String(value), + configValue, + type + } + validations[`${envName}Valid`] = value !== '' + formErrors[envName] = '' + }) + tmpTemplateForms = { ...form } + tmpTemplateValidations = { ...validations, formErrors } + tmpTemplateValidForm = Object.keys(validations).findIndex(element => validations[element] === false) === -1 + } + tmpObj.form = { ...tmpTemplateForms } + tmpObj.validations = { ...tmpTemplateValidations } + tmpObj.validForm = tmpTemplateValidForm + if (addUpdatedAt) { + tmpObj.updatedAt = new Date().toISOString() + } + // handling plugins + tmpObj.plugins = [] + let pluginForm + let pluginValidations + let pluginFormErrors + let tmpPluginObj + + (service?.plugins || []).forEach(plugin => { + tmpPluginObj = {} + pluginForm = {} + pluginValidations = {} + pluginFormErrors = {} + + if (plugin.envVars.length > 0) { + plugin.envVars.forEach(envVar => { + const { name: envName, type, default: envDefault, path } = envVar + const value = envDefault || '' + pluginForm[envName] = { + path, + value, + type + } + pluginValidations[`${envName}Valid`] = value !== '' + pluginFormErrors[envName] = '' + }) + } + tmpPluginObj = { + name: plugin.name, + form: { ...pluginForm }, + validations: { ...pluginValidations, formErrors: { ...pluginFormErrors } }, + validForm: Object.keys(pluginValidations).findIndex(element => pluginValidations[element] === false) === -1 + } + if (addUpdatedAt) { + tmpPluginObj.updatedAt = new Date().toISOString() + } + tmpObj.plugins.push(tmpPluginObj) + }) + tmpServices.push(tmpObj) + }) + return tmpServices +} + +export const preapareFormForCreateApplication = configuredServices => + configuredServices.map(({ name, template, form, plugins }) => ({ + name, + template, + fields: Object.keys(form).map(k => { + const { label, ...rest } = form[k] + return { ...rest } + }), + plugins: plugins.map(plugin => ({ + name: plugin.name, + options: Object.keys(plugin.form).map(k => { + const { type, path, value } = plugin.form[k] + return { type, path, value } + }) + })) + })) diff --git a/test/renderer/utils.test.mjs b/test/renderer/utils.test.mjs new file mode 100644 index 00000000..4ee75fc6 --- /dev/null +++ b/test/renderer/utils.test.mjs @@ -0,0 +1,302 @@ +import { test, expect } from 'vitest' +import { generateForm, preapareFormForCreateApplication } from '../../src/renderer/src/utils' + +const expectedA = [{ + name: 'test-1', + template: '@platformatic/service', + form: { + PLT_SERVER_HOSTNAME: { + configValue: 'hostname', + label: 'What is the hostname?', + type: 'string', + value: '0.0.0.0', + var: 'PLT_SERVER_HOSTNAME' + }, + PLT_SERVER_LOGGER_LEVEL: { + configValue: '', + label: 'What is the logger level?', + type: 'string', + value: 'info', + var: 'PLT_SERVER_LOGGER_LEVEL' + }, + PORT: { + configValue: 'port', + label: 'Which port do you want to use?', + value: '3042', + var: 'PORT' + } + }, + validForm: true, + validations: { + PLT_SERVER_HOSTNAMEValid: true, + PLT_SERVER_LOGGER_LEVELValid: true, + PORTValid: true, + formErrors: { + PLT_SERVER_HOSTNAME: '', + PLT_SERVER_LOGGER_LEVEL: '', + PORT: '' + } + }, + plugins: [] +}] + +const expectedB = [{ + name: 'lunasa-1', + template: '@platformatic/service', + form: { + PLT_SERVER_HOSTNAME: { + configValue: 'hostname', + label: 'What is the hostname?', + type: 'string', + value: '0.0.0.0', + var: 'PLT_SERVER_HOSTNAME' + }, + PLT_SERVER_LOGGER_LEVEL: { + configValue: '', + label: 'What is the logger level?', + type: 'string', + value: 'info', + var: 'PLT_SERVER_LOGGER_LEVEL' + }, + PORT: { + configValue: 'port', + label: 'Which port do you want to use?', + value: '3042', + var: 'PORT' + } + }, + validForm: true, + validations: { + PLT_SERVER_HOSTNAMEValid: true, + PLT_SERVER_LOGGER_LEVELValid: true, + PORTValid: true, + formErrors: { + PLT_SERVER_HOSTNAME: '', + PLT_SERVER_LOGGER_LEVEL: '', + PORT: '' + } + }, + plugins: [{ + name: '@fastify/accepts', + form: { + PLT_COOKIE_SECRET: { + value: '', + path: 'secret', + type: 'string' + }, + PLT_COOKIE_HOOK: { + value: '', + path: 'hook', + type: 'string' + }, + PLT_COOKIE_PARSEOPTIONS_DOMAIN: { + value: '', + path: 'parseOptions.domain', + type: 'string' + }, + PLT_COOKIE_PASEOPTIONS_MAXAGE: { + value: '', + path: 'parseOptions.maxAge', + type: 'number' + } + }, + validForm: false, + validations: { + PLT_COOKIE_SECRETValid: false, + PLT_COOKIE_HOOKValid: false, + PLT_COOKIE_PARSEOPTIONS_DOMAINValid: false, + PLT_COOKIE_PASEOPTIONS_MAXAGEValid: false, + formErrors: { + PLT_COOKIE_SECRET: '', + PLT_COOKIE_HOOK: '', + PLT_COOKIE_PARSEOPTIONS_DOMAIN: '', + PLT_COOKIE_PASEOPTIONS_MAXAGE: '' + } + } + }] +}] +test('return service on form without plugin', async () => { + const servicesReceived = + [ + { + template: { + orgId: 'platformatic', + orgName: 'Platformatic', + name: '@platformatic/service', + description: 'A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js', + author: 'Platformatic', + homepage: 'https://platformatic.dev', + public: true, + platformaticService: true, + envVars: [ + { + var: 'PLT_SERVER_HOSTNAME', + label: 'What is the hostname?', + default: '0.0.0.0', + type: 'string', + configValue: 'hostname' + }, + { + var: 'PLT_SERVER_LOGGER_LEVEL', + label: 'What is the logger level?', + default: 'info', + type: 'string', + configValue: '' + }, + { + label: 'Which port do you want to use?', + var: 'PORT', + default: 3042, + tyoe: 'number', + configValue: 'port' + } + ] + }, + name: 'test-1', + plugins: [] + } + ] + + expect(expectedA).toEqual(generateForm(servicesReceived, false)) +}) + +test('return service on form with a single plugin', async () => { + const servicesReceived = + [ + { + template: { + orgId: 'platformatic', + orgName: 'Platformatic', + name: '@platformatic/service', + description: 'A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js', + author: 'Platformatic', + homepage: 'https://platformatic.dev', + public: true, + platformaticService: true, + envVars: [ + { + var: 'PLT_SERVER_HOSTNAME', + label: 'What is the hostname?', + default: '0.0.0.0', + type: 'string', + configValue: 'hostname' + }, + { + var: 'PLT_SERVER_LOGGER_LEVEL', + label: 'What is the logger level?', + default: 'info', + type: 'string', + configValue: '' + }, + { + label: 'Which port do you want to use?', + var: 'PORT', + default: 3042, + tyoe: 'number', + configValue: 'port' + } + ] + }, + plugins: [ + { + name: '@fastify/accepts', + description: 'To have accepts in your request object.', + author: 'mock author', + homepage: 'https://example.com', + envVars: [ + { + name: 'PLT_COOKIE_SECRET', + path: 'secret', + type: 'string' + }, + { + name: 'PLT_COOKIE_HOOK', + path: 'hook', + type: 'string' + }, + { + name: 'PLT_COOKIE_PARSEOPTIONS_DOMAIN', + path: 'parseOptions.domain', + type: 'string' + }, + { + name: 'PLT_COOKIE_PASEOPTIONS_MAXAGE', + path: 'parseOptions.maxAge', + type: 'number' + } + ] + } + ], + name: 'lunasa-1' + } + ] + + expect(expectedB).toEqual(generateForm(servicesReceived, false)) +}) + +test('prepare services without plugins', async () => { + const expected = [{ + name: 'test-1', + template: '@platformatic/service', + fields: [{ + configValue: 'hostname', + type: 'string', + value: '0.0.0.0', + var: 'PLT_SERVER_HOSTNAME' + }, { + configValue: '', + type: 'string', + value: 'info', + var: 'PLT_SERVER_LOGGER_LEVEL' + }, { + configValue: 'port', + value: '3042', + var: 'PORT' + }], + plugins: [] + }] + expect(expected).toEqual(preapareFormForCreateApplication(expectedA)) +}) + +test('prepare services with a single plugins', async () => { + const expected = [{ + name: 'lunasa-1', + template: '@platformatic/service', + fields: [{ + configValue: 'hostname', + type: 'string', + value: '0.0.0.0', + var: 'PLT_SERVER_HOSTNAME' + }, { + configValue: '', + type: 'string', + value: 'info', + var: 'PLT_SERVER_LOGGER_LEVEL' + }, { + configValue: 'port', + value: '3042', + var: 'PORT' + }], + plugins: [{ + name: '@fastify/accepts', + options: [{ + path: 'secret', + type: 'string', + value: '' + }, { + path: 'hook', + type: 'string', + value: '' + }, { + path: 'parseOptions.domain', + type: 'string', + value: '' + }, { + path: 'parseOptions.maxAge', + type: 'number', + value: '' + }] + }] + }] + expect(expected).toEqual(preapareFormForCreateApplication(expectedB)) +})