Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions console/common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/**
* 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<HTMLInputElement>(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<HTMLTextAreaElement>) => {
setContent(e.target.value);
},
[]
);

// Handle file upload
const handleFileUpload = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
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 = "";
},
[]
);
Comment on lines +61 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add file size validation and error handling.

The file upload handler has the following concerns:

  1. Missing file size validation: Large files could cause memory issues or make the browser unresponsive.
  2. Missing error handling: If FileReader fails (e.g., unreadable file, permission issues), the user receives no feedback.
  3. No file type validation: Binary or non-text files could be selected and cause issues.
🔎 Proposed fix with validation and error handling
 const handleFileUpload = useCallback(
     (e: ChangeEvent<HTMLInputElement>) => {
         const file = e.target.files?.[0];
         if (!file) return;
+
+        // Validate file size (e.g., max 1MB)
+        const MAX_FILE_SIZE = 1024 * 1024; // 1MB
+        if (file.size > MAX_FILE_SIZE) {
+            alert(`File is too large. Maximum size is ${MAX_FILE_SIZE / 1024}KB.`);
+            e.target.value = "";
+            return;
+        }

         const reader = new FileReader();
+        reader.onerror = () => {
+            alert("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 = "";
     },
     []
 );

Note: Consider using a proper notification/toast system instead of alert() for better UX.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle file upload
const handleFileUpload = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
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 = "";
},
[]
);
// Handle file upload
const handleFileUpload = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate file size (e.g., max 1MB)
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
if (file.size > MAX_FILE_SIZE) {
alert(`File is too large. Maximum size is ${MAX_FILE_SIZE / 1024}KB.`);
e.target.value = "";
return;
}
const reader = new FileReader();
reader.onerror = () => {
alert("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("");
onClose();
}
}, [validCount, parseResult.valid, onImport, onClose]);

// Handle cancel/close
const handleClose = useCallback(() => {
setContent("");
onClose();
}, [onClose]);

return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>
<Box display="flex" alignItems="center" gap={1}>
<FileText size={20} />
<Typography variant="h6">
Import Environment Variables
</Typography>
</Box>
</DialogTitle>

<DialogContent>
<Box display="flex" flexDirection="column" gap={2}>
<Typography variant="body2" color="text.secondary">
Paste your .env content below or upload a file.
</Typography>

{/* Textarea for pasting .env content */}
<Box
component="textarea"
value={content}
onChange={handleContentChange}
placeholder={`# Example format:\nAPI_KEY=your_api_key\nDATABASE_URL=postgres://...\nDEBUG="true"`}
sx={{
width: "100%",
minHeight: 200,
padding: 1.5,
fontFamily: "monospace",
fontSize: 13,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
resize: "vertical",
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
"&:focus": {
outline: "none",
borderColor: theme.palette.primary.main,
},
}}
/>
Comment on lines +125 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add accessibility label to textarea.

The textarea is missing an aria-label attribute, which is an accessibility blocker for screen reader users who need to understand the purpose of the input field.

🔎 Proposed fix
 <Box
     component="textarea"
     value={content}
     onChange={handleContentChange}
     placeholder={`# Example format:\nAPI_KEY=your_api_key\nDATABASE_URL=postgres://...\nDEBUG="true"`}
