diff --git a/package.json b/package.json index a874539d..36cccbfb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "deeploy-dapp", "private": true, "type": "module", - "version": "0.1.0", + "version": "0.1.1", "scripts": { "dev": "vite", "dev:logs": "vite --debug", diff --git a/src/components/create-job/JobFormWrapper.tsx b/src/components/create-job/JobFormWrapper.tsx index e727a8a5..2c649faa 100644 --- a/src/components/create-job/JobFormWrapper.tsx +++ b/src/components/create-job/JobFormWrapper.tsx @@ -84,6 +84,7 @@ function JobFormWrapper({ projectName, draftJobsCount }) { }, restartPolicy: POLICY_TYPES[0], imagePullPolicy: POLICY_TYPES[0], + customParams: [], }, }); diff --git a/src/components/create-job/plugins/GenericPluginSections.tsx b/src/components/create-job/plugins/GenericPluginSections.tsx index e3f0969e..d86a596d 100644 --- a/src/components/create-job/plugins/GenericPluginSections.tsx +++ b/src/components/create-job/plugins/GenericPluginSections.tsx @@ -2,6 +2,7 @@ import ConfigSectionTitle from '@components/job/config/ConfigSectionTitle'; import DynamicEnvSection from '@shared/jobs/DynamicEnvSection'; import FileVolumesSection from '@shared/jobs/FileVolumesSection'; import KeyValueEntriesSection from '@shared/jobs/KeyValueEntriesSection'; +import CustomParametersSection from '@shared/jobs/native/CustomParametersSection'; import PortMappingSection from '@shared/PortMappingSection'; import AppParametersSection from '../sections/AppParametersSection'; import PluginEnvVariablesSection from '../sections/PluginEnvVariablesSection'; @@ -34,6 +35,9 @@ export default function GenericPluginSections({ name }: { name: string }) { + + + ); } diff --git a/src/components/create-job/steps/deployment/GenericDeployment.tsx b/src/components/create-job/steps/deployment/GenericDeployment.tsx index 86ca12c2..ef26481c 100644 --- a/src/components/create-job/steps/deployment/GenericDeployment.tsx +++ b/src/components/create-job/steps/deployment/GenericDeployment.tsx @@ -7,6 +7,7 @@ import DynamicEnvSection from '@shared/jobs/DynamicEnvSection'; import EnvVariablesCard from '@shared/jobs/EnvVariablesCard'; import FileVolumesSection from '@shared/jobs/FileVolumesSection'; import KeyValueEntriesSection from '@shared/jobs/KeyValueEntriesSection'; +import CustomParametersSection from '@shared/jobs/native/CustomParametersSection'; import TargetNodesCard from '@shared/jobs/target-nodes/TargetNodesCard'; import PortMappingSection from '@shared/PortMappingSection'; @@ -15,7 +16,12 @@ function GenericDeployment({ isEditingRunningJob }: { isEditingRunningJob?: bool
- +
@@ -53,6 +59,10 @@ function GenericDeployment({ isEditingRunningJob }: { isEditingRunningJob?: bool + + + +
); } diff --git a/src/components/create-job/steps/deployment/NativeDeployment.tsx b/src/components/create-job/steps/deployment/NativeDeployment.tsx index 2c7ccbbf..4d9e034d 100644 --- a/src/components/create-job/steps/deployment/NativeDeployment.tsx +++ b/src/components/create-job/steps/deployment/NativeDeployment.tsx @@ -11,7 +11,12 @@ function NativeDeployment({ isEditingRunningJob }: { isEditingRunningJob?: boole return (
- + diff --git a/src/components/create-job/steps/deployment/ServiceDeployment.tsx b/src/components/create-job/steps/deployment/ServiceDeployment.tsx index f907ff94..0297d958 100644 --- a/src/components/create-job/steps/deployment/ServiceDeployment.tsx +++ b/src/components/create-job/steps/deployment/ServiceDeployment.tsx @@ -78,7 +78,12 @@ function ServiceDeployment({ isEditingRunningJob }: { isEditingRunningJob?: bool
- +
diff --git a/src/components/edit-job/JobEditFormWrapper.tsx b/src/components/edit-job/JobEditFormWrapper.tsx index 1e929198..ccc679ee 100644 --- a/src/components/edit-job/JobEditFormWrapper.tsx +++ b/src/components/edit-job/JobEditFormWrapper.tsx @@ -4,11 +4,18 @@ import Plugins from '@components/create-job/steps/Plugins'; import Services from '@components/create-job/steps/Services'; import Specifications from '@components/create-job/steps/Specifications'; import { BOOLEAN_TYPES } from '@data/booleanTypes'; +import { PLUGIN_SIGNATURE_TYPES } from '@data/pluginSignatureTypes'; import { getRunningService } from '@data/containerResources'; import { CR_VISIBILITY_OPTIONS } from '@data/crVisibilityOptions'; import { zodResolver } from '@hookform/resolvers/zod'; import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment'; -import { boolToBooleanType, isGenericPlugin, NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS, titlecase } from '@lib/deeploy-utils'; +import { + boolToBooleanType, + GENERIC_JOB_RESERVED_KEYS, + isGenericPlugin, + NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS, + titlecase, +} from '@lib/deeploy-utils'; import { Step, STEPS } from '@lib/steps/steps'; import { jobSchema } from '@schemas/index'; import JobFormHeaderInterface from '@shared/jobs/JobFormHeaderInterface'; @@ -111,6 +118,9 @@ export default function JobEditFormWrapper({ // Policies restartPolicy: titlecase(config.RESTART_POLICY!), imagePullPolicy: titlecase(config.IMAGE_PULL_POLICY!), + + // Custom Parameters + customParams: formatCustomParams(config, GENERIC_JOB_RESERVED_KEYS), }); const getGenericPluginSchemaDefaults = (config: JobConfig) => ({ @@ -122,18 +132,27 @@ export default function JobEditFormWrapper({ ...getGenericSpecificDeploymentDefaults(config), }); - const getNativePluginSchemaDefaults = (pluginInfo: AppsPlugin & { signature: string }) => ({ - basePluginType: BasePluginType.Native, + const getNativePluginSchemaDefaults = (pluginInfo: AppsPlugin & { signature: string }) => { + const isKnownSignature = PLUGIN_SIGNATURE_TYPES.includes( + pluginInfo.signature as (typeof PLUGIN_SIGNATURE_TYPES)[number], + ); - // Signature - pluginSignature: pluginInfo.signature, + return { + basePluginType: BasePluginType.Native, - // Tunneling - ...getBaseSchemaTunnelingDefaults(pluginInfo.instance_conf), + // Signature - if not in the predefined list, select CUSTOM and pre-fill customPluginSignature + pluginSignature: isKnownSignature + ? pluginInfo.signature + : PLUGIN_SIGNATURE_TYPES[PLUGIN_SIGNATURE_TYPES.length - 1], + customPluginSignature: isKnownSignature ? undefined : pluginInfo.signature, - // Custom Parameters - customParams: formatCustomParams(pluginInfo.instance_conf), - }); + // Tunneling + ...getBaseSchemaTunnelingDefaults(pluginInfo.instance_conf), + + // Custom Parameters + customParams: formatCustomParams(pluginInfo.instance_conf, NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS), + }; + }; const getBaseSchemaDefaults = (config: JobConfig = jobConfig) => ({ jobType: job.resources.jobType, @@ -252,11 +271,11 @@ export default function JobEditFormWrapper({ ]; }; - const formatCustomParams = (config: JobConfig) => { + const formatCustomParams = (config: JobConfig, reservedKeys: (keyof JobConfig)[]) => { const customParams: CustomParameterEntry[] = []; Object.entries(config).forEach(([key, value]) => { - if (!NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS.includes(key as keyof JobConfig)) { + if (!reservedKeys.includes(key as keyof JobConfig)) { const valueType = typeof value === 'string' ? 'string' : 'json'; let parsedValue: string = ''; diff --git a/src/data/pipelineInputTypes.ts b/src/data/pipelineInputTypes.ts index 5a518d1e..915a04c7 100644 --- a/src/data/pipelineInputTypes.ts +++ b/src/data/pipelineInputTypes.ts @@ -1,7 +1,8 @@ export const PIPELINE_INPUT_TYPES = [ + 'Void', + 'Loopback', 'JeevesListener', 'JeevesApiListener', 'JeevesEmbedAgentListener', 'JeevesLlmAgentListener', - 'void', ] as const; diff --git a/src/lib/deeploy-utils.ts b/src/lib/deeploy-utils.ts index 8936e755..bd2d97fb 100644 --- a/src/lib/deeploy-utils.ts +++ b/src/lib/deeploy-utils.ts @@ -26,6 +26,7 @@ import { import { BasePluginType, ContainerDeploymentType, + CustomParameterEntry, GenericPlugin, NativePlugin, PluginType, @@ -55,6 +56,41 @@ export const NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS: (keyof JobConfig)[] = [ 'NGROK_USE_API', ]; +// Keys that are system-managed and cannot be edited by users +export const SYSTEM_MANAGED_JOB_CONFIG_KEYS: (keyof JobConfig)[] = [ + 'CHAINSTORE_PEERS', + 'CHAINSTORE_RESPONSE_KEY', + 'INSTANCE_ID', + 'TUNNEL_ENGINE', + 'NGROK_AUTH_TOKEN', + 'NGROK_EDGE_LABEL', + 'NGROK_USE_API', +]; + +// Keys that are editable via dedicated UI sections in generic job deployment +export const GENERIC_JOB_UI_MANAGED_KEYS: (keyof JobConfig)[] = [ + 'ENV', + 'DYNAMIC_ENV', + 'VOLUMES', + 'FILE_VOLUMES', + 'CONTAINER_RESOURCES', + 'PORT', + 'TUNNEL_ENGINE_ENABLED', + 'CLOUDFLARE_TOKEN', + 'RESTART_POLICY', + 'IMAGE_PULL_POLICY', + 'IMAGE', + 'VCS_DATA', + 'CR_DATA', + 'BUILD_AND_RUN_COMMANDS', +]; + +// Combined: all keys that should be excluded from custom parameters for generic jobs +export const GENERIC_JOB_RESERVED_KEYS: (keyof JobConfig)[] = [ + ...SYSTEM_MANAGED_JOB_CONFIG_KEYS, + ...GENERIC_JOB_UI_MANAGED_KEYS, +]; + export const getDiscountPercentage = (_paymentMonthsCount: number): number => { // Disabled for now return 0; @@ -287,6 +323,20 @@ const formatNativeJobCustomParams = (plugin: NativePlugin) => { return customParams; }; +const formatGenericJobCustomParams = (customParams: CustomParameterEntry[]) => { + const formatted: Record = {}; + + if (!_.isEmpty(customParams)) { + customParams.forEach((param) => { + if (param.key) { + formatted[param.key] = parseIfJson(param.value); + } + }); + } + + return formatted; +}; + export const formatGenericPluginConfigAndSignature = ( resources: { cpu: number; @@ -377,6 +427,8 @@ export const formatGenericJobPayload = ( deployment, ); + const customParams = formatGenericJobCustomParams(deployment.customParams); + const nonce = generateDeeployNonce(); return { @@ -391,6 +443,7 @@ export const formatGenericJobPayload = ( { plugin_signature: pluginSignature, ...pluginConfig, + ...customParams, }, ], pipeline_input_type: 'void', @@ -424,16 +477,20 @@ export const formatNativeJobPayload = ( // Build plugins array const plugins = deployment.plugins.map((plugin) => { if (plugin.basePluginType === BasePluginType.Generic) { - const secondaryPluginNodeResources = formatContainerResources(workerType, (plugin as GenericPlugin).ports); + const genericPlugin = plugin as GenericPlugin; + const secondaryPluginNodeResources = formatContainerResources(workerType, genericPlugin.ports); const { pluginConfig, pluginSignature } = formatGenericPluginConfigAndSignature( secondaryPluginNodeResources, - plugin as GenericPlugin, + genericPlugin, ); + const customParams = formatGenericJobCustomParams(genericPlugin.customParams); + return { plugin_signature: pluginSignature, ...pluginConfig, + ...customParams, }; } diff --git a/src/schemas/steps/deployment.ts b/src/schemas/steps/deployment.ts index a746d63b..0cc2fc11 100644 --- a/src/schemas/steps/deployment.ts +++ b/src/schemas/steps/deployment.ts @@ -322,6 +322,7 @@ const genericAppDeploymentSchemaWihtoutRefinements = baseDeploymentSchema.extend fileVolumes: validations.fileVolumes, restartPolicy: validations.restartPolicy, imagePullPolicy: validations.imagePullPolicy, + customParams: validations.customParams, }); export const genericAppDeploymentSchema = applyDeploymentTypeRefinements( @@ -352,6 +353,9 @@ const genericPluginSchema = z.object({ // Policies restartPolicy: z.enum(POLICY_TYPES, { required_error: 'Value is required' }), imagePullPolicy: z.enum(POLICY_TYPES, { required_error: 'Value is required' }), + + // Custom Parameters + customParams: validations.customParams, }); const nativePluginSchema = z.object({ diff --git a/src/shared/jobs/KeyValueEntriesSection.tsx b/src/shared/jobs/KeyValueEntriesSection.tsx index d4af503b..79e66f1a 100644 --- a/src/shared/jobs/KeyValueEntriesSection.tsx +++ b/src/shared/jobs/KeyValueEntriesSection.tsx @@ -148,6 +148,11 @@ export default function KeyValueEntriesSection({ onChange={async (e) => { const value = e.target.value; field.onChange(value); + + // Re-validate on change if field has error to clear it immediately + if (hasError) { + await trigger(`${name}.${index}.key`); + } }} onBlur={async () => { field.onBlur(); @@ -183,9 +188,14 @@ export default function KeyValueEntriesSection({ { + onChange={async (e) => { const value = e.target.value; field.onChange(value); + + // Re-validate on change if field has error to clear it immediately + if (hasError) { + await trigger(`${name}.${index}.value`); + } }} onBlur={async () => { field.onBlur(); diff --git a/src/shared/jobs/deployment-type/WorkerSection.tsx b/src/shared/jobs/deployment-type/WorkerSection.tsx index 2e9b7b42..fbc25975 100644 --- a/src/shared/jobs/deployment-type/WorkerSection.tsx +++ b/src/shared/jobs/deployment-type/WorkerSection.tsx @@ -62,21 +62,30 @@ export default function WorkerSection({ try { const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`); - if (!response.ok) { - throw new Error(`GitHub repository lookup failed with status ${response.status}`); + if (response.ok) { + // 200 - Repository is public + setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public'); + REPOS_CACHE[trimmedUrl] = 'public'; + } else if (response.status === 404) { + // 404 - Repository is private or doesn't exist + setValue(`${baseName}.deploymentType.repositoryVisibility`, 'private'); + REPOS_CACHE[trimmedUrl] = 'private'; + } else { + // Other errors (e.g., 403 rate limit) - default to public (credentials optional) + // If it's actually private, user will get an error during deployment + console.warn(`GitHub API returned status ${response.status}, defaulting to public`); + setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public'); + // Don't cache rate-limited results } - - // console.log('Repository visibility is: public'); - setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public'); - REPOS_CACHE[trimmedUrl] = 'public'; } catch (error: any) { if (error?.name === 'AbortError') { return; } - // console.log('Repository visibility is: private'); - setValue(`${baseName}.deploymentType.repositoryVisibility`, 'private'); - REPOS_CACHE[trimmedUrl] = 'private'; + // Network error - default to public (credentials optional) + console.error('Failed to check repository visibility:', error); + setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public'); + // Don't cache network errors } }; diff --git a/src/shared/jobs/native/CustomParametersSection.tsx b/src/shared/jobs/native/CustomParametersSection.tsx index 30e65109..596ea798 100644 --- a/src/shared/jobs/native/CustomParametersSection.tsx +++ b/src/shared/jobs/native/CustomParametersSection.tsx @@ -93,6 +93,11 @@ export default function CustomParametersSection({ baseName = 'deployment' }: { b onChange={async (e) => { const value = e.target.value; field.onChange(value); + + // Re-validate on change if field has error to clear it immediately + if (hasError) { + await trigger(`${name}.${index}.key`); + } }} onBlur={async () => { field.onBlur(); @@ -141,9 +146,14 @@ export default function CustomParametersSection({ baseName = 'deployment' }: { b { + onChange={async (e) => { const value = e.target.value; field.onChange(value); + + // Re-validate on change if field has error to clear it immediately + if (hasError) { + await trigger(`${name}.${index}.value`); + } }} onBlur={async () => { field.onBlur(); diff --git a/src/shared/jobs/target-nodes/TargetNodesCard.tsx b/src/shared/jobs/target-nodes/TargetNodesCard.tsx index 53e1097c..319c25cd 100644 --- a/src/shared/jobs/target-nodes/TargetNodesCard.tsx +++ b/src/shared/jobs/target-nodes/TargetNodesCard.tsx @@ -33,6 +33,18 @@ function TargetNodesCard({ isEditingRunningJob }: { isEditingRunningJob?: boolea ); setValue('deployment.spareNodes', []); + } else { + // When auto-assign is disabled, resize array to match targetNodesCount + if (targetNodes.length < targetNodesCount) { + // Add empty entries if count increased + setValue('deployment.targetNodes', [ + ...targetNodes, + ...Array.from({ length: targetNodesCount - targetNodes.length }, () => ({ address: '' })), + ]); + } else if (targetNodes.length > targetNodesCount) { + // Trim entries if count decreased + setValue('deployment.targetNodes', targetNodes.slice(0, targetNodesCount)); + } } } }, [autoAssign, targetNodesCount]); diff --git a/src/typedefs/steps/deploymentStepTypes.ts b/src/typedefs/steps/deploymentStepTypes.ts index 50be4556..26af6b25 100644 --- a/src/typedefs/steps/deploymentStepTypes.ts +++ b/src/typedefs/steps/deploymentStepTypes.ts @@ -95,6 +95,9 @@ type GenericPlugin = { // Policies restartPolicy: (typeof POLICY_TYPES)[number]; imagePullPolicy: (typeof POLICY_TYPES)[number]; + + // Custom Parameters + customParams: Array; }; type NativePlugin = { @@ -143,6 +146,9 @@ type GenericJobDeployment = BaseJobDeployment & { // Policies restartPolicy: (typeof POLICY_TYPES)[number]; imagePullPolicy: (typeof POLICY_TYPES)[number]; + + // Custom Parameters + customParams: Array; }; type NativeJobDeployment = BaseJobDeployment & {