From 37ad5ef9fba09f96be5dd6b43e592c57e7d9e439 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Tue, 23 Dec 2025 11:19:27 +0530 Subject: [PATCH 01/12] feat: add .env parser utility Add utility function to parse .env file content into key-value pairs. Handles comments, empty lines, quoted values, and duplicate keys. --- .../shared-component/src/utils/envParser.ts | 70 +++++++++++++++++++ .../libs/shared-component/src/utils/index.ts | 20 ++++++ 2 files changed, 90 insertions(+) create mode 100644 console/workspaces/libs/shared-component/src/utils/envParser.ts create mode 100644 console/workspaces/libs/shared-component/src/utils/index.ts diff --git a/console/workspaces/libs/shared-component/src/utils/envParser.ts b/console/workspaces/libs/shared-component/src/utils/envParser.ts new file mode 100644 index 000000000..d2f7e9042 --- /dev/null +++ b/console/workspaces/libs/shared-component/src/utils/envParser.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface EnvVariable { + key: string; + value: string; +} + +// Strips surrounding quotes from a value (single or double quotes) +function stripQuotes(value: string): string { + const trimmed = value.trim(); + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +// Parses .env file content into an array of key-value pairs +export function parseEnvContent(content: string): EnvVariable[] { + const lines = content.split(/\r?\n/); + const envMap = new Map(); + + for (const line of lines) { + const trimmedLine = line.trim(); + + // Skip empty lines and comments + if (!trimmedLine || trimmedLine.startsWith('#')) { + continue; + } + + // Find the first '=' to split key and value + const equalIndex = trimmedLine.indexOf('='); + if (equalIndex === -1) { + continue; // Skip lines without '=' + } + + const key = trimmedLine.substring(0, equalIndex).trim(); + const rawValue = trimmedLine.substring(equalIndex + 1); + const value = stripQuotes(rawValue); + + // Skip entries with empty keys + if (!key) { + continue; + } + + // Use Map to handle duplicates (last value wins) + envMap.set(key, value); + } + + // Convert Map to array + return Array.from(envMap.entries()).map(([key, value]) => ({ key, value })); +} diff --git a/console/workspaces/libs/shared-component/src/utils/index.ts b/console/workspaces/libs/shared-component/src/utils/index.ts new file mode 100644 index 000000000..11f555d47 --- /dev/null +++ b/console/workspaces/libs/shared-component/src/utils/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { parseEnvContent } from './envParser'; +export type { EnvVariable } from './envParser'; From a79445edd088abffc2a47774251f3f2321873cc6 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Tue, 23 Dec 2025 11:21:10 +0530 Subject: [PATCH 02/12] feat: add bulk import modal component Add modal dialog for importing environment variables from .env content. Supports both pasting content and file upload with real-time preview. --- .../src/components/EnvBulkImportModal.tsx | 190 ++++++++++++++++++ .../shared-component/src/components/index.ts | 1 + .../libs/shared-component/src/index.ts | 1 + 3 files changed, 192 insertions(+) create mode 100644 console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx diff --git a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx new file mode 100644 index 000000000..dd27c16fd --- /dev/null +++ b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useRef, useCallback, useMemo, ChangeEvent } from "react"; +import { + Box, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Typography, + useTheme, +} from "@wso2/oxygen-ui"; +import { FileText, Upload } from "@wso2/oxygen-ui-icons-react"; +import { parseEnvContent, EnvVariable } from "../utils"; + +interface EnvBulkImportModalProps { + open: boolean; + onClose: () => void; + onImport: (envVars: EnvVariable[]) => void; +} + +export function EnvBulkImportModal({ + open, + onClose, + onImport, +}: EnvBulkImportModalProps) { + const theme = useTheme(); + const [content, setContent] = useState(""); + const fileInputRef = useRef(null); + + // Parse content and get variables count + const parsedVars = useMemo(() => parseEnvContent(content), [content]); + const variablesCount = parsedVars.length; + + // Handle textarea change + const handleContentChange = useCallback( + (e: ChangeEvent) => { + setContent(e.target.value); + }, + [] + ); + + // Handle file upload + const handleFileUpload = useCallback( + (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result; + if (typeof text === "string") { + setContent(text); + } + }; + reader.readAsText(file); + + // Reset input so same file can be selected again + e.target.value = ""; + }, + [] + ); + + // Trigger file input click + const handleUploadClick = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + // Handle import button click + const handleImport = useCallback(() => { + if (variablesCount > 0) { + onImport(parsedVars); + setContent(""); + onClose(); + } + }, [variablesCount, parsedVars, onImport, onClose]); + + // Handle cancel/close + const handleClose = useCallback(() => { + setContent(""); + onClose(); + }, [onClose]); + + return ( + + + + + + Bulk Import Environment Variables + + + + + + + + Paste your .env content below or upload a file. + + + {/* Textarea for pasting .env content */} + + + {/* File upload button */} + + + + + + {/* Variables count indicator */} + 0 ? "success.main" : "text.secondary"} + > + {variablesCount > 0 + ? `${variablesCount} variable${variablesCount !== 1 ? "s" : ""} detected` + : "No variables detected"} + + + + + + + + + + ); +} diff --git a/console/workspaces/libs/shared-component/src/components/index.ts b/console/workspaces/libs/shared-component/src/components/index.ts index 223e202c4..79775b6a3 100644 --- a/console/workspaces/libs/shared-component/src/components/index.ts +++ b/console/workspaces/libs/shared-component/src/components/index.ts @@ -24,3 +24,4 @@ export * from './DeploymentConfig'; export * from './EnvironmentVariable'; export * from './EnvironmentCard'; export * from './ConfirmationDialog'; +export * from './EnvBulkImportModal'; diff --git a/console/workspaces/libs/shared-component/src/index.ts b/console/workspaces/libs/shared-component/src/index.ts index 700fb529c..339dcdb32 100644 --- a/console/workspaces/libs/shared-component/src/index.ts +++ b/console/workspaces/libs/shared-component/src/index.ts @@ -17,3 +17,4 @@ */ export * from './components'; +export * from './utils'; From 7d9817c8fb556e74e565205aa89fe002ad63ee54 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Tue, 23 Dec 2025 11:22:13 +0530 Subject: [PATCH 03/12] feat: integrate bulk import into EnvironmentVariable component Add Bulk Import button and modal integration. Imported variables merge with existing ones (duplicates are replaced). --- .../src/components/EnvironmentVariable.tsx | 143 +++++++++++------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx b/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx index 3e9e80765..06b99080e 100644 --- a/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx @@ -16,14 +16,17 @@ * under the License. */ +import { useState, useCallback } from "react"; import { Box, Button, Typography } from "@wso2/oxygen-ui"; -import { Plus as Add } from "@wso2/oxygen-ui-icons-react"; +import { Plus as Add, FileText } from "@wso2/oxygen-ui-icons-react"; import { EnvVariableEditor } from "@agent-management-platform/views"; +import { EnvBulkImportModal } from "./EnvBulkImportModal"; +import type { EnvVariable } from "../utils"; interface EnvironmentVariableProps { envVariables: Array<{ key: string; value: string }>; setEnvVariables: React.Dispatch>>; - /** When true, the "Add" button is hidden */ + /** When true, the "Add" and "Import" buttons are hidden */ hideAddButton?: boolean; /** When true, key fields are disabled so only values can be edited */ keyFieldsDisabled?: boolean; @@ -35,61 +38,93 @@ interface EnvironmentVariableProps { description?: string; } -export const EnvironmentVariable = - ({ envVariables, setEnvVariables, hideAddButton = false, keyFieldsDisabled = false, isValueSecret = false, title = "Environment Variables (Optional)", description = "Set environment variables for your agent deployment." }: EnvironmentVariableProps) => { - const isOneEmpty = envVariables.some((e) => !e?.key || !e?.value); +export const EnvironmentVariable = ({ + envVariables, + setEnvVariables, + hideAddButton = false, + keyFieldsDisabled = false, + isValueSecret = false, + title = "Environment Variables (Optional)", + description = "Set environment variables for your agent deployment.", +}: EnvironmentVariableProps) => { + const [importModalOpen, setImportModalOpen] = useState(false); + const isOneEmpty = envVariables.some((e) => !e?.key || !e?.value); - const handleAdd = () => { - setEnvVariables(prev => [...prev, { key: '', value: '' }]); - }; - - const handleRemove = (index: number) => { - setEnvVariables(prev => prev.filter((_, i) => i !== index)); - }; + const handleAdd = () => { + setEnvVariables((prev) => [...prev, { key: '', value: '' }]); + }; - const handleChange = (index: number, field: 'key' | 'value', value: string) => { - setEnvVariables(prev => prev.map((item, i) => - i === index ? { ...item, [field]: value } : item - )); - }; + const handleRemove = (index: number) => { + setEnvVariables((prev) => prev.filter((_, i) => i !== index)); + }; - return ( - - - {title} - - - {description} - - - {envVariables.map((envVar, index: number) => ( - handleChange(index, 'key', value)} - onValueChange={(value) => handleChange(index, 'value', value)} - onRemove={() => handleRemove(index)} - keyDisabled={keyFieldsDisabled} - isValueSecret={isValueSecret} - /> - ))} - - {!hideAddButton && ( - - - - )} - + const handleChange = (index: number, field: 'key' | 'value', value: string) => { + setEnvVariables((prev) => + prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)) ); }; + const handleImport = useCallback((importedVars: EnvVariable[]) => { + setEnvVariables((prev) => { + // Filter out rows with no key (value may be intentionally empty) + const nonEmpty = prev.filter((env) => env?.key); + + // Build map from existing vars; imported vars override on same key + const existingMap = new Map(nonEmpty.map((env) => [env.key, env.value])); + importedVars.forEach((v) => existingMap.set(v.key, v.value)); + + return Array.from(existingMap.entries()).map(([key, value]) => ({ key, value })); + }); + }, [setEnvVariables]); + + const handleModalClose = useCallback(() => setImportModalOpen(false), []); + + return ( + + {title} + {description} + + {envVariables.map((envVar, index: number) => ( + handleChange(index, 'key', value)} + onValueChange={(value) => handleChange(index, 'value', value)} + onRemove={() => handleRemove(index)} + keyDisabled={keyFieldsDisabled} + isValueSecret={isValueSecret} + /> + ))} + + {!hideAddButton && ( + + + + + )} + + + + ); +}; From cda5cde303a48686e2f97fbc8b827b5ea646fa1a Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Wed, 24 Dec 2025 21:14:32 +0530 Subject: [PATCH 04/12] feat: add bulk import to Create Agent page Apply the same bulk import functionality to the add-new-agent page. --- .../src/components/EnvironmentVariable.tsx | 190 +++++++++--------- 1 file changed, 93 insertions(+), 97 deletions(-) diff --git a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx index ec2e28af4..69bcf093d 100644 --- a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx @@ -16,111 +16,107 @@ * under the License. */ +import { useState, useCallback, useMemo } from "react"; import { Box, Button, Card, CardContent, Typography } from "@wso2/oxygen-ui"; -import { Plus as Add } from "@wso2/oxygen-ui-icons-react"; +import { Plus as Add, FileText } from "@wso2/oxygen-ui-icons-react"; +import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { EnvVariableEditor } from "@agent-management-platform/views"; -import { CreateAgentFormValues } from "../form/schema"; +import { EnvBulkImportModal, EnvVariable } from "@agent-management-platform/shared-component"; -interface EnvironmentVariableProps { - formData: CreateAgentFormValues; - setFormData: React.Dispatch>; -} +export const EnvironmentVariable = () => { + const { control, formState: { errors }, register, setValue } = useFormContext(); + const { fields, append, remove } = useFieldArray({ control, name: 'env' }); + const watchedEnvValues = useWatch({ control, name: 'env' }); + const [importModalOpen, setImportModalOpen] = useState(false); -export const EnvironmentVariable = ({ - formData, - setFormData, -}: EnvironmentVariableProps) => { - const envVariables = formData.env || []; - const isOneEmpty = envVariables.some((e) => !e?.key || !e?.value); + // Memoize envValues to stabilize dependency for useCallback + const envValues = useMemo( + () => (watchedEnvValues || []) as EnvVariable[], + [watchedEnvValues] + ); - const handleAdd = () => { - setFormData((prev) => ({ - ...prev, - env: [...(prev.env || []), { key: '', value: '' }], - })); - }; + const isOneEmpty = envValues.some((e) => !e?.key || !e?.value); - const handleRemove = (index: number) => { - setFormData((prev) => ({ - ...prev, - env: prev.env?.filter((_, i) => i !== index) || [], - })); - }; + // Handle bulk import - merge imported vars with existing ones + const handleImport = useCallback((importedVars: EnvVariable[]) => { + const existingMap = new Map(); - const handleChange = (index: number, field: 'key' | 'value', value: string) => { - setFormData((prev) => ({ - ...prev, - env: prev.env?.map((item, i) => - i === index ? { ...item, [field]: value } : item - ) || [], - })); - }; + // Map existing keys to their indices + envValues.forEach((env, index) => { + if (env?.key) { + existingMap.set(env.key, index); + } + }); - const handleInitialEdit = (field: 'key' | 'value', value: string) => { - setFormData((prev) => { - const envList = prev.env || []; - if (envList.length > 0) { - return { - ...prev, - env: envList.map((item, i) => - i === 0 ? { ...item, [field]: value } : item - ), - }; - } + // Process imported variables + const updatedEnv = [...envValues]; + const newVars: EnvVariable[] = []; - return { - ...prev, - env: [ - { - key: field === 'key' ? value : '', - value: field === 'value' ? value : '', - }, - ], - }; - }); - }; + importedVars.forEach((imported) => { + if (existingMap.has(imported.key)) { + // Update existing variable + const idx = existingMap.get(imported.key)!; + updatedEnv[idx] = { key: imported.key, value: imported.value }; + } else { + // Add new variable + newVars.push(imported); + } + }); - return ( - - - - - Environment Variables (Optional) - - - - {envVariables.length ? envVariables.map((item, index) => ( - handleChange(index, 'key', value)} - onValueChange={(value) => handleChange(index, 'value', value)} - onRemove={() => handleRemove(index)} - /> - )) : - handleInitialEdit('key', value)} - onValueChange={(value) => handleInitialEdit('value', value)} - onRemove={() => handleRemove(0)} - /> - } - - - - - ); + // Set updated values and append new ones + setValue('env', updatedEnv); + newVars.forEach((v) => append(v)); + }, [envValues, setValue, append]); + + const handleModalClose = useCallback(() => setImportModalOpen(false), []); + + return ( + + + + + Environment Variables (Optional) + + + + {fields.map((field, index) => ( + remove(index)} + /> + ))} + + + + + + + + + + ); }; From 8d33e77e0cd2d8da15525a7a0f7cd406f95df34a Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Wed, 24 Dec 2025 22:12:08 +0530 Subject: [PATCH 05/12] feat: add shared-component dependency to add-new-agent Enables bulk import modal usage in the add new agent page. --- console/common/config/rush/pnpm-lock.yaml | 3 ++ .../pages/add-new-agent/package.json | 7 +++- .../src/components/EnvironmentVariable.tsx | 40 ++++++++----------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/console/common/config/rush/pnpm-lock.yaml b/console/common/config/rush/pnpm-lock.yaml index e1406e123..12bac8b1d 100644 --- a/console/common/config/rush/pnpm-lock.yaml +++ b/console/common/config/rush/pnpm-lock.yaml @@ -727,6 +727,9 @@ importers: '@agent-management-platform/api-client': specifier: workspace:* version: link:../../libs/api-client + '@agent-management-platform/shared-component': + specifier: workspace:* + version: link:../../libs/shared-component '@agent-management-platform/types': specifier: workspace:* version: link:../../libs/types diff --git a/console/workspaces/pages/add-new-agent/package.json b/console/workspaces/pages/add-new-agent/package.json index b483b80a4..72bb01adf 100644 --- a/console/workspaces/pages/add-new-agent/package.json +++ b/console/workspaces/pages/add-new-agent/package.json @@ -49,8 +49,11 @@ "@agent-management-platform/views": "workspace:*", "@agent-management-platform/api-client": "workspace:*", "@agent-management-platform/types": "workspace:*", - "date-fns": "4.1.0", - "zod": "4.3.6", + "@agent-management-platform/shared-component": "workspace:*", + "dayjs": "1.11.18", + "yup": "1.4.0", + "react-hook-form": "7.53.0", + "@hookform/resolvers": "3.9.0", "lodash": "4.17.21" }, "devDependencies": { diff --git a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx index 69bcf093d..21dc6ca68 100644 --- a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx @@ -25,7 +25,7 @@ import { EnvBulkImportModal, EnvVariable } from "@agent-management-platform/shar export const EnvironmentVariable = () => { const { control, formState: { errors }, register, setValue } = useFormContext(); - const { fields, append, remove } = useFieldArray({ control, name: 'env' }); + const { fields, append, remove, replace } = useFieldArray({ control, name: 'env' }); const watchedEnvValues = useWatch({ control, name: 'env' }); const [importModalOpen, setImportModalOpen] = useState(false); @@ -37,36 +37,28 @@ export const EnvironmentVariable = () => { const isOneEmpty = envValues.some((e) => !e?.key || !e?.value); - // Handle bulk import - merge imported vars with existing ones + // Handle bulk import - merge imported vars with existing ones, remove empty rows const handleImport = useCallback((importedVars: EnvVariable[]) => { - const existingMap = new Map(); + // Filter out empty rows from existing values + const nonEmptyExisting = envValues.filter((env) => env?.key && env?.value); - // Map existing keys to their indices - envValues.forEach((env, index) => { - if (env?.key) { - existingMap.set(env.key, index); - } + // Map existing keys to their values for merging + const existingMap = new Map(); + nonEmptyExisting.forEach((env) => { + existingMap.set(env.key, env.value); }); - // Process imported variables - const updatedEnv = [...envValues]; - const newVars: EnvVariable[] = []; - + // Merge: imported vars override existing ones with same key importedVars.forEach((imported) => { - if (existingMap.has(imported.key)) { - // Update existing variable - const idx = existingMap.get(imported.key)!; - updatedEnv[idx] = { key: imported.key, value: imported.value }; - } else { - // Add new variable - newVars.push(imported); - } + existingMap.set(imported.key, imported.value); }); - // Set updated values and append new ones - setValue('env', updatedEnv); - newVars.forEach((v) => append(v)); - }, [envValues, setValue, append]); + // Convert map back to array + const mergedEnv = Array.from(existingMap.entries()).map(([key, value]) => ({ key, value })); + + // Replace all fields with merged result + replace(mergedEnv); + }, [envValues, replace]); const handleModalClose = useCallback(() => setImportModalOpen(false), []); From 1c1d10ff2eb6dc74a1d46e8f6295be93e62a4208 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Wed, 24 Dec 2025 22:54:29 +0530 Subject: [PATCH 06/12] fix: use getValues to avoid stale closure in bulk import Gets current form values directly at import time instead of relying on memoized watched values. --- .../add-new-agent/src/components/EnvironmentVariable.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx index 21dc6ca68..20d08415f 100644 --- a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx @@ -24,7 +24,7 @@ import { EnvVariableEditor } from "@agent-management-platform/views"; import { EnvBulkImportModal, EnvVariable } from "@agent-management-platform/shared-component"; export const EnvironmentVariable = () => { - const { control, formState: { errors }, register, setValue } = useFormContext(); + const { control, formState: { errors }, register, getValues } = useFormContext(); const { fields, append, remove, replace } = useFieldArray({ control, name: 'env' }); const watchedEnvValues = useWatch({ control, name: 'env' }); const [importModalOpen, setImportModalOpen] = useState(false); @@ -39,8 +39,11 @@ export const EnvironmentVariable = () => { // Handle bulk import - merge imported vars with existing ones, remove empty rows const handleImport = useCallback((importedVars: EnvVariable[]) => { + // Get current values directly from form to avoid stale closure + const currentEnv = (getValues('env') || []) as EnvVariable[]; + // Filter out empty rows from existing values - const nonEmptyExisting = envValues.filter((env) => env?.key && env?.value); + const nonEmptyExisting = currentEnv.filter((env) => env?.key && env?.value); // Map existing keys to their values for merging const existingMap = new Map(); @@ -58,7 +61,7 @@ export const EnvironmentVariable = () => { // Replace all fields with merged result replace(mergedEnv); - }, [envValues, replace]); + }, [getValues, replace]); const handleModalClose = useCallback(() => setImportModalOpen(false), []); From 124b422eb27b7f679d40ab6a6d2dc058732714cb Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Mon, 5 Jan 2026 14:54:21 +0530 Subject: [PATCH 07/12] refactor: rename 'Bulk Import' to 'Import' for environment variables - Updated button text from 'Bulk Import' to 'Import' - Updated modal title from 'Bulk Import Environment Variables' to 'Import Environment Variables' --- .../libs/shared-component/src/components/EnvBulkImportModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx index dd27c16fd..d31972bf7 100644 --- a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx +++ b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx @@ -109,7 +109,7 @@ export function EnvBulkImportModal({ - Bulk Import Environment Variables + Import Environment Variables From 0a6f678393a75ecaf8306097810aa5569d8f8137 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Mon, 5 Jan 2026 15:39:32 +0530 Subject: [PATCH 08/12] feat: add validation feedback for invalid environment variable keys - Added regex validation for env variable keys (POSIX standard) - Parser now returns both valid entries and invalid keys - Modal displays error feedback showing which keys were skipped - Added explanation of valid key format to help users --- .../src/components/EnvBulkImportModal.tsx | 44 ++++++++++++++----- .../shared-component/src/utils/envParser.ts | 26 ++++++++++- .../libs/shared-component/src/utils/index.ts | 2 +- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx index d31972bf7..6df7dbb29 100644 --- a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx +++ b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx @@ -46,8 +46,9 @@ export function EnvBulkImportModal({ const fileInputRef = useRef(null); // Parse content and get variables count - const parsedVars = useMemo(() => parseEnvContent(content), [content]); - const variablesCount = parsedVars.length; + const parseResult = useMemo(() => parseEnvContent(content), [content]); + const validCount = parseResult.valid.length; + const invalidKeys = parseResult.invalid; // Handle textarea change const handleContentChange = useCallback( @@ -85,12 +86,12 @@ export function EnvBulkImportModal({ // Handle import button click const handleImport = useCallback(() => { - if (variablesCount > 0) { - onImport(parsedVars); + if (validCount > 0) { + onImport(parseResult.valid); setContent(""); onClose(); } - }, [variablesCount, parsedVars, onImport, onClose]); + }, [validCount, parseResult.valid, onImport, onClose]); // Handle cancel/close const handleClose = useCallback(() => { @@ -149,7 +150,6 @@ export function EnvBulkImportModal({ @@ -166,12 +166,34 @@ export function EnvBulkImportModal({ {/* Variables count indicator */} 0 ? "success.main" : "text.secondary"} + color={validCount > 0 ? "success.main" : "text.secondary"} > - {variablesCount > 0 - ? `${variablesCount} variable${variablesCount !== 1 ? "s" : ""} detected` - : "No variables detected"} + {validCount > 0 + ? `${validCount} valid variable${validCount !== 1 ? "s" : ""} detected` + : "No valid variables detected"} + + {/* Invalid keys warning */} + {invalidKeys.length > 0 && ( + + + {invalidKeys.length} invalid key{invalidKeys.length !== 1 ? "s" : ""} skipped: + + + {invalidKeys.join(", ")} + + + Keys must start with a letter or underscore, and contain only letters, numbers, or underscores. + + + )} @@ -180,7 +202,7 @@ export function EnvBulkImportModal({ diff --git a/console/workspaces/libs/shared-component/src/utils/envParser.ts b/console/workspaces/libs/shared-component/src/utils/envParser.ts index d2f7e9042..bc2f7091d 100644 --- a/console/workspaces/libs/shared-component/src/utils/envParser.ts +++ b/console/workspaces/libs/shared-component/src/utils/envParser.ts @@ -21,6 +21,20 @@ export interface EnvVariable { value: string; } +export interface ParseResult { + valid: EnvVariable[]; + invalid: string[]; +} + +// Regex pattern for valid environment variable keys +// Must start with a letter or underscore, followed by letters, numbers, or underscores +const ENV_KEY_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/; + +// Validates if a key is a valid environment variable name +function isValidEnvKey(key: string): boolean { + return ENV_KEY_REGEX.test(key); +} + // Strips surrounding quotes from a value (single or double quotes) function stripQuotes(value: string): string { const trimmed = value.trim(); @@ -34,9 +48,10 @@ function stripQuotes(value: string): string { } // Parses .env file content into an array of key-value pairs -export function parseEnvContent(content: string): EnvVariable[] { +export function parseEnvContent(content: string): ParseResult { const lines = content.split(/\r?\n/); const envMap = new Map(); + const invalid: string[] = []; for (const line of lines) { const trimmedLine = line.trim(); @@ -61,10 +76,17 @@ export function parseEnvContent(content: string): EnvVariable[] { continue; } + // Check if key is valid + if (!isValidEnvKey(key)) { + invalid.push(key); + continue; + } + // Use Map to handle duplicates (last value wins) envMap.set(key, value); } // Convert Map to array - return Array.from(envMap.entries()).map(([key, value]) => ({ key, value })); + const valid = Array.from(envMap.entries()).map(([key, value]) => ({ key, value })); + return { valid, invalid }; } diff --git a/console/workspaces/libs/shared-component/src/utils/index.ts b/console/workspaces/libs/shared-component/src/utils/index.ts index 11f555d47..d2e0bf8f2 100644 --- a/console/workspaces/libs/shared-component/src/utils/index.ts +++ b/console/workspaces/libs/shared-component/src/utils/index.ts @@ -17,4 +17,4 @@ */ export { parseEnvContent } from './envParser'; -export type { EnvVariable } from './envParser'; +export type { EnvVariable, ParseResult } from './envParser'; From c516841352795dcf6b9e6b3f3f5902ca997778fe Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Wed, 25 Feb 2026 14:52:08 +0530 Subject: [PATCH 09/12] fix: allow intentionally empty env variable values during import merge Update the import merge logic in both the shared EnvironmentVariable component and the add-new-agent page to filter existing rows only by key presence, preserving variables that have an empty value intentionally. --- .../add-new-agent/src/components/EnvironmentVariable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx index 20d08415f..d57a56854 100644 --- a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx @@ -42,8 +42,8 @@ export const EnvironmentVariable = () => { // Get current values directly from form to avoid stale closure const currentEnv = (getValues('env') || []) as EnvVariable[]; - // Filter out empty rows from existing values - const nonEmptyExisting = currentEnv.filter((env) => env?.key && env?.value); + // Filter out rows with no key (value may be intentionally empty) + const nonEmptyExisting = currentEnv.filter((env) => env?.key); // Map existing keys to their values for merging const existingMap = new Map(); From 7d9749242f84a6457084040dcf4a5677a4fd41c1 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Wed, 25 Feb 2026 14:53:07 +0530 Subject: [PATCH 10/12] refactor: use Stack and TextField in EnvBulkImportModal Replace the outer Box layout with Stack for consistent spacing, and replace the raw Box textarea with the Oxygen UI TextField component (multiline) as requested in code review. --- .../src/components/EnvBulkImportModal.tsx | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx index 6df7dbb29..1343effad 100644 --- a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx +++ b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx @@ -24,12 +24,17 @@ import { DialogTitle, DialogContent, DialogActions, + Stack, + TextField, Typography, useTheme, } from "@wso2/oxygen-ui"; +import { alpha } from "@mui/material/styles"; import { FileText, Upload } from "@wso2/oxygen-ui-icons-react"; import { parseEnvContent, EnvVariable } from "../utils"; +const MAX_FILE_SIZE = 1024 * 1024; // 1MB + interface EnvBulkImportModalProps { open: boolean; onClose: () => void; @@ -43,6 +48,7 @@ export function EnvBulkImportModal({ }: EnvBulkImportModalProps) { const theme = useTheme(); const [content, setContent] = useState(""); + const [fileError, setFileError] = useState(null); const fileInputRef = useRef(null); // Parse content and get variables count @@ -52,7 +58,7 @@ export function EnvBulkImportModal({ // Handle textarea change const handleContentChange = useCallback( - (e: ChangeEvent) => { + (e: ChangeEvent) => { setContent(e.target.value); }, [] @@ -64,7 +70,19 @@ export function EnvBulkImportModal({ const file = e.target.files?.[0]; if (!file) return; + setFileError(null); + + if (file.size > MAX_FILE_SIZE) { + setFileError(`File is too large. Maximum size is ${MAX_FILE_SIZE / 1024}KB.`); + e.target.value = ""; + return; + } + const reader = new FileReader(); + reader.onerror = () => { + setFileError("Failed to read file. Please try again."); + e.target.value = ""; + }; reader.onload = (event) => { const text = event.target?.result; if (typeof text === "string") { @@ -89,6 +107,7 @@ export function EnvBulkImportModal({ if (validCount > 0) { onImport(parseResult.valid); setContent(""); + setFileError(null); onClose(); } }, [validCount, parseResult.valid, onImport, onClose]); @@ -96,6 +115,7 @@ export function EnvBulkImportModal({ // Handle cancel/close const handleClose = useCallback(() => { setContent(""); + setFileError(null); onClose(); }, [onClose]); @@ -116,32 +136,22 @@ export function EnvBulkImportModal({ - + Paste your .env content below or upload a file. {/* Textarea for pasting .env content */} - @@ -150,8 +160,10 @@ export function EnvBulkImportModal({ + {fileError && ( + + {fileError} + + )} {/* Variables count indicator */} @@ -178,7 +195,7 @@ export function EnvBulkImportModal({ )} - + From fa785558d70c9fe8d33cd57415706e29843fcf79 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Wed, 25 Feb 2026 15:25:07 +0530 Subject: [PATCH 11/12] fix: import alpha from @wso2/oxygen-ui instead of @mui/material/styles --- .../libs/shared-component/src/components/EnvBulkImportModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx index 1343effad..06b89e3e0 100644 --- a/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx +++ b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx @@ -18,6 +18,7 @@ import { useState, useRef, useCallback, useMemo, ChangeEvent } from "react"; import { + alpha, Box, Button, Dialog, @@ -29,7 +30,6 @@ import { Typography, useTheme, } from "@wso2/oxygen-ui"; -import { alpha } from "@mui/material/styles"; import { FileText, Upload } from "@wso2/oxygen-ui-icons-react"; import { parseEnvContent, EnvVariable } from "../utils"; From 22b392135ad74e6c433f088c07b02ca4c9d5d7e3 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Fri, 27 Feb 2026 03:49:47 +0530 Subject: [PATCH 12/12] refactor: drop react-hook-form and use props-based env variable components Replace react-hook-form integration in EnvironmentVariable components with the props-based approach consistent with the rest of the codebase. The shared component now accepts the upstream's full props interface (envVariables, setEnvVariables, hideAddButton, keyFieldsDisabled, isValueSecret, title, description) while retaining the Import button and EnvBulkImportModal. The add-new-agent component is updated to match, using formData/setFormData props. Removed react-hook-form and @hookform/resolvers from add-new-agent dependencies. --- .../pages/add-new-agent/package.json | 3 - .../src/components/EnvironmentVariable.tsx | 204 +++++++++++------- 2 files changed, 122 insertions(+), 85 deletions(-) diff --git a/console/workspaces/pages/add-new-agent/package.json b/console/workspaces/pages/add-new-agent/package.json index 72bb01adf..254e7e729 100644 --- a/console/workspaces/pages/add-new-agent/package.json +++ b/console/workspaces/pages/add-new-agent/package.json @@ -51,9 +51,6 @@ "@agent-management-platform/types": "workspace:*", "@agent-management-platform/shared-component": "workspace:*", "dayjs": "1.11.18", - "yup": "1.4.0", - "react-hook-form": "7.53.0", - "@hookform/resolvers": "3.9.0", "lodash": "4.17.21" }, "devDependencies": { diff --git a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx index d57a56854..3e861a143 100644 --- a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx @@ -16,102 +16,142 @@ * under the License. */ -import { useState, useCallback, useMemo } from "react"; +import { useState, useCallback } from "react"; import { Box, Button, Card, CardContent, Typography } from "@wso2/oxygen-ui"; import { Plus as Add, FileText } from "@wso2/oxygen-ui-icons-react"; -import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { EnvVariableEditor } from "@agent-management-platform/views"; import { EnvBulkImportModal, EnvVariable } from "@agent-management-platform/shared-component"; +import { CreateAgentFormValues } from "../form/schema"; -export const EnvironmentVariable = () => { - const { control, formState: { errors }, register, getValues } = useFormContext(); - const { fields, append, remove, replace } = useFieldArray({ control, name: 'env' }); - const watchedEnvValues = useWatch({ control, name: 'env' }); - const [importModalOpen, setImportModalOpen] = useState(false); +interface EnvironmentVariableProps { + formData: CreateAgentFormValues; + setFormData: React.Dispatch>; +} - // Memoize envValues to stabilize dependency for useCallback - const envValues = useMemo( - () => (watchedEnvValues || []) as EnvVariable[], - [watchedEnvValues] - ); +export const EnvironmentVariable = ({ + formData, + setFormData, +}: EnvironmentVariableProps) => { + const [importModalOpen, setImportModalOpen] = useState(false); + const envVariables = formData.env || []; + const isOneEmpty = envVariables.some((e) => !e?.key || !e?.value); - const isOneEmpty = envValues.some((e) => !e?.key || !e?.value); + const handleAdd = () => { + setFormData((prev) => ({ + ...prev, + env: [...(prev.env || []), { key: '', value: '' }], + })); + }; - // Handle bulk import - merge imported vars with existing ones, remove empty rows - const handleImport = useCallback((importedVars: EnvVariable[]) => { - // Get current values directly from form to avoid stale closure - const currentEnv = (getValues('env') || []) as EnvVariable[]; + const handleRemove = (index: number) => { + setFormData((prev) => ({ + ...prev, + env: prev.env?.filter((_, i) => i !== index) || [], + })); + }; - // Filter out rows with no key (value may be intentionally empty) - const nonEmptyExisting = currentEnv.filter((env) => env?.key); + const handleChange = (index: number, field: 'key' | 'value', value: string) => { + setFormData((prev) => ({ + ...prev, + env: prev.env?.map((item, i) => + i === index ? { ...item, [field]: value } : item + ) || [], + })); + }; - // Map existing keys to their values for merging - const existingMap = new Map(); - nonEmptyExisting.forEach((env) => { - existingMap.set(env.key, env.value); - }); + const handleInitialEdit = (field: 'key' | 'value', value: string) => { + setFormData((prev) => { + const envList = prev.env || []; + if (envList.length > 0) { + return { + ...prev, + env: envList.map((item, i) => + i === 0 ? { ...item, [field]: value } : item + ), + }; + } + return { + ...prev, + env: [{ key: field === 'key' ? value : '', value: field === 'value' ? value : '' }], + }; + }); + }; - // Merge: imported vars override existing ones with same key - importedVars.forEach((imported) => { - existingMap.set(imported.key, imported.value); - }); + const handleImport = useCallback((importedVars: EnvVariable[]) => { + setFormData((prev) => { + // Filter out rows with no key (value may be intentionally empty) + const nonEmpty = (prev.env || []).filter((env) => env?.key); - // Convert map back to array - const mergedEnv = Array.from(existingMap.entries()).map(([key, value]) => ({ key, value })); + // Build map from existing vars; imported vars override on same key + const existingMap = new Map(nonEmpty.map((env) => [env.key, env.value])); + importedVars.forEach((v) => existingMap.set(v.key, v.value)); - // Replace all fields with merged result - replace(mergedEnv); - }, [getValues, replace]); + return { + ...prev, + env: Array.from(existingMap.entries()).map(([key, value]) => ({ key, value })), + }; + }); + }, [setFormData]); - const handleModalClose = useCallback(() => setImportModalOpen(false), []); + const handleModalClose = useCallback(() => setImportModalOpen(false), []); - return ( - - - - - Environment Variables (Optional) - - - - {fields.map((field, index) => ( - remove(index)} - /> - ))} - - - - - + return ( + + + + + Environment Variables (Optional) + + + + {envVariables.length ? envVariables.map((item, index) => ( + handleChange(index, 'key', value)} + onValueChange={(value) => handleChange(index, 'value', value)} + onRemove={() => handleRemove(index)} + /> + )) : ( + handleInitialEdit('key', value)} + onValueChange={(value) => handleInitialEdit('value', value)} + onRemove={() => handleRemove(0)} + /> + )} + + + + + - - - - ); + + + + ); };