diff --git a/api/src/routes/migration.routes.ts b/api/src/routes/migration.routes.ts index fdcefaee6..6bcf58783 100644 --- a/api/src/routes/migration.routes.ts +++ b/api/src/routes/migration.routes.ts @@ -60,7 +60,7 @@ router.post( ); router.get( - "/get_migration_logs/:orgId/:projectId/:stackId", + "/get_migration_logs/:orgId/:projectId/:stackId/:skip/:limit/:startIndex/:stopIndex/:searchText/:filter", asyncRouter(migrationController.getLogs) ) diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index 3ea443587..88b5be057 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -1,35 +1,36 @@ -import { Request } from 'express'; -import path from 'path'; -import ProjectModelLowdb from '../models/project-lowdb.js'; -import { config } from '../config/index.js'; -import { safePromise, getLogMessage } from '../utils/index.js'; -import https from '../utils/https.utils.js'; -import { LoginServiceType } from '../models/types.js'; -import getAuthtoken from '../utils/auth.utils.js'; -import logger from '../utils/logger.js'; +import { Request } from "express"; +import path from "path"; +import ProjectModelLowdb from "../models/project-lowdb.js"; +import { config } from "../config/index.js"; +import { safePromise, getLogMessage } from "../utils/index.js"; +import https from "../utils/https.utils.js"; +import { LoginServiceType } from "../models/types.js"; +import getAuthtoken from "../utils/auth.utils.js"; +import logger from "../utils/logger.js"; import { HTTP_TEXTS, HTTP_CODES, LOCALE_MAPPER, STEPPER_STEPS, CMS, -} from '../constants/index.js'; +} from "../constants/index.js"; import { BadRequestError, ExceptionFunction, -} from '../utils/custom-errors.utils.js'; -import { fieldAttacher } from '../utils/field-attacher.utils.js'; -import { siteCoreService } from './sitecore.service.js'; -import { wordpressService } from './wordpress.service.js'; -import { testFolderCreator } from '../utils/test-folder-creator.utils.js'; -import { utilsCli } from './runCli.service.js'; -import customLogger from '../utils/custom-logger.utils.js'; -import { setLogFilePath } from '../server.js'; -import fs from 'fs'; -import { contentfulService } from './contentful.service.js'; -import { marketPlaceAppService } from './marketplace.service.js'; -import { extensionService } from './extension.service.js'; -import fsPromises from 'fs/promises'; +} from "../utils/custom-errors.utils.js"; +import { fieldAttacher } from "../utils/field-attacher.utils.js"; +import { siteCoreService } from "./sitecore.service.js"; +import { wordpressService } from "./wordpress.service.js"; +import { testFolderCreator } from "../utils/test-folder-creator.utils.js"; +import { utilsCli } from "./runCli.service.js"; +import customLogger from "../utils/custom-logger.utils.js"; +import { setLogFilePath } from "../server.js"; +import fs from "fs"; +import { contentfulService } from "./contentful.service.js"; +import { marketPlaceAppService } from "./marketplace.service.js"; +import { extensionService } from "./extension.service.js"; +import fsPromises from "fs/promises"; +import { matchesSearchText } from "../utils/search.util.js"; // import { getSafePath } from "../utils/sanitize-path.utils.js"; /** @@ -40,11 +41,11 @@ import fsPromises from 'fs/promises'; * @throws ExceptionFunction if there is an error creating the stack. */ const createTestStack = async (req: Request): Promise => { - const srcFun = 'createTestStack'; + const srcFun = "createTestStack"; const orgId = req?.params?.orgId; const projectId = req?.params?.projectId; const { name, token_payload } = req.body; - const description = 'This is a system-generated test stack.'; + const description = "This is a system-generated test stack."; const testStackName = `${name}-Test`; try { @@ -55,18 +56,18 @@ const createTestStack = async (req: Request): Promise => { await ProjectModelLowdb.read(); const projectData: any = ProjectModelLowdb.chain - .get('projects') + .get("projects") .find({ id: projectId }) .value(); const master_locale = projectData?.stackDetails?.master_locale ?? Object?.keys?.(LOCALE_MAPPER?.masterLocale)?.[0]; const testStackCount = projectData?.test_stacks?.length + 1; - const newName = testStackName + '-' + testStackCount; + const newName = testStackName + "-" + testStackCount; const [err, res] = await safePromise( https({ - method: 'POST', + method: "POST", url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, @@ -101,12 +102,12 @@ const createTestStack = async (req: Request): Promise => { } const index = ProjectModelLowdb.chain - .get('projects') + .get("projects") .findIndex({ id: projectId }) .value(); if (index > -1) { ProjectModelLowdb.update((data: any) => { - data.projects[index].current_step = STEPPER_STEPS['TESTING']; + data.projects[index].current_step = STEPPER_STEPS["TESTING"]; data.projects[index].current_test_stack_id = res?.data?.stack?.api_key; data.projects[index].test_stacks.push({ stackUid: res?.data?.stack?.api_key, @@ -128,7 +129,7 @@ const createTestStack = async (req: Request): Promise => { logger.error( getLogMessage( srcFun, - 'Error while creating a stack', + "Error while creating a stack", token_payload, error ) @@ -147,7 +148,7 @@ const createTestStack = async (req: Request): Promise => { * @returns A promise that resolves to a LoginServiceType object. */ const deleteTestStack = async (req: Request): Promise => { - const srcFun = 'deleteTestStack'; + const srcFun = "deleteTestStack"; const projectId = req?.params?.projectId; const { token_payload, stack_key } = req.body; @@ -159,7 +160,7 @@ const deleteTestStack = async (req: Request): Promise => { const [err, res] = await safePromise( https({ - method: 'DELETE', + method: "DELETE", url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, @@ -187,13 +188,13 @@ const deleteTestStack = async (req: Request): Promise => { } const index = ProjectModelLowdb.chain - .get('projects') + .get("projects") .findIndex({ id: projectId }) .value(); if (index > -1) { ProjectModelLowdb.update((data: any) => { - data.projects[index].current_test_stack_id = ''; + data.projects[index].current_test_stack_id = ""; const stackIndex = data.projects[index].test_stacks.indexOf(stack_key); if (stackIndex > -1) { data.projects[index].test_stacks.splice(stackIndex, 1); @@ -208,7 +209,7 @@ const deleteTestStack = async (req: Request): Promise => { logger.error( getLogMessage( srcFun, - 'Error while creating a stack', + "Error while creating a stack", token_payload, error ) @@ -231,7 +232,7 @@ const startTestMigration = async (req: Request): Promise => { const { region, user_id } = req?.body?.token_payload ?? {}; await ProjectModelLowdb.read(); const project: any = ProjectModelLowdb.chain - .get('projects') + .get("projects") .find({ id: projectId }) .value(); const packagePath = project?.extract_path; @@ -241,19 +242,19 @@ const startTestMigration = async (req: Request): Promise => { } = project; const loggerPath = path.join( process.cwd(), - 'logs', + "logs", projectId, `${project?.current_test_stack_id}.log` ); const message = getLogMessage( - 'startTestMigration', - 'Starting Test Migration...', + "startTestMigration", + "Starting Test Migration...", {} ); await customLogger( projectId, project?.current_test_stack_id, - 'info', + "info", message ); await setLogFilePath(loggerPath); @@ -265,17 +266,17 @@ const startTestMigration = async (req: Request): Promise => { // Path to source logs const importLogsPath = path.join( process.cwd(), - 'migration-data', + "migration-data", stackUid, - 'logs', - 'import' + "logs", + "import" ); // Read error and success logs - const errorLogPath = path.join(importLogsPath, 'error.log'); - const successLogPath = path.join(importLogsPath, 'success.log'); + const errorLogPath = path.join(importLogsPath, "error.log"); + const successLogPath = path.join(importLogsPath, "success.log"); - let combinedLogs = ''; + let combinedLogs = ""; // Read and combine error logs if ( @@ -284,8 +285,8 @@ const startTestMigration = async (req: Request): Promise => { .then(() => true) .catch(() => false) ) { - const errorLogs = await fsPromises.readFile(errorLogPath, 'utf8'); - combinedLogs += errorLogs + '\n'; + const errorLogs = await fsPromises.readFile(errorLogPath, "utf8"); + combinedLogs += errorLogs + "\n"; } // Read and combine success logs @@ -295,14 +296,14 @@ const startTestMigration = async (req: Request): Promise => { .then(() => true) .catch(() => false) ) { - const successLogs = await fsPromises.readFile(successLogPath, 'utf8'); + const successLogs = await fsPromises.readFile(successLogPath, "utf8"); combinedLogs += successLogs; } // Write combined logs to test stack log file await fsPromises.appendFile(projectLogPath, combinedLogs); } catch (error) { - console.error('Error copying logs:', error); + console.error("Error copying logs:", error); } }; @@ -335,7 +336,7 @@ const startTestMigration = async (req: Request): Promise => { destinationStackId: project?.current_test_stack_id, projectId, keyMapper: project?.mapperKeys, - project + project, }); await siteCoreService?.createLocale( req, @@ -351,24 +352,98 @@ const startTestMigration = async (req: Request): Promise => { } case CMS.WORDPRESS: { if (packagePath) { - await wordpressService?.createLocale(req, project?.current_test_stack_id, projectId, project); - await wordpressService?.getAllAssets(file_path, packagePath, project?.current_test_stack_id, projectId) - await wordpressService?.createAssetFolderFile(file_path, project?.current_test_stack_id, projectId) - await wordpressService?.getAllreference(file_path, packagePath, project?.current_test_stack_id, projectId) - await wordpressService?.extractChunks(file_path, packagePath, project?.current_test_stack_id, projectId) - await wordpressService?.getAllAuthors(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) + await wordpressService?.createLocale( + req, + project?.current_test_stack_id, + projectId, + project + ); + await wordpressService?.getAllAssets( + file_path, + packagePath, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.createAssetFolderFile( + file_path, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.getAllreference( + file_path, + packagePath, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.extractChunks( + file_path, + packagePath, + project?.current_test_stack_id, + projectId + ); + await wordpressService?.getAllAuthors( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); //await wordpressService?.extractContentTypes(projectId, project?.current_test_stack_id, contentTypes) - await wordpressService?.getAllTerms(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllTags(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllCategories(file_path, packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractPosts(packagePath, project?.current_test_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractGlobalFields(project?.current_test_stack_id, projectId) - await wordpressService?.createVersionFile(project?.current_test_stack_id, projectId); + await wordpressService?.getAllTerms( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllTags( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllCategories( + file_path, + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractPosts( + packagePath, + project?.current_test_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractGlobalFields( + project?.current_test_stack_id, + projectId + ); + await wordpressService?.createVersionFile( + project?.current_test_stack_id, + projectId + ); } break; } case CMS.CONTENTFUL: { - const cleanLocalPath = file_path?.replace?.(/\/$/, ''); + const cleanLocalPath = file_path?.replace?.(/\/$/, ""); await contentfulService?.createLocale( cleanLocalPath, project?.current_test_stack_id, @@ -438,12 +513,12 @@ const startMigration = async (req: Request): Promise => { const { region, user_id } = req?.body?.token_payload ?? {}; await ProjectModelLowdb.read(); const project: any = ProjectModelLowdb.chain - .get('projects') + .get("projects") .find({ id: projectId }) .value(); const index = ProjectModelLowdb.chain - .get('projects') + .get("projects") .findIndex({ id: projectId }) .value(); if (index > -1) { @@ -459,7 +534,7 @@ const startMigration = async (req: Request): Promise => { } = project; const loggerPath = path.join( process.cwd(), - 'logs', + "logs", projectId, `${project?.destination_stack_id}.log` ); @@ -473,17 +548,17 @@ const startMigration = async (req: Request): Promise => { // Path to source logs const importLogsPath = path.join( process.cwd(), - 'migration-data', + "migration-data", stackUid, - 'logs', - 'import' + "logs", + "import" ); // Read error and success logs - const errorLogPath = path.join(importLogsPath, 'error.log'); - const successLogPath = path.join(importLogsPath, 'success.log'); + const errorLogPath = path.join(importLogsPath, "error.log"); + const successLogPath = path.join(importLogsPath, "success.log"); - let combinedLogs = ''; + let combinedLogs = ""; // Read and combine error logs if ( @@ -492,8 +567,8 @@ const startMigration = async (req: Request): Promise => { .then(() => true) .catch(() => false) ) { - const errorLogs = await fsPromises.readFile(errorLogPath, 'utf8'); - combinedLogs += errorLogs + '\n'; + const errorLogs = await fsPromises.readFile(errorLogPath, "utf8"); + combinedLogs += errorLogs + "\n"; } // Read and combine success logs @@ -503,14 +578,14 @@ const startMigration = async (req: Request): Promise => { .then(() => true) .catch(() => false) ) { - const successLogs = await fsPromises.readFile(successLogPath, 'utf8'); + const successLogs = await fsPromises.readFile(successLogPath, "utf8"); combinedLogs += successLogs; } // Write combined logs to stack log file await fsPromises.appendFile(projectLogPath, combinedLogs); } catch (error) { - console.error('Error copying logs:', error); + console.error("Error copying logs:", error); } }; @@ -544,7 +619,7 @@ const startMigration = async (req: Request): Promise => { destinationStackId: project?.destination_stack_id, projectId, keyMapper: project?.mapperKeys, - project + project, }); await siteCoreService?.createLocale( req, @@ -560,24 +635,98 @@ const startMigration = async (req: Request): Promise => { } case CMS.WORDPRESS: { if (packagePath) { - await wordpressService?.createLocale(req, project?.current_test_stack_id, projectId, project); - await wordpressService?.getAllAssets(file_path, packagePath, project?.destination_stack_id, projectId,) - await wordpressService?.createAssetFolderFile(file_path, project?.destination_stack_id, projectId) - await wordpressService?.getAllreference(file_path, packagePath, project?.destination_stack_id, projectId) - await wordpressService?.extractChunks(file_path, packagePath, project?.destination_stack_id, projectId) - await wordpressService?.getAllAuthors(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) + await wordpressService?.createLocale( + req, + project?.current_test_stack_id, + projectId, + project + ); + await wordpressService?.getAllAssets( + file_path, + packagePath, + project?.destination_stack_id, + projectId + ); + await wordpressService?.createAssetFolderFile( + file_path, + project?.destination_stack_id, + projectId + ); + await wordpressService?.getAllreference( + file_path, + packagePath, + project?.destination_stack_id, + projectId + ); + await wordpressService?.extractChunks( + file_path, + packagePath, + project?.destination_stack_id, + projectId + ); + await wordpressService?.getAllAuthors( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); //await wordpressService?.extractContentTypes(projectId, project?.destination_stack_id) - await wordpressService?.getAllTerms(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllTags(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.getAllCategories(file_path, packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractPosts(packagePath, project?.destination_stack_id, projectId, contentTypes, project?.mapperKeys, project?.stackDetails?.master_locale, project) - await wordpressService?.extractGlobalFields(project?.destination_stack_id, projectId) - await wordpressService?.createVersionFile(project?.destination_stack_id, projectId); + await wordpressService?.getAllTerms( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllTags( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.getAllCategories( + file_path, + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractPosts( + packagePath, + project?.destination_stack_id, + projectId, + contentTypes, + project?.mapperKeys, + project?.stackDetails?.master_locale, + project + ); + await wordpressService?.extractGlobalFields( + project?.destination_stack_id, + projectId + ); + await wordpressService?.createVersionFile( + project?.destination_stack_id, + projectId + ); } break; } case CMS.CONTENTFUL: { - const cleanLocalPath = file_path?.replace?.(/\/$/, ''); + const cleanLocalPath = file_path?.replace?.(/\/$/, ""); await contentfulService?.createLocale( cleanLocalPath, project?.destination_stack_id, @@ -633,37 +782,82 @@ const startMigration = async (req: Request): Promise => { } }; -const getLogs = async (req: Request): Promise => { - const projectId = path.basename(req?.params?.projectId); - const stackId = path.basename(req?.params?.stackId); - const srcFunc = 'getLogs'; - if (projectId.includes('..') || stackId.includes('..')) { - throw new BadRequestError('Invalid projectId or stackId'); +const getLogs = async (req: Request): Promise => { + const projectId = req?.params?.projectId ? path?.basename(req.params.projectId) : ""; + const stackId = req?.params?.stackId ? path?.basename(req.params.stackId) : ""; + const limit = req?.params?.limit ? parseInt(req.params.limit) : 10; + const startIndex = req?.params?.startIndex ? parseInt(req.params.startIndex) : 0; + const stopIndex = startIndex + limit; + const searchText = req?.params?.searchText ?? null; + const filter = req?.params?.filter ?? "all"; + + const srcFunc = "getLogs"; + + if ( + !projectId || + !stackId || + projectId?.includes("..") || + stackId?.includes("..") + ) { + throw new BadRequestError("Invalid projectId or stackId"); } try { - const logsDir = path.join(process.cwd(), 'logs'); - const loggerPath = path.join(logsDir, projectId, `${stackId}.log`); - const absolutePath = path.resolve(loggerPath); // Resolve the absolute path + const mainPath = process?.cwd()?.split("migration-v2")?.[0]; + if (!mainPath) { + throw new BadRequestError("Invalid application path"); + } - if (!absolutePath.startsWith(logsDir)) { - throw new BadRequestError('Access to this file is not allowed.'); + const logsDir = path?.join(mainPath, "migration-v2", "api", "logs"); + const loggerPath = path?.join(logsDir, projectId, `${stackId}.log`); + const absolutePath = path?.resolve(loggerPath); + + if (!absolutePath?.startsWith(logsDir)) { + throw new BadRequestError("Access to this file is not allowed."); } if (fs.existsSync(absolutePath)) { - const logs = await fs.promises.readFile(absolutePath, 'utf8'); - const logEntries = logs - .split('\n') - .map((line) => { + const logs = await fs.promises.readFile(absolutePath, "utf8"); + let logEntries = logs + ?.split("\n") + ?.map((line) => { try { - return JSON.parse(line); + return line ? JSON?.parse(line) : null; } catch (error) { return null; } }) - .filter((entry) => entry !== null); - return logEntries; + ?.filter?.((entry) => entry !== null); + + if (!logEntries?.length) { + return { logs: [], total: 0 }; + } + + logEntries = logEntries?.slice?.(1, logEntries?.length - 2); + + if (filter !== "all") { + const filters = filter?.split("-") ?? []; + logEntries = logEntries?.filter((log) => { + return filters?.some((filter) => { + return log?.level + ?.toLowerCase() + ?.includes?.(filter?.toLowerCase() ?? ""); + }); + }); + } + + if (searchText && searchText !== "null") { + logEntries = logEntries?.filter?.((log) => + matchesSearchText(log, searchText) + ); + } + + const paginatedLogs = logEntries?.slice?.(startIndex, stopIndex) ?? []; + return { + logs: paginatedLogs, + total: logEntries?.length ?? 0, + }; } else { logger.error(getLogMessage(srcFunc, HTTP_TEXTS.LOGS_NOT_FOUND)); throw new BadRequestError(HTTP_TEXTS.LOGS_NOT_FOUND); @@ -692,7 +886,7 @@ export const createSourceLocales = async (req: Request) => { // Find the project with the specified projectId await ProjectModelLowdb?.read?.(); const index = ProjectModelLowdb?.chain - ?.get?.('projects') + ?.get?.("projects") ?.findIndex?.({ id: projectId }) ?.value?.(); if (index > -1) { @@ -707,11 +901,11 @@ export const createSourceLocales = async (req: Request) => { } } catch (err: any) { console.error( - '🚀 ~ createSourceLocales ~ err:', + "🚀 ~ createSourceLocales ~ err:", err?.response?.data ?? err, err ); - logger.warn('Bad Request', { + logger.warn("Bad Request", { status: HTTP_CODES?.BAD_REQUEST, message: HTTP_TEXTS?.INTERNAL_ERROR, }); @@ -737,7 +931,7 @@ export const updateLocaleMapper = async (req: Request) => { // Find the project with the specified projectId await ProjectModelLowdb?.read?.(); const index = ProjectModelLowdb?.chain - ?.get?.('projects') + ?.get?.("projects") ?.findIndex?.({ id: projectId }) ?.value?.(); if (index > -1) { @@ -754,11 +948,11 @@ export const updateLocaleMapper = async (req: Request) => { } } catch (err: any) { console.error( - '🚀 ~ updateLocaleMapper ~ err:', + "🚀 ~ updateLocaleMapper ~ err:", err?.response?.data ?? err, err ); - logger.warn('Bad Request', { + logger.warn("Bad Request", { status: HTTP_CODES?.BAD_REQUEST, message: HTTP_TEXTS?.INTERNAL_ERROR, }); diff --git a/api/src/utils/search.util.ts b/api/src/utils/search.util.ts new file mode 100644 index 000000000..b114ca933 --- /dev/null +++ b/api/src/utils/search.util.ts @@ -0,0 +1,13 @@ +import { LogEntry } from "winston/index.js"; + +export const matchesSearchText = (log: LogEntry, searchText: string): boolean => { + if (!searchText || searchText === "null") return true; + + const loweredSearch = searchText.toLowerCase(); + + const fieldsToSearch = ["level", "message", "methodName", "timestamp"]; + + return fieldsToSearch.some((field) => + log?.[field]?.toString()?.toLowerCase()?.includes(loweredSearch) + ); +}; \ No newline at end of file diff --git a/ui/src/cmsData/setting.json b/ui/src/cmsData/setting.json index 5967843c6..692a38c09 100644 --- a/ui/src/cmsData/setting.json +++ b/ui/src/cmsData/setting.json @@ -12,15 +12,18 @@ "theme": "secondary", "title": "Delete Project", "url": "", - "with_icon": true + "with_icon": true, + "icon": "Delete" }, "save_project": { "open_in_new_tab": false, "theme": "primary", "title": "Save", "url": "", - "with_icon": true - } + "with_icon": true, + "icon": "v2-Save" + }, + "back_button": "LeftArrow" }, "execution_logs": { "title": "Execution Logs" diff --git a/ui/src/components/Common/Settings/Settings.scss b/ui/src/components/Common/Settings/Settings.scss index bfdca554e..4cefaab17 100644 --- a/ui/src/components/Common/Settings/Settings.scss +++ b/ui/src/components/Common/Settings/Settings.scss @@ -103,3 +103,8 @@ color: #6c5ce7 !important; font-weight: 600; } + +.back-button{ + cursor: pointer; + margin-bottom: 20px ; +} \ No newline at end of file diff --git a/ui/src/components/Common/Settings/index.tsx b/ui/src/components/Common/Settings/index.tsx index 63beb0bea..82f334f9d 100644 --- a/ui/src/components/Common/Settings/index.tsx +++ b/ui/src/components/Common/Settings/index.tsx @@ -1,4 +1,3 @@ -// Libraries import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { Params, useNavigate, useParams } from 'react-router'; @@ -24,7 +23,7 @@ import { ModalObj } from '../../../components/Modal/modal.interface'; // Service import { deleteProject, getProject, updateProject } from '../../../services/api/project.service'; -import { CS_ENTRIES } from '../../../utilities/constants'; +import { CS_ENTRIES, HTTP_CODES } from '../../../utilities/constants'; import { getCMSDataFromFile } from '../../../cmsData/cmsSelector'; // Component @@ -35,6 +34,7 @@ import './Settings.scss'; import { useDispatch } from 'react-redux'; import { updateNewMigrationData } from '../../../store/slice/migrationDataSlice'; import { DEFAULT_NEW_MIGRATION } from '../../../context/app/app.interface'; +import ExecutionLog from '../../../components/ExecutionLogs'; /** * Renders the Settings component. @@ -47,14 +47,19 @@ const Settings = () => { const [active, setActive] = useState(); const [currentHeader, setCurrentHeader] = useState(); const [projectName, setProjectName] = useState(''); + const [projectId, setProjectId] = useState(''); const [projectDescription, setProjectDescription] = useState(''); const selectedOrganisation = useSelector( (state: RootState) => state?.authentication?.selectedOrganisation ); + const currentStep = useSelector( + (state: RootState) => state?.migration?.newMigrationData?.project_current_step + ); + const navigate = useNavigate(); - const dispatch = useDispatch() + const dispatch = useDispatch(); useEffect(() => { const fetchData = async () => { @@ -75,9 +80,10 @@ const Settings = () => { params?.projectId ?? '' ); - if (status === 200) { + if (status === HTTP_CODES.OK) { setProjectName(data?.name); setProjectDescription(data?.description); + setProjectId(params?.projectId ?? ''); } }; @@ -104,7 +110,7 @@ const Settings = () => { projectData ); - if (status === 200) { + if (status === HTTP_CODES.OK) { Notification({ notificationContent: { text: 'Project Updated Successfully' }, notificationProps: { @@ -124,29 +130,34 @@ const Settings = () => { }); } }; - const handleDeleteProject = async (closeModal: ()=> void): Promise => { - //setIsLoading(true); - const response = await deleteProject(selectedOrganisation?.value, params?.projectId ?? ''); - - if (response?.status === 200) { - //setIsLoading(false); - closeModal(); - dispatch(updateNewMigrationData(DEFAULT_NEW_MIGRATION)); - setTimeout(() => { - navigate('/projects'); - }, 800); - setTimeout(() => { - Notification({ - notificationContent: { text: response?.data?.data?.message }, - notificationProps: { - position: 'bottom-center', - hideProgressBar: true - }, - type: 'success' - }); - }, 1200); - } - }; + + const handleDeleteProject = async (closeModal: () => void): Promise => { + //setIsLoading(true); + const response = await deleteProject(selectedOrganisation?.value, params?.projectId ?? ''); + + if (response?.status === HTTP_CODES.OK) { + //setIsLoading(false); + closeModal(); + dispatch(updateNewMigrationData(DEFAULT_NEW_MIGRATION)); + setTimeout(() => { + navigate('/projects'); + }, 800); + setTimeout(() => { + Notification({ + notificationContent: { text: response?.data?.data?.message }, + notificationProps: { + position: 'bottom-center', + hideProgressBar: true + }, + type: 'success' + }); + }, 1200); + } + }; + + const handleBack = () => { + navigate(`/projects/${params?.projectId}/migration/steps/${currentStep}`); + } const handleClick = () => { cbModal({ @@ -178,15 +189,13 @@ const Settings = () => { class="Button Button--secondary Button--size-large Button--icon-alignment-left Button--v2" aria-label="Delete Project for deleting project" type="button" - onClick={handleClick} - > + onClick={handleClick}>
+ data={cmsData?.project?.delete_project?.title}>
@@ -214,8 +223,7 @@ const Settings = () => { aria-label="projectname" version="v2" value={projectName} - onChange={handleProjectNameChange} - > + onChange={handleProjectNameChange}> @@ -241,11 +249,10 @@ const Settings = () => { buttonType="primary" aria-label="save for saving update" version="v2" - icon={'v2-Save'} + icon={cmsData?.project?.save_project?.icon} autoClose={5000} label={'Success'} - onClick={handleUpdateProject} - > + onClick={handleUpdateProject}> {cmsData?.project?.save_project?.title} @@ -253,7 +260,9 @@ const Settings = () => { )} - {active === cmsData?.execution_logs?.title &&
} + {active === cmsData?.execution_logs?.title && ( + + )} ) }; @@ -265,8 +274,21 @@ const Settings = () => { data-testid="cs-section-header" className="SectionHeader SectionHeader--extra-bold SectionHeader--medium SectionHeader--black SectionHeader--v2" aria-label={cmsData?.title} - aria-level={1} - > + aria-level={1}> +
+ { + handleBack(); + }} + withTooltip={true} + tooltipContent={'Back'} + tooltipPosition="right" + className='back-button' + /> +
{cmsData?.title} @@ -281,6 +303,18 @@ const Settings = () => { }} version="v2" /> + + } + onClick={() => { + setActive(cmsData?.execution_logs?.title); + setCurrentHeader(cmsData?.execution_logs?.title); + }} + version="v2" + /> ) }; diff --git a/ui/src/components/Common/Settings/setting.interface.ts b/ui/src/components/Common/Settings/setting.interface.ts index ec8f6ad07..78a3ac06d 100644 --- a/ui/src/components/Common/Settings/setting.interface.ts +++ b/ui/src/components/Common/Settings/setting.interface.ts @@ -10,6 +10,7 @@ interface IProject { save_project: CTA; email: string; description_placeholder: string; + back_button: string; } /** * Represents a Call to Action (CTA) object. @@ -20,6 +21,7 @@ interface CTA { title: string; url: string; with_icon: boolean; + icon: string } interface IExecutionLogs { title: string; diff --git a/ui/src/components/ExecutionLogs/executionlog.interface.ts b/ui/src/components/ExecutionLogs/executionlog.interface.ts new file mode 100644 index 000000000..ebdc9e183 --- /dev/null +++ b/ui/src/components/ExecutionLogs/executionlog.interface.ts @@ -0,0 +1,23 @@ +export type LogEntry = { + level: string; + message: string; + methodName: string; + timestamp: string; +}; + +export type StackIds = { + stackUid?: string; + stackName?: string; + isMigrated?: boolean; +}; + + +export type DropdownOption = { + label: string ; + value: string; +}; + +export type FilterOption = { + label: string; + value: string; +}; \ No newline at end of file diff --git a/ui/src/components/ExecutionLogs/index.scss b/ui/src/components/ExecutionLogs/index.scss new file mode 100644 index 000000000..325927294 --- /dev/null +++ b/ui/src/components/ExecutionLogs/index.scss @@ -0,0 +1,49 @@ +.Search-input-show { + margin-bottom: 8px; + width: 300px; +} +.Search .Search__search-icon { + top: calc(50% - 10px); + width: 20px; +} + +.Search .Search__input.regular-corners { + margin-left: 8px; +} + +.dropdown-wrapper { + margin-bottom: 8px; +} + +.PageLayout--primary .PageLayout__leftSidebar + .PageLayout__content .PageLayout__body { + width: calc(100% - 15.4rem); +} + +.Table__head__column { + align-items: center; + display: flex; + justify-content: space-between; +} + +.Table__head{ + height: auto; +} + +.Table:has(.custom-empty-state) { + height: 40.5rem; +} + + +.custom-empty-state { + .Icon--original { + width: 207px !important; + height: auto !important; + max-width: 100%; + display: block; + margin: 0 auto; + } +} + +.select-container { + margin-bottom: 8px; +} diff --git a/ui/src/components/ExecutionLogs/index.tsx b/ui/src/components/ExecutionLogs/index.tsx new file mode 100644 index 000000000..4c51547ca --- /dev/null +++ b/ui/src/components/ExecutionLogs/index.tsx @@ -0,0 +1,341 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + InfiniteScrollTable, + Button, + EmptyState, + Select +} from '@contentstack/venus-components'; +import { RootState } from '../../store'; +import { DropdownOption, FilterOption, LogEntry, StackIds } from './executionlog.interface'; +import './index.scss'; + +import FilterModal from '../FilterModale'; +import { getMigrationLogs } from '../../services/api/migration.service'; +import { EXECUTION_LOGS_UI_TEXT } from '../../utilities/constants'; + +const ExecutionLogs = ({ projectId }: { projectId: string }) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [totalCounts, setTotalCounts] = useState(0); + const [searchText, setSearchText] = useState(''); + const [filterOption, setFilterOption] = useState([]); + const [isCursorInside, setIsCursorInside] = useState(false); + const [isFilterApplied, setIsFilterApplied] = useState(false); + const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); + const [filterValue, setFilterValue] = useState('all'); + + const selectedOrganisation = useSelector( + (state: RootState) => state?.authentication?.selectedOrganisation + ); + + const testStacks = useSelector( + (state: RootState) => state?.migration?.newMigrationData?.testStacks + ); + + const mainStack = useSelector( + (state: RootState) => state?.migration?.newMigrationData?.stackDetails + ); + const migrationCompleted = useSelector( + (state: RootState) => + state?.migration?.newMigrationData?.migration_execution?.migrationCompleted + ); + + const stackIds = testStacks?.map?.((stack: StackIds) => ({ + label: stack?.stackName, + value: stack?.stackUid + })); + + if (migrationCompleted) { + stackIds?.push({ + label: mainStack?.label, + value: mainStack?.value + }); + } + + const [selectedStack, setSelectedStack] = useState( + { + label: stackIds?.[stackIds?.length - 1]?.label ?? '' , + value: stackIds?.[stackIds?.length - 1]?.value ?? '' + } + ); + + useEffect(() => { + if (selectedStack) { + fetchData({}); + } + }, [selectedStack]); + + const ColumnFilter = () => { + const closeModal = () => { + setIsFilterDropdownOpen(false); + }; + + const openFilterDropdown = () => { + if (!isFilterDropdownOpen) { + setIsFilterDropdownOpen(true); + } + }; + + const handleClickOutside = () => { + if (!isCursorInside) { + closeModal && closeModal(); + } + }; + + //Method to maintain filter value + const updateValue = ({ value, isChecked }: { value: FilterOption; isChecked: boolean }) => { + try { + let filterValueCopy: FilterOption[] = [...filterOption]; + + if (!filterValueCopy?.length && isChecked) { + filterValueCopy?.push(value); + } else if (isChecked) { + // Remove the old value and keep updated one in case old value exists + const updatedFilter = filterValueCopy?.filter((v) => v?.value !== value?.value); + filterValueCopy = [...updatedFilter, value]; + } else if (!isChecked) { + filterValueCopy = filterValueCopy?.filter((v) => v?.value !== value?.value); + } + + setFilterOption(filterValueCopy); + } catch (error) { + console.error('Error updating filter value:', error); + } + }; + + // Method to handle Apply + const onApply = () => { + try { + if (!filterOption?.length) { + const newFilter = 'all'; + setFilterValue(newFilter); + fetchData({ filter: newFilter }); + closeModal(); + return; + } + + const usersQueryArray = filterOption?.map((item) => item?.value); + const newFilter = usersQueryArray?.length > 1 ? usersQueryArray?.join('-') : usersQueryArray?.[0]; + setFilterValue(newFilter); + fetchData({ filter: newFilter }); + setIsFilterApplied(true); + closeModal(); + } catch (error) { + console.error('Error applying filter:', error); + } + }; + + useEffect(() => { + document.addEventListener('click', handleClickOutside, false); + return () => { + document.removeEventListener('click', handleClickOutside, false); + }; + }, [isCursorInside]); + + const iconProps = { + className: isFilterApplied + ? EXECUTION_LOGS_UI_TEXT.FILTER_ICON.FILTER_ON + : EXECUTION_LOGS_UI_TEXT.FILTER_ICON.FILTER_OFF, + withTooltip: true, + tooltipContent: 'Filter', + tooltipPosition: 'left' + }; + + return ( +
{ + setIsCursorInside(true); + }} + onMouseLeave={() => { + setIsCursorInside(false); + }}> +
+ ); + }; + + const columns = [ + { + Header: 'Timestamp', + disableSortBy: true, + accessor: (data: LogEntry) => { + if (data?.timestamp) { + const date = new Date(data?.timestamp); + const options: Intl.DateTimeFormatOptions = { + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: true + }; + const formatted = new Intl.DateTimeFormat('en-US', options)?.format(date); + return
{formatted}
; + } + return
No Data Available
; + }, + width: 240 + }, + { + Header: 'Level', + id: 'level', + disableSortBy: true, + accessor: (data: LogEntry) =>
{data?.level}
, + width: 150, + filter: ColumnFilter + }, + { + Header: 'Message', + disableSortBy: true, + accessor: (data: LogEntry) => { + return ( +
+
{data?.message}
+
+ ); + }, + width: 550 + }, + { + Header: 'Method Name', + disableSortBy: true, + accessor: (data: LogEntry) => { + return ( +
+
{data?.methodName}
+
+ ); + }, + + width: 200 + } + ]; + + // Method to fetch data from API + const fetchData = async ({ + skip = 0, + limit = 30, + startIndex = 0, + stopIndex = 30, + searchText = 'null', + filter = filterValue + }) => { + searchText = searchText === '' ? 'null' : searchText; + + if (!selectedStack) { + setLoading(false); + return; + } + + setLoading(true); + try { + const response = await getMigrationLogs( + selectedOrganisation?.value || '', + projectId, + selectedStack?.value, + skip, + limit, + startIndex, + stopIndex, + searchText, + filter + ); + + if (response?.status !== 200) { + console.error('Error fetching logs:', response); + setData([]); + setTotalCounts(0); + } else { + setData(response?.data?.logs); + setTotalCounts(response?.data?.total); + } + } catch (error) { + console.error('Unexpected error while fetching logs:', error); + setData([]); + setTotalCounts(0); + } finally { + setLoading(false); + } + }; + + return ( +
+ setSearchText(value)} + withExportCta={{ + component: ( +