+    aria-label="Environment variables content"
     sx={{
         width: "100%",
         minHeight: 200,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Box
component="textarea"
value={content}
onChange={handleContentChange}
placeholder={`# Example format:\nAPI_KEY=your_api_key\nDATABASE_URL=postgres://...\nDEBUG="true"`}
sx={{
width: "100%",
minHeight: 200,
padding: 1.5,
fontFamily: "monospace",
fontSize: 13,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
resize: "vertical",
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
"&:focus": {
outline: "none",
borderColor: theme.palette.primary.main,
},
}}
/>
<Box
component="textarea"
value={content}
onChange={handleContentChange}
placeholder={`# Example format:\nAPI_KEY=your_api_key\nDATABASE_URL=postgres://...\nDEBUG="true"`}
aria-label="Environment variables content"
sx={{
width: "100%",
minHeight: 200,
padding: 1.5,
fontFamily: "monospace",
fontSize: 13,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
resize: "vertical",
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
"&:focus": {
outline: "none",
borderColor: theme.palette.primary.main,
},
}}
/>
🤖 Prompt for AI Agents
In
@console/workspaces/libs/shared-component/src/components/EnvBulkImportModal.tsx
around lines 125-146, The textarea in EnvBulkImportModal (the Box with
component="textarea" using value={content} and onChange={handleContentChange})
lacks an accessibility label; add an ARIA label or associate it with a visible
label by adding an aria-label (e.g., "Environment variables input" or similar)
or aria-labelledby referencing a nearby label element to describe its purpose
for screen readers, ensuring the attribute is applied directly on the Box
element.


{/* File upload button */}
<Box>
<input
ref={fileInputRef}
type="file"
onChange={handleFileUpload}
style={{ display: "none" }}
/>
<Button
variant="outlined"
size="small"
startIcon={<Upload size={16} />}
onClick={handleUploadClick}
>
Upload .env File
</Button>
</Box>

{/* Variables count indicator */}
<Typography
variant="body2"
color={validCount > 0 ? "success.main" : "text.secondary"}
>
{validCount > 0
? `${validCount} valid variable${validCount !== 1 ? "s" : ""} detected`
: "No valid variables detected"}
</Typography>

{/* Invalid keys warning */}
{invalidKeys.length > 0 && (
<Box
sx={{
padding: 1.5,
backgroundColor: theme.palette.error.light + '20',
borderRadius: 1,
border: `1px solid ${theme.palette.error.light}`,
}}
>
<Typography variant="body2" color="error.main" fontWeight="medium">
{invalidKeys.length} invalid key{invalidKeys.length !== 1 ? "s" : ""} skipped:
</Typography>
<Typography variant="body2" color="error.main" sx={{ fontFamily: "monospace", mt: 0.5 }}>
{invalidKeys.join(", ")}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 0.5 }}>
Keys must start with a letter or underscore, and contain only letters, numbers, or underscores.
</Typography>
</Box>
)}
</Box>
</DialogContent>

<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button
variant="contained"
onClick={handleImport}
disabled={validCount === 0}
>
Import
</Button>
</DialogActions>
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,53 @@
* under the License.
*/

import { useState, useCallback, useMemo } 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 { useFieldArray, useFormContext, useWatch } from "react-hook-form";
import { EnvVariableEditor } from "@agent-management-platform/views";
import { EnvBulkImportModal } from "./EnvBulkImportModal";
import type { EnvVariable } from "../utils";

export const EnvironmentVariable = () => {
const { control, formState: { errors }, register } = useFormContext();
const { fields, append, remove } = useFieldArray({ control, name: 'env' });
const envValues = useWatch({ control, name: 'env' }) || [];
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);

const isOneEmpty = envValues.some((e: any) => !e?.key || !e?.value);
// Memoize envValues to stabilize dependency for useCallback
const envValues = useMemo(
() => (watchedEnvValues || []) as EnvVariable[],
[watchedEnvValues]
);

const isOneEmpty = envValues.some((e) => !e?.key || !e?.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[];

// Filter out empty rows from existing values
const nonEmptyExisting = currentEnv.filter((env) => env?.key && env?.value);

// Map existing keys to their values for merging
const existingMap = new Map<string, string>();
nonEmptyExisting.forEach((env) => {
existingMap.set(env.key, env.value);
});

// Merge: imported vars override existing ones with same key
importedVars.forEach((imported) => {
existingMap.set(imported.key, imported.value);
});

// Convert map back to array
const mergedEnv = Array.from(existingMap.entries()).map(([key, value]) => ({ key, value }));

// Replace all fields with merged result
replace(mergedEnv);
}, [getValues, replace]);

return (
<Box display="flex" flexDirection="column" gap={2} width="100%">
Expand All @@ -37,7 +73,7 @@ export const EnvironmentVariable = () => {
Set environment variables for your agent deployment.
</Typography>
<Box display="flex" flexDirection="column" gap={2}>
{fields.map((field: any, index: number) => (
{fields.map((field, index) => (
<EnvVariableEditor
key={field.id}
fieldName="env"
Expand All @@ -49,7 +85,7 @@ export const EnvironmentVariable = () => {
/>
))}
</Box>
<Box display="flex" justifyContent="flex-start" width="100%">
<Box display="flex" justifyContent="flex-start" gap={1} width="100%">
<Button
startIcon={<Add fontSize="small" />}
disabled={isOneEmpty}
Expand All @@ -59,7 +95,21 @@ export const EnvironmentVariable = () => {
>
Add
</Button>
<Button
startIcon={<FileText fontSize="small" />}
variant="outlined"
color="primary"
onClick={() => setImportModalOpen(true)}
>
Import
</Button>
</Box>

<EnvBulkImportModal
open={importModalOpen}
onClose={() => setImportModalOpen(false)}
onImport={handleImport}
/>
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export * from './EnvironmentVariable';
export * from './AgentLayout';
export * from './EnvironmentCard';
export * from './ConfirmationDialog';
export * from './EnvBulkImportModal';
1 change: 1 addition & 0 deletions console/workspaces/libs/shared-component/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
*/

export * from './components';
export * from './utils';
Loading