diff --git a/console/common/config/rush/pnpm-lock.yaml b/console/common/config/rush/pnpm-lock.yaml index 0114d502b..e9267e696 100644 --- a/console/common/config/rush/pnpm-lock.yaml +++ b/console/common/config/rush/pnpm-lock.yaml @@ -527,7 +527,7 @@ importers: version: 8.16.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/x-date-pickers': specifier: 8.16.0 - version: 8.16.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 8.16.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/x-tree-view': specifier: 8.14.0 version: 8.14.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -739,6 +739,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 @@ -766,9 +769,9 @@ importers: clsx: specifier: 2.1.1 version: 2.1.1 - date-fns: - specifier: 4.1.0 - version: 4.1.0 + dayjs: + specifier: 1.11.18 + version: 1.11.18 lodash: specifier: 4.17.21 version: 4.17.21 @@ -778,9 +781,6 @@ importers: react-router-dom: specifier: 6.28.0 version: 6.28.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - zod: - specifier: 4.3.6 - version: 4.3.6 devDependencies: '@agent-management-platform/eslint-config': specifier: workspace:* @@ -5741,6 +5741,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -10464,7 +10467,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-date-pickers@8.16.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@mui/x-date-pickers@8.16.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.29.2 '@mui/material': 7.0.0(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -10481,10 +10484,11 @@ snapshots: '@emotion/react': 11.13.5(@types/react@19.1.16)(react@19.1.1) '@emotion/styled': 11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1) date-fns: 4.1.0 + dayjs: 1.11.18 transitivePeerDependencies: - '@types/react' - '@mui/x-date-pickers@8.16.0(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@mui/x-date-pickers@8.16.0(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.29.2 '@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -10501,6 +10505,7 @@ snapshots: '@emotion/react': 11.14.0(@types/react@19.1.16)(react@19.1.1) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1) date-fns: 4.1.0 + dayjs: 1.11.18 transitivePeerDependencies: - '@types/react' @@ -12296,7 +12301,7 @@ snapshots: '@fontsource-variable/inter': 5.2.8 '@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/x-data-grid': 8.16.0(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@mui/x-date-pickers': 8.16.0(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/x-date-pickers': 8.16.0(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/x-tree-view': 8.14.0(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.9(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react@19.1.1))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@wso2/oxygen-ui-icons-react': 0.8.0(react@19.1.1) date-fns: 4.1.0 @@ -13058,6 +13063,8 @@ snapshots: date-fns@4.1.0: {} + dayjs@1.11.18: {} + de-indent@1.0.2: {} debug@2.6.9: 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..46e5b6d50 --- /dev/null +++ b/console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx @@ -0,0 +1,232 @@ +/** + * 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 { + alpha, + Box, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Stack, + TextField, + Typography, + useTheme, +} from "@wso2/oxygen-ui"; +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; + onImport: (envVars: EnvVariable[]) => void; +} + +export function EnvBulkImportModal({ + open, + onClose, + onImport, +}: EnvBulkImportModalProps) { + const theme = useTheme(); + const [content, setContent] = useState(""); + const [fileError, setFileError] = useState(null); + const fileInputRef = useRef(null); + + // Parse content and get variables count + const parseResult = useMemo(() => parseEnvContent(content), [content]); + const validCount = parseResult.valid.length; + const invalidKeys = parseResult.invalid; + + // 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; + + 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") { + 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 (validCount > 0) { + onImport(parseResult.valid); + setContent(""); + setFileError(null); + onClose(); + } + }, [validCount, parseResult.valid, onImport, onClose]); + + // Handle cancel/close + const handleClose = useCallback(() => { + setContent(""); + setFileError(null); + onClose(); + }, [onClose]); + + return ( + + + + + + Import Environment Variables + + + + + + + + Paste your .env content below or upload a file. + + + {/* Textarea for pasting .env content */} + + + {/* File upload button */} + + + + {fileError && ( + + {fileError} + + )} + + + {/* Variables count indicator */} + 0 ? "success.main" : "text.secondary"} + > + {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. + + + )} + + + + + + + + + ); +} diff --git a/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx b/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx index 7a09c00e1..7aba25bc5 100644 --- a/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/libs/shared-component/src/components/EnvironmentVariable.tsx @@ -16,7 +16,7 @@ * under the License. */ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { Alert, Box, @@ -35,8 +35,11 @@ import { Trash2 as DeleteIcon, Eye, EyeOff, + FileText, } from "@wso2/oxygen-ui-icons-react"; import { TextInput } from "@agent-management-platform/views"; +import { EnvBulkImportModal } from "./EnvBulkImportModal"; +import type { EnvVariable } from "../utils"; export interface EnvVariableItem { key: string; @@ -50,7 +53,7 @@ export interface EnvVariableItem { interface EnvironmentVariableProps { envVariables: Array; 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; @@ -79,6 +82,7 @@ export const EnvironmentVariable = ({ isExistingData = false, }: EnvironmentVariableProps) => { const [isAddFormOpen, setIsAddFormOpen] = useState(false); + const [importModalOpen, setImportModalOpen] = useState(false); const [newEnvVar, setNewEnvVar] = useState({ key: "", value: "", @@ -142,6 +146,25 @@ export const EnvironmentVariable = ({ setShowEditPassword(false); }; + 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 merged = new Map( + nonEmpty.map((env) => [env.key, env]) + ); + importedVars.forEach((v) => + merged.set(v.key, { key: v.key, value: v.value, isSensitive: false }) + ); + + return Array.from(merged.values()); + }); + }, [setEnvVariables]); + + const handleModalClose = useCallback(() => setImportModalOpen(false), []); + const isAddDisabled = !newEnvVar.key || !newEnvVar.value; return ( @@ -378,9 +401,9 @@ export const EnvironmentVariable = ({ )} - {/* Add button */} + {/* Add and Import buttons */} {!hideAddButton && !isAddFormOpen && ( - + + )} + + ); }; diff --git a/console/workspaces/libs/shared-component/src/components/index.ts b/console/workspaces/libs/shared-component/src/components/index.ts index 28dc34458..0c8be7218 100644 --- a/console/workspaces/libs/shared-component/src/components/index.ts +++ b/console/workspaces/libs/shared-component/src/components/index.ts @@ -24,4 +24,5 @@ export * from './DeploymentConfig'; export * from './EnvironmentVariable'; export * from './EnvironmentCard'; export * from './ConfirmationDialog'; +export * from './EnvBulkImportModal'; export * from './ErrorPages'; diff --git a/console/workspaces/libs/shared-component/src/index.ts b/console/workspaces/libs/shared-component/src/index.ts index 4befaa8c8..175784007 100644 --- a/console/workspaces/libs/shared-component/src/index.ts +++ b/console/workspaces/libs/shared-component/src/index.ts @@ -17,4 +17,5 @@ */ export * from './components'; +export * from './utils'; export * from './utils/errorHelpers'; 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..bc2f7091d --- /dev/null +++ b/console/workspaces/libs/shared-component/src/utils/envParser.ts @@ -0,0 +1,92 @@ +/** + * 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; +} + +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(); + 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): ParseResult { + const lines = content.split(/\r?\n/); + const envMap = new Map(); + const invalid: string[] = []; + + 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; + } + + // 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 + 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 new file mode 100644 index 000000000..d2e0bf8f2 --- /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, ParseResult } from './envParser'; diff --git a/console/workspaces/pages/add-new-agent/package.json b/console/workspaces/pages/add-new-agent/package.json index 5b1e04792..4f4069e1e 100644 --- a/console/workspaces/pages/add-new-agent/package.json +++ b/console/workspaces/pages/add-new-agent/package.json @@ -49,8 +49,8 @@ "@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", "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 3292e0e7c..da5a13963 100644 --- a/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx +++ b/console/workspaces/pages/add-new-agent/src/components/EnvironmentVariable.tsx @@ -16,9 +16,11 @@ * under the License. */ +import { useState, useCallback } 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 { EnvVariableEditor } from "@agent-management-platform/views"; +import { EnvBulkImportModal, EnvVariable } from "@agent-management-platform/shared-component"; import { CreateAgentFormValues } from "../form/schema"; interface EnvironmentVariableProps { @@ -30,6 +32,7 @@ export const EnvironmentVariable = ({ formData, setFormData, }: EnvironmentVariableProps) => { + const [importModalOpen, setImportModalOpen] = useState(false); const envVariables = formData.env || []; const isOneEmpty = envVariables.some((e) => !e?.key || !e?.value); @@ -67,7 +70,6 @@ export const EnvironmentVariable = ({ ), }; } - return { ...prev, env: [ @@ -81,6 +83,24 @@ export const EnvironmentVariable = ({ }); }; + 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); + + // 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 { + ...prev, + env: Array.from(existingMap.entries()).map(([key, value]) => ({ key, value, isSensitive: false })), + }; + }); + }, [setFormData]); + + const handleModalClose = useCallback(() => setImportModalOpen(false), []); + return ( @@ -102,9 +122,9 @@ export const EnvironmentVariable = ({ onSensitiveChange={(value: boolean) => handleChange(index, 'isSensitive', value)} onRemove={() => handleRemove(index)} /> - )) : + )) : ( handleInitialEdit('isSensitive', value)} onRemove={() => handleRemove(0)} /> - } + )} + + + + - + + );