diff --git a/api/src/server.ts b/api/src/server.ts index 157dd0e9..86dcab25 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -115,16 +115,41 @@ try { credentials: true, }, }); + let fileOffset = 0; + const CHUNK_SIZE = 1024 * 1024; // Limit batch size to 1MB // Emit initial log file content to connected clients // File watcher for log file changes watcher.on("change", async (path) => { console.info(`File changed: ${path}`); - // Read the updated file content try { - const data = await fs.promises.readFile(path, "utf8") - // Emit the updated log content to connected clients - io.emit("logUpdate", data); + const fileStats = await fs.promises.stat(path); + + // Read the entire file if there is an update (new logs or changes) + const stream = fs.createReadStream(path, { start: fileOffset }); + let fileData = ''; + + stream.on('data', (chunk) => { + fileData += chunk.toString(); // Collect the file data as a string + }); + + stream.on('end', () => { + // Emit the file content in chunks + let index = 0; + while (index < fileData?.length) { + const chunk = fileData?.slice(index, index + CHUNK_SIZE); // Get the next chunk + + io.emit('logUpdate', chunk); + index += CHUNK_SIZE; // Move to the next chunk + } + + // Update the file offset for the next read + fileOffset = fileStats.size; + }); + + stream.on('error', (err) => { + console.error('Error reading log file:', err); + }); } catch (error) { logger.error(`Error emitting log data: ${error}`); } diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 15c1fceb..8c1a16dc 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -1694,7 +1694,7 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R } }; - const handleCTDeleted = async(isContentType:boolean) => { + const handleCTDeleted = async(isContentType:boolean, contentTypes:ContentTypeList[]) => { const updatedContentTypeMapping = Object.fromEntries( Object.entries(newMigrationData?.content_mapping?.content_type_mapping || {}).filter( ([key]) => !selectedContentType?.contentstackUid.includes(key) @@ -1776,6 +1776,7 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R ...newMigrationData, content_mapping:{ ...newMigrationData?.content_mapping, + existingCT: contentTypes, content_type_mapping : updatedContentTypeMapping } @@ -1793,10 +1794,10 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R if (isContentType) { try { const { data , status} = await getExistingContentTypes(projectId, otherContentType?.id ?? ''); - if (status == 201 && data?.contentTypes?.length > 0 && data?.selectedContentType) { + if (status == 201 && data?.contentTypes?.length > 0) { (otherContentType?.id === data?.selectedContentType?.uid) && setsCsCTypeUpdated(false); - (otherContentType?.id && otherContentType?.label !== data?.selectedContentType?.title) + (otherContentType?.id && otherContentType?.label !== data?.selectedContentType?.title && data?.selectedContentType?.title) && setOtherContentType({ label: data?.selectedContentType?.title, value: data?.selectedContentType?.title, @@ -1824,8 +1825,6 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R setContentTypeSchema(data?.selectedContentType?.schema); } } else { - - await handleCTDeleted(isContentType); Notification({ notificationContent: { text: "No content found in the stack" }, notificationProps: { @@ -1835,6 +1834,9 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R type: 'error' }); } + if(otherContentType?.id && data?.contentTypes?.every((item: any) => item?.uid !== otherContentType?.id)){ + await handleCTDeleted(isContentType, data?.contentTypes); + } } catch (error) { console.log(error); return error; @@ -1843,10 +1845,10 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R try { const { data, status } = await getExistingGlobalFields(projectId, otherContentType?.id ?? ''); - if (status == 201 && data?.globalFields?.length > 0 && data?.selectedGlobalField) { + if (status == 201 && data?.globalFields?.length > 0) { (otherContentType?.id === data?.selectedGlobalField?.uid) && setsCsCTypeUpdated(false); - (otherContentType?.id && otherContentType?.label !== data?.selectedGlobalField?.title) + (otherContentType?.id && otherContentType?.label !== data?.selectedGlobalField?.title && data?.selectedGlobalField?.title) && setOtherContentType({ label: data?.selectedGlobalField?.title, value:data?.selectedGlobalField?.title, @@ -1874,8 +1876,6 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R }); } else { - await handleCTDeleted(isContentType); - Notification({ notificationContent: { text: "No Global Fields found in the stack" }, notificationProps: { @@ -1885,6 +1885,9 @@ const ContentMapper = forwardRef(({handleStepChange}: contentMapperProps, ref: R type: 'error' }); } + if(otherContentType?.id && data?.globalFields?.every((item: any) => item?.uid !== otherContentType?.id)){ + await handleCTDeleted(isContentType, data?.globalFields); + } } catch (error) { console.log(error); return error; diff --git a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx index fd9ef810..e4efff6a 100644 --- a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx +++ b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx @@ -146,7 +146,7 @@ const LoadStacks = (props: LoadFileFormatProps) => { }; - + /**** ALL METHODS HERE ****/ //Handle Legacy cms selection @@ -214,11 +214,11 @@ const LoadStacks = (props: LoadFileFormatProps) => { return stack?.value === newMigrationData?.destination_stack?.selectedStack?.value } ) - : DEFAULT_DROPDOWN; - if (stackData?.data?.stacks?.length === 0 && (!stackData?.data?.stack)) { - setIsError(true); - setErrorMessage("Please create new stack there is no stack available"); - } + : null; + // if (stackData?.data?.stacks?.length === 0 && (!stackData?.data?.stack)) { + // setIsError(true); + // setErrorMessage("Please create new stack there is no stack available"); + // } if(selectedStackData){ setSelectedStack(selectedStackData); diff --git a/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx b/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx index af7054b7..d96f647b 100644 --- a/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx +++ b/ui/src/components/LegacyCms/Actions/LoadFileFormat.tsx @@ -74,7 +74,7 @@ const LoadFileFormat = (props: LoadFileFormatProps) => { const cmsType = !isEmptyString(newMigrationData?.legacy_cms?.selectedCms?.parent) ? newMigrationData?.legacy_cms?.selectedCms?.parent : data?.cmsType?.toLowerCase(); const filePath = data?.localPath?.toLowerCase(); const fileFormat = getFileExtension(filePath); - if(! isEmptyString(selectedCard?.fileformat_id)){ + if(! isEmptyString(selectedCard?.fileformat_id) && selectedCard?.fileformat_id !== fileFormat && newMigrationData?.project_current_step > 1){ setFileIcon(selectedCard?.title); } else{ diff --git a/ui/src/components/LogScreen/MigrationLogViewer.tsx b/ui/src/components/LogScreen/MigrationLogViewer.tsx index 89fabf83..5cfe82e4 100644 --- a/ui/src/components/LogScreen/MigrationLogViewer.tsx +++ b/ui/src/components/LogScreen/MigrationLogViewer.tsx @@ -41,12 +41,18 @@ type LogsType = { handleStepChange: (currentStep: number) => void; } +export interface LogEntry { + level?: string; + message?: string; + timestamp?: string | null; +} + /** * MigrationLogViewer component displays logs received from the server. * @param {string} serverPath - The path of the server to connect to. */ const MigrationLogViewer = ({ serverPath }: LogsType) => { - const [logs, setLogs] = useState([JSON.stringify({ message: "Migration logs will appear here once the process begins.", level: '' })]); + const [logs, setLogs] = useState([{ message: "Migration logs will appear here once the process begins.", level: '' }]); const [isModalOpen, setIsModalOpen] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); @@ -75,8 +81,27 @@ const MigrationLogViewer = ({ serverPath }: LogsType) => { * @param {string} newLogs - The new logs received from the server. */ socket.on('logUpdate', (newLogs: string) => { - const logArray = newLogs.split('\n'); - setLogs(logArray); + const parsedLogsArray: LogEntry[] = []; + const logArray = newLogs?.split('\n') + + logArray?.forEach((logLine) => { + try { + //parse each log entry as a JSON object + const parsedLog = JSON?.parse(logLine); + + // Build the log object with default values + const plogs = { + level: parsedLog.level || 'info', + message: parsedLog.message || 'Unknown message', + timestamp: parsedLog.timestamp || null, + }; + parsedLogsArray.push(plogs); + }catch(error){ + console.log("error in parsing logs : ", error); + } + }); + + setLogs((prevLogs) => [...prevLogs, ...parsedLogsArray]); }); return () => { @@ -147,8 +172,8 @@ const MigrationLogViewer = ({ serverPath }: LogsType) => { logs?.forEach((log) => { try { - const logObject = JSON.parse(log); - const message = logObject.message; + //const logObject = JSON.parse(log); + const message = log.message; if (message === "Migration Process Completed") { @@ -209,38 +234,46 @@ const MigrationLogViewer = ({ serverPath }: LogsType) => { transformOrigin: "top left", transition: "transform 0.1s ease" }}> - {logs?.map((log, index) => { - const key = `${index}-${new Date().getMilliseconds()}` - try { - const logObject = JSON.parse(log); - const level = logObject.level; - const timestamp = logObject.timestamp; - const message = logObject.message; - - return ( + {logs.map((log, index) => { + try { + //const logObject = JSON.parse(log); + const { level, timestamp, message } = log; + + return ( newMigrationData?.destination_stack?.migratedStacks?.includes(newMigrationData?.destination_stack?.selectedStack?.value) ? -
-
Migration has already done in selected stack. Please create a new project.
-
- : - message === "Migration logs will appear here once the process begins." - ?
-
{message}
+
+
Migration has already done in selected stack. Please create a new project.
- :
-
{index}
-
{timestamp ? new Date(timestamp)?.toTimeString()?.split(' ')[0] : new Date()?.toTimeString()?.split(' ')[0]}
-
{message}
+ : +
+ {message === "Migration logs will appear here once the process begins." + ?
+
{message}
+
: +
+
{index}
+
+ {timestamp ? new Date(timestamp)?.toTimeString()?.split(' ')[0] : + new Date()?.toTimeString()?.split(' ')[0]} +
+
{message}
+
+ }
- ); - } catch (error) { - console.error('Invalid JSON string', error); - } - })} + ); + } catch (error) { + console.error('Invalid log format', error); + return null; + } + })}
}
- {!newMigrationData?.migration_execution?.migrationCompleted && ( + {!newMigrationData?.migration_execution?.migrationCompleted && !logs?.every((log) => log.message === "Migration logs will appear here once the process begins.") && (
diff --git a/ui/src/components/LogScreen/index.tsx b/ui/src/components/LogScreen/index.tsx index 1370695a..800fb527 100644 --- a/ui/src/components/LogScreen/index.tsx +++ b/ui/src/components/LogScreen/index.tsx @@ -9,13 +9,14 @@ import { RootState } from '../../store'; import { updateNewMigrationData } from '../../store/slice/migrationDataSlice'; // Interface -import { INewMigration } from '../../context/app/app.interface'; +import { INewMigration, TestStacks } from '../../context/app/app.interface'; // CSS import './index.scss'; import { MAGNIFY,DEMAGNIFY } from '../../common/assets'; import { saveStateToLocalStorage } from '../../utilities/functions'; +import { LogEntry } from './MigrationLogViewer'; // Define log styles for different levels const logStyles: { [key: string]: React.CSSProperties } = { @@ -39,26 +40,61 @@ type LogsType = { * @param {string} projectId - The project ID for saving state to local storage. */ const TestMigrationLogViewer = ({ serverPath, sendDataToParent,projectId }: LogsType) => { - const [logs, setLogs] = useState([JSON.stringify({ message: "Migration logs will appear here once the process begins.", level: ''})]); + const [logs, setLogs] = useState([{ message: "Migration logs will appear here once the process begins.", level: ''}]); const newMigrationData = useSelector((state: RootState) => state?.migration?.newMigrationData); + const [migratedStack, setmigratedSatck] = useState( + (newMigrationData?.testStacks ?? [])?.find((test) => test?.stackUid === newMigrationData?.test_migration?.stack_api_key)); + const [isLogsLoading, setisLogsLoading] = useState(false) // Redux dispatcher const dispatch = useDispatch(); + + + useEffect(()=>{ + const migratedTestStack = newMigrationData?.testStacks?.find((test) => test?.stackUid === newMigrationData?.test_migration?.stack_api_key); + setmigratedSatck(migratedTestStack); + },[newMigrationData?.test_migration]); + // Set up WebSocket connection useEffect(() => { - const socket = io(serverPath || ''); // Connect to the server + const socket = io(serverPath || '',{ + reconnection: true, + }); // Connect to the server + + socket.on('disconnect', () => { + console.warn('Disconnected from server. Retrying...'); + setTimeout(() => socket.connect(), 3000); // Retry connection after 3 seconds + }); /** * Event listener for 'logUpdate' event. * @param {string} newLogs - The new logs received from the server. */ socket.on('logUpdate', (newLogs: string) => { - const logArray = newLogs.split('\n'); - setLogs(logArray); - }); - + setisLogsLoading(true); + const parsedLogsArray: LogEntry[] = []; + const logArray = newLogs?.split('\n') + + logArray?.forEach((logLine) => { + try { + // parse each log entry as a JSON object + const parsedLog = JSON.parse(logLine); + + const plogs = { + level: parsedLog.level || 'info', + message: parsedLog.message || 'Unknown message', + timestamp: parsedLog.timestamp || null, + }; + parsedLogsArray.push(plogs); + + }catch(error){ + console.log("error in parsing logs : ", error); + } + }); + setLogs((prevLogs) => [...prevLogs, ...parsedLogsArray]); + }) return () => { socket.disconnect(); // Cleanup on component unmount }; @@ -120,19 +156,19 @@ const TestMigrationLogViewer = ({ serverPath, sendDataToParent,projectId }: Logs const logsContainerRef = useRef(null); - const migratedTestStack = newMigrationData?.testStacks?.find((test) => test?.stackUid === newMigrationData?.test_migration?.stack_api_key) useEffect(() => { if (logsContainerRef.current) { logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; } - logs?.forEach((log) => { + logs?.forEach((log: LogEntry) => { try { - const logObject = JSON.parse(log); - const message = logObject.message; + //const logObject = JSON.parse(log); + const message = log?.message; if (message === "Test Migration Process Completed") { + setisLogsLoading(false); // Save test migration state to local storage saveStateToLocalStorage(`testmigration_${projectId}`, { @@ -149,11 +185,21 @@ const TestMigrationLogViewer = ({ serverPath, sendDataToParent,projectId }: Logs type: 'success' }); sendDataToParent?.(false); - + const stacks = newMigrationData?.testStacks?.length > 0 ? + newMigrationData?.testStacks?.map((stack)=> + stack?.stackUid === newMigrationData?.test_migration?.stack_api_key + ? { + ...stack, + stackName: newMigrationData?.test_migration?.stack_name, + isMigrated: true + } + : stack + ) : [{stackUid: newMigrationData?.test_migration?.stack_api_key, stackName: newMigrationData?.test_migration?.stack_name, isMigrated: true}] + // Update testStacks data in Redux const newMigrationObj: INewMigration = { ...newMigrationData, - testStacks: [...newMigrationData?.testStacks ?? [], {stackUid: newMigrationData?.test_migration?.stack_api_key, stackName: newMigrationData?.test_migration?.stack_name, isMigrated: true}], + testStacks: stacks, test_migration:{ ...newMigrationData?.test_migration, isMigrationComplete:true, @@ -169,14 +215,22 @@ const TestMigrationLogViewer = ({ serverPath, sendDataToParent,projectId }: Logs }); }, [logs]); + useEffect(()=>{ + if(! isLogsLoading && !migratedStack?.isMigrated){ + setLogs([{ message: "Migration logs will appear here once the process begins.", level: ''}]); + } + + },[isLogsLoading, migratedStack?.isMigrated]); + + return (
{/* Logs container */}
- {migratedTestStack?.isMigrated + {migratedStack?.isMigrated ?
-
Test Migration is completed for stack {migratedTestStack?.stackName}
-
+
Test Migration is completed for stack {migratedStack?.stackName}
+
:
{logs?.map((log, index) => { - // const key = `${index}-${new Date().getMilliseconds()}` try { - const logObject = JSON.parse(log); - const level = logObject.level; - const timestamp = logObject.timestamp; - const message = logObject.message; + const { level, timestamp, message } = log; + return ( -
+
{message === "Migration logs will appear here once the process begins." - ?
-
{message}
-
- :
+ ?
+
{message}
+
: +
{index}
-
{ timestamp ? new Date(timestamp)?.toTimeString()?.split(' ')[0] : new Date()?.toTimeString()?.split(' ')[0]}
-
{message}
-
+
+ {timestamp ? new Date(timestamp)?.toTimeString()?.split(' ')[0] : new Date()?.toTimeString()?.split(' ')[0]} +
+
{message}
+
}
); @@ -213,7 +268,7 @@ const TestMigrationLogViewer = ({ serverPath, sendDataToParent,projectId }: Logs
{/* Action buttons for scrolling and zooming */} - {!migratedTestStack?.isMigrated && !logs?.some((log) => log === "Migration logs will appear here once the process begins.") && ( + {!migratedStack?.isMigrated && !logs?.every((log) => log.message === "Migration logs will appear here once the process begins.") && (
diff --git a/ui/src/components/TestMigration/index.tsx b/ui/src/components/TestMigration/index.tsx index 6b800ccc..80a1ffcd 100644 --- a/ui/src/components/TestMigration/index.tsx +++ b/ui/src/components/TestMigration/index.tsx @@ -75,7 +75,7 @@ const TestMigration = () => { ? !newMigrationData?.testStacks?.some( (stack) => stack?.stackUid === newMigrationData?.test_migration?.stack_api_key && - stack.isMigrated + stack?.isMigrated ) || newMigrationData?.test_migration?.isMigrationStarted : newMigrationData?.migration_execution?.migrationCompleted || newMigrationData?.migration_execution?.migrationStarted || false;