diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..5171c5408 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/.talismanrc b/.talismanrc index 5774bcaeb..1797c2062 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,6 +1,15 @@ version: "1.0.0-beta" fileignoreconfig: + - filename: ui/src/components/AuditLogs/auditLogs.interface.ts + checksum: 8eaf7502faeaa062a42ee5629f12ba1e8b961b8b8e0d209c4680c59c8de6269c + + - filename: api/src/services/migration.service.ts + checksum: bd5374b31c0fad4a266bc9affdcf595f71456edb68b075c1fdfba762f7462e38 + + - filename: ui/src/components/AuditLogs/index.tsx + checksum: e1402f3cd5b96328d5e1d1e5f2a7fa5179858d6133db831b1b96714eb0f8c260 + - filename: .github/workflows/secrets-scan.yml ignore_detectors: - filecontent @@ -40,3 +49,4 @@ fileignoreconfig: - filename: upload-api/src/helper/index.ts checksum: beef34c30cc18c55d66df0124e8bfb69899be9aaef074252afe291c93d4c0f77 + diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 000000000..1667c2976 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +migration-data +logs +database +cmsMigrationData +combine.log \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 000000000..a08a28b86 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,13 @@ +FROM --platform=linux/amd64 node:24.1.0-alpine3.22 + +WORKDIR /usr/src/app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 5001 + +CMD [ "npm","run", "dev"] \ No newline at end of file diff --git a/api/src/constants/index.ts b/api/src/constants/index.ts index c2db12a17..719de8dea 100644 --- a/api/src/constants/index.ts +++ b/api/src/constants/index.ts @@ -273,3 +273,12 @@ export const MIGRATION_DATA_CONFIG = { EXPORT_INFO_FILE: "export-info.json", }; +export const GET_AUDIT_DATA = { + MIGRATION: "migration-v2", + API_DIR: "api", + MIGRATION_DATA_DIR: "migration-data", + LOGS_DIR: "logs", + AUDIT_DIR: "audit", + AUDIT_REPORT: "audit-report", + FILTERALL: "all", +} diff --git a/api/src/controllers/migration.controller.ts b/api/src/controllers/migration.controller.ts index fbbc75996..f44d034b6 100644 --- a/api/src/controllers/migration.controller.ts +++ b/api/src/controllers/migration.controller.ts @@ -12,7 +12,10 @@ const createTestStack = async (req: Request, res: Response): Promise => { const resp = await migrationService.createTestStack(req); res.status(resp?.status).json(resp); }; - +const getAuditData = async (req: Request, res: Response): Promise => { + const resp = await migrationService.getAuditData(req); + res.status(resp?.status).json(resp); +}; /** * Start Test Migartion. * @@ -72,5 +75,6 @@ export const migrationController = { startMigration, getLogs, saveLocales, - saveMappedLocales + saveMappedLocales, + getAuditData }; diff --git a/api/src/routes/migration.routes.ts b/api/src/routes/migration.routes.ts index 9766671f0..1d6d22345 100644 --- a/api/src/routes/migration.routes.ts +++ b/api/src/routes/migration.routes.ts @@ -63,7 +63,10 @@ router.get( "/get_migration_logs/:orgId/:projectId/:stackId/:skip/:limit/:startIndex/:stopIndex/:searchText/:filter", asyncRouter(migrationController.getLogs) ) - +router.get( + "/get_audit_data/:orgId/:projectId/:stackId/:moduleName/:skip/:limit/:startIndex/:stopIndex/:searchText/:filter", + asyncRouter(migrationController?.getAuditData) +) /** * Route for updating the source locales from legacy CMS * @route POST /validator diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index 7d4e61b93..2289b6f99 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -13,6 +13,7 @@ import { LOCALE_MAPPER, STEPPER_STEPS, CMS, + GET_AUDIT_DATA } from '../constants/index.js'; import { BadRequestError, @@ -119,9 +120,8 @@ const createTestStack = async (req: Request): Promise => { return { data: { data: res.data, - url: `${ - config.CS_URL[token_payload?.region as keyof typeof config.CS_URL] - }/stack/${res.data.stack.api_key}/dashboard`, + url: `${config.CS_URL[token_payload?.region as keyof typeof config.CS_URL] + }/stack/${res.data.stack.api_key}/dashboard`, }, status: res.status, }; @@ -633,7 +633,144 @@ const startMigration = async (req: Request): Promise => { ); } }; +const getAuditData = async (req: Request): Promise => { + const projectId = path?.basename(req?.params?.projectId); + const stackId = path?.basename(req?.params?.stackId); + const moduleName = path.basename(req?.params?.moduleName); + const limit = parseInt(req?.params?.limit); + const startIndex = parseInt(req?.params?.startIndex); + const stopIndex = startIndex + limit; + const searchText = req?.params?.searchText; + const filter = req?.params?.filter; + const srcFunc = "getAuditData"; + + if (projectId?.includes('..') || stackId?.includes('..') || moduleName?.includes('..')) { + throw new BadRequestError("Invalid projectId, stackId, or moduleName"); + } + + try { + const mainPath = process?.cwd() + const logsDir = path.join(mainPath, GET_AUDIT_DATA?.MIGRATION_DATA_DIR); + + const stackFolders = fs.readdirSync(logsDir); + + const stackFolder = stackFolders?.find(folder => folder?.startsWith?.(stackId)); + if (!stackFolder) { + throw new BadRequestError("Migration data not found for this stack"); + } + const auditLogPath = path?.resolve(logsDir, stackFolder, GET_AUDIT_DATA?.LOGS_DIR, GET_AUDIT_DATA?.AUDIT_DIR, GET_AUDIT_DATA?.AUDIT_REPORT); + if (!fs.existsSync(auditLogPath)) { + throw new BadRequestError("Audit log path not found"); + } + const filePath = path?.resolve(auditLogPath, `${moduleName}.json`); + let fileData; + if (fs?.existsSync(filePath)) { + const fileContent = await fsPromises?.readFile(filePath, 'utf8'); + try { + if (typeof fileContent === 'string') { + fileData = JSON?.parse(fileContent); + } + } catch (error) { + logger.error(`Error parsing JSON from file ${filePath}:`, error); + throw new BadRequestError('Invalid JSON format in audit file'); + } + } + + if (!fileData) { + throw new BadRequestError(`No audit data found for module: ${moduleName}`); + } + let transformedData = transformAndFlattenData(fileData); + if (filter != GET_AUDIT_DATA?.FILTERALL) { + const filters = filter?.split("-"); + moduleName === 'Entries_Select_feild' ? transformedData = transformedData?.filter((log) => { + return filters?.some((filter) => { + return ( + log?.display_type?.toLowerCase()?.includes(filter?.toLowerCase()) + ); + }); + }) : transformedData = transformedData?.filter((log) => { + return filters?.some((filter) => { + return ( + log?.data_type?.toLowerCase()?.includes(filter?.toLowerCase()) + ); + }); + }); + + } + if (searchText && searchText !== null && searchText !== "null") { + transformedData = transformedData?.filter((item: any) => { + return Object?.values(item)?.some(value => + value && + typeof value === 'string' && + value?.toLowerCase?.()?.includes(searchText?.toLowerCase()) + ); + }); + } + const paginatedData = transformedData?.slice?.(startIndex, stopIndex); + + return { + data: paginatedData, + totalCount: transformedData?.length, + status: HTTP_CODES?.OK + }; + } catch (error: any) { + logger.error( + getLogMessage( + srcFunc, + `Error getting audit log data for module: ${moduleName}`, + error + ) + ); + throw new ExceptionFunction( + error?.message || HTTP_TEXTS?.INTERNAL_ERROR, + error?.statusCode || error?.status || HTTP_CODES?.SERVER_ERROR + ); + } +}; +/** + * Transforms and flattens nested data structure into an array of items + * with sequential tuid values + */ +const transformAndFlattenData = (data: any): Array<{ [key: string]: any, id: number }> => { + try { + const flattenedItems: Array<{ [key: string]: any }> = []; + if (Array.isArray(data)) { + data?.forEach((item, index) => { + flattenedItems?.push({ + ...item ?? {}, + uid: item?.uid || `item-${index}` + }); + }); + } else if (typeof data === 'object' && data !== null) { + Object?.entries?.(data)?.forEach(([key, value]) => { + if (Array.isArray(value)) { + value?.forEach((item, index) => { + flattenedItems?.push({ + ...item ?? {}, + parentKey: key, + uid: item?.uid || `${key}-${index}` + }); + }); + } else if (typeof value === 'object' && value !== null) { + flattenedItems?.push({ + ...value, + key, + uid: (value as any)?.uid || key + }); + } + }); + } + + return flattenedItems?.map((item, index) => ({ + ...item ?? {}, + id: index + 1 + })); + } catch (error) { + console.error('Error transforming data:', error); + return []; + } +}; 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) : ""; @@ -717,14 +854,14 @@ const getLogs = async (req: Request): Promise => { status: HTTP_CODES?.OK }; } else { - logger.error(getLogMessage(srcFunc, HTTP_TEXTS.LOGS_NOT_FOUND)); - throw new BadRequestError(HTTP_TEXTS.LOGS_NOT_FOUND); + logger.error(getLogMessage(srcFunc, HTTP_TEXTS?.LOGS_NOT_FOUND)); + throw new BadRequestError(HTTP_TEXTS?.LOGS_NOT_FOUND); } } catch (error: any) { - logger.error(getLogMessage(srcFunc, HTTP_TEXTS.LOGS_NOT_FOUND, error)); + logger.error(getLogMessage(srcFunc, HTTP_TEXTS?.LOGS_NOT_FOUND, error)); throw new ExceptionFunction( - error?.message || HTTP_TEXTS.INTERNAL_ERROR, - error?.statusCode || error?.status || HTTP_CODES.SERVER_ERROR + error?.message || HTTP_TEXTS?.INTERNAL_ERROR, + error?.statusCode || error?.status || HTTP_CODES?.SERVER_ERROR ); } }; @@ -829,4 +966,5 @@ export const migrationService = { getLogs, createSourceLocales, updateLocaleMapper, + getAuditData }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..396408a5c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.8" +services: + api: + container_name: migration-api + build: + context: ./api + ports: + - "5001:5001" + restart: always + + upload-api: + container_name: migration-upload-api + build: + context: ./upload-api + ports: + - "4002:4002" + restart: always + volumes: + - ${CMS_DATA_PATH}:${CONTAINER_PATH} + + ui: + container_name: migration-ui + build: + context: ./ui + ports: + - "3000:3000" + restart: always diff --git a/setup-docker.sh b/setup-docker.sh new file mode 100755 index 000000000..8e46ca552 --- /dev/null +++ b/setup-docker.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +echo "Choose the CMS (numbers):" +select CMS_TYPE in "sitecore" "contentful" "wordpress"; do + case $CMS_TYPE in + sitecore) + EXAMPLE_FILE="sitecore.zip" + break + ;; + contentful) + EXAMPLE_FILE="contentful.json" + break + ;; + wordpress) + EXAMPLE_FILE="wordpress.xml" + break + ;; + *) + echo "Invalid option. Please select 1, 2, or 3." + ;; + esac +done + +read -p "Enter the full path to your $CMS_TYPE data file (e.g., $EXAMPLE_FILE): " CMS_DATA_PATH + +if [ ! -f "$CMS_DATA_PATH" ]; then + echo "❌ File does not exist: $CMS_DATA_PATH" + exit 1 +fi + +FILENAME=$(basename "$CMS_DATA_PATH") +CONTAINER_PATH="/data/$FILENAME" + +export CMS_TYPE +export CMS_DATA_PATH +export CONTAINER_PATH + +ENV_PATH="./upload-api/.env" + +set_env_var() { + VAR_NAME="$1" + VAR_VALUE="$2" + if grep -q "^${VAR_NAME}=" "$ENV_PATH" 2>/dev/null; then + # Update existing variable (cross-platform) + sed -i.bak "s|^${VAR_NAME}=.*|${VAR_NAME}=${VAR_VALUE}|" "$ENV_PATH" + rm -f "$ENV_PATH.bak" + else + # Append new variable + echo "${VAR_NAME}=${VAR_VALUE}" >> "$ENV_PATH" + fi +} + +set_env_var "CMS_TYPE" "$CMS_TYPE" +set_env_var "CMS_DATA_PATH" "$CMS_DATA_PATH" +set_env_var "CONTAINER_PATH" "$CONTAINER_PATH" +set_env_var "NODE_BACKEND_API" "http://migration-api:5001" + + +docker compose up --build \ No newline at end of file diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 000000000..af54d5fa7 --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +build \ No newline at end of file diff --git a/ui/Dockerfile b/ui/Dockerfile new file mode 100644 index 000000000..db1e5218c --- /dev/null +++ b/ui/Dockerfile @@ -0,0 +1,13 @@ +FROM --platform=linux/amd64 node:22-alpine + +WORKDIR /usr/src/app + +COPY package*.json ./ + +RUN apk update && apk upgrade && npm install + +COPY . . + +EXPOSE 3000 + +CMD [ "npm","run", "start"] \ No newline at end of file diff --git a/ui/src/cmsData/setting.json b/ui/src/cmsData/setting.json index 65adceee0..3a4f7c9dc 100644 --- a/ui/src/cmsData/setting.json +++ b/ui/src/cmsData/setting.json @@ -29,6 +29,9 @@ "execution_logs": { "title": "Execution Logs" }, + "audit_logs": { + "title": "Audit Logs" + }, "tags": [], "locale": "en-us", "uid": "blt96cda740c3157d20", @@ -38,4 +41,4 @@ "updated_at": "2024-03-11T06:03:23.420Z", "_version": 3, "_in_progress": false -} +} \ No newline at end of file diff --git a/ui/src/components/AuditFilterModal/auditlog.interface.ts b/ui/src/components/AuditFilterModal/auditlog.interface.ts new file mode 100644 index 000000000..43cad689d --- /dev/null +++ b/ui/src/components/AuditFilterModal/auditlog.interface.ts @@ -0,0 +1,11 @@ +import { FilterOption } from "../AuditLogs/auditLogs.interface"; + +export type AuditFilterModalProps = { + isOpen: boolean; + closeModal: () => void; + updateValue: (params: { value: FilterOption; isChecked: boolean }) => void; + onApply: () => void; + selectedLevels: FilterOption[]; + setFilterValue: (levels: FilterOption[]) => void; + selectedFileType: string; +}; diff --git a/ui/src/components/AuditFilterModal/index.scss b/ui/src/components/AuditFilterModal/index.scss new file mode 100644 index 000000000..646860218 --- /dev/null +++ b/ui/src/components/AuditFilterModal/index.scss @@ -0,0 +1,85 @@ +@import '../../scss/variables'; + +.tableFilterModalStories { + position: absolute; + z-index: 1000; + width: 350px; + background-color: $color-brand-white-base; + border-radius: 12px; + box-shadow: 0 8px 24px $color-base-black-31; + display: flex; + flex-direction: column; + max-height: 350px; + overflow: hidden; + font-family: 'Inter', sans-serif; +} + +.tableFilterModalStories__header { + padding: 16px 16px; + color: #3d3f4c; + border-bottom: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: space-between; +} + +.tableFilterModalStories__suggestion-item { + padding: 8px 16px; +} + +.Checkbox { + display: flex; + align-items: center; + cursor: pointer; +} + +.Checkbox .Checkbox__tick svg { + display: block !important; +} + +.Checkbox .Checkbox__label { + font-size: $size-font-medium; + line-height: $line-height-reset; + color: #3d3f4c; + text-transform: capitalize; +} + +.tableFilterModalStories__footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 16px; + padding: 16px 8px; + border-top: 1px solid #e5e7eb; + background: #fff; + font-size: $size-font-medium; + line-height: $line-height-reset; +} + +.close-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; +} + +.close-btn:hover { + box-shadow: 0 4px 12px $color-base-black-31; + transform: scale(1.05); + border-radius: 8px; + background-color: $color-base-black-31 ; + cursor: pointer; + transition: + box-shadow 0.2s ease, + transform 0.2s ease; +} + +.close-btn:active { + transform: scale(0.95); +} + +.text-size { + font-size: $size-font-medium; + line-height: $line-height-reset; +} \ No newline at end of file diff --git a/ui/src/components/AuditFilterModal/index.tsx b/ui/src/components/AuditFilterModal/index.tsx new file mode 100644 index 000000000..2a62913e6 --- /dev/null +++ b/ui/src/components/AuditFilterModal/index.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useRef } from 'react'; +import { + Button, + ButtonGroup, + Checkbox, + Icon +} from '@contentstack/venus-components'; +import './index.scss'; +import { FilterOption } from '../AuditLogs/auditLogs.interface'; +import { auditLogsConstants } from '../../utilities/constants'; +import { AuditFilterModalProps } from './auditlog.interface'; + +const AuditFilterModal = ({ + isOpen, + closeModal, + updateValue, + onApply, + selectedLevels, + setFilterValue, + selectedFileType +}: AuditFilterModalProps) => { + const modalRef = useRef(null); + + const getFilterOptions = (): FilterOption[] => { + if (!selectedFileType) return []; + + if (selectedFileType?.includes?.('content-types') || selectedFileType?.includes?.('global-fields')) { + return [ + { label: 'global_field', value: 'global_field' }, + { label: 'reference', value: 'reference' }, + { label: 'group', value: 'group' }, + ]; + } + + if (selectedFileType?.includes?.('Entries')) { + return [{ label: 'dropdown', value: 'dropdown' }]; + } + + return []; + }; + + const filterOptions = getFilterOptions(); + + const clearAll = () => { + setFilterValue([]); + }; + + useEffect(() => { + if (isOpen && modalRef?.current) { + const modalElement = modalRef?.current; + const rect = modalElement?.getBoundingClientRect(); + const viewportHeight = window?.innerHeight; + const viewportWidth = window?.innerWidth; + + if (rect.bottom > viewportHeight) { + modalElement?.classList?.add('position-bottom'); + } + + if (rect.right > viewportWidth) { + modalElement?.classList?.add('position-right'); + } + } + }, [isOpen]); + + if (!isOpen) return
; + + return ( +
+
+ + {selectedFileType?.includes?.('Entries') ? 'Display Type' : 'Field Type'} + +
+ +
+
+ +
+ {filterOptions?.length > 0 ? ( + filterOptions.map((item) => { + const uid = item?.value + return ( +
+
+ v?.value === item?.value)} + onChange={(e: React.ChangeEvent) => + updateValue({ value: item, isChecked: e?.target?.checked }) + } + version="v2" + label={item?.label} + className="text-size" + /> +
+
+ ) + }) + ) : ( +
+ {auditLogsConstants?.filterModal?.noFilterAvailabe} +
+ )} +
+ +
+ + + + +
+
+ ); +}; + +export default AuditFilterModal; diff --git a/ui/src/components/AuditLogs/auditLogs.interface.ts b/ui/src/components/AuditLogs/auditLogs.interface.ts new file mode 100644 index 000000000..aca472a61 --- /dev/null +++ b/ui/src/components/AuditLogs/auditLogs.interface.ts @@ -0,0 +1,54 @@ +// src/components/AuditLogs/interfaces.ts + + +export interface FileData { + fileData: any; + [key: string]: any; +} + +export interface StackOption { + label: string; + value: string; + name?: string; + stackName?: string; + stackUid?: string; + [key: string]: any; +} + +export interface FileOption { + label: string; + value: string; +} + +export interface TableDataItem { + uid?: string; + name?: string; + display_name?: string; + display_type?: string; + data_type?: string; + missingRefs?: string[] | string; + treeStr?: string; + fixStatus?: string; + missingCTSelectFieldValues?: string; + parentKey?: string; + ct_uid?: string; + +} +export type DropdownOption = { + label: string; + value: string; +}; + +export type FilterOption = { + label: string; + value: string; +}; +export interface TableColumn { + Header: string; + accessor: (data: TableDataItem) => JSX.Element; + addToColumnSelector: boolean; + disableSortBy: boolean; + disableResizing: boolean; + canDragDrop: boolean; + width: number; +} \ No newline at end of file diff --git a/ui/src/components/AuditLogs/index.scss b/ui/src/components/AuditLogs/index.scss new file mode 100644 index 000000000..994387411 --- /dev/null +++ b/ui/src/components/AuditLogs/index.scss @@ -0,0 +1,55 @@ +.select-container { + display: flex; + + .select-wrapper { + display: flex; + margin-left: 1rem; + } +} + +.Search-input-show { + margin-bottom: 4px; +} + +.PageLayout--primary .PageLayout__leftSidebar+.PageLayout__content .PageLayout__body { + width: calc(100% - 15rem); +} + +.Table__head__row { + height: 100% !important; +} + +.PageLayout__body { + .table-height { + .Table { + height: calc(100vh - 12.75rem) !important; + } + + .Table.TableWithPaginated { + .Table__body { + height: calc(100vh - 18.5rem) !important; + } + } + } +} + +.custom-empty-state { + .Icon--original { + width: 207px !important; + height: auto !important; + max-width: 100%; + display: block; + margin: 0 auto; + } +} + +.Table__head__column { + align-items: center; + display: flex; + justify-content: space-between; +} + +.TablePagination { + position: sticky; + bottom: 0; +} \ No newline at end of file diff --git a/ui/src/components/AuditLogs/index.tsx b/ui/src/components/AuditLogs/index.tsx new file mode 100644 index 000000000..f36e4f5c8 --- /dev/null +++ b/ui/src/components/AuditLogs/index.tsx @@ -0,0 +1,455 @@ +import React, { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router'; +import { Button, EmptyState, InfiniteScrollTable, Select } from '@contentstack/venus-components'; +// Redux +import { RootState } from '../../store'; +// Service +import { getAuditData } from '../../services/api/project.service'; +// Interfaces +import { + StackOption, + FileOption, + TableDataItem, + FilterOption +} from './auditLogs.interface'; +import './index.scss'; +import { auditLogsConstants } from '../../utilities/constants'; +import AuditFilterModal from '../AuditFilterModal'; +const AuditLogs: React.FC = () => { + const params = useParams<{ projectId?: string }>(); + const [loading, setLoading] = useState(false); + const [selectedStack, setSelectedStack] = useState(null); + const [stackOptions, setStackOptions] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); + const [fileOptions, setFileOptions] = useState([]); + const [searchText, setSearchText] = useState(''); + const [tableData, setTableData] = useState([]); + const [totalCounts, setTotalCounts] = useState(0); + const [tableKey, setTableKey] = useState(0); + const [filterOption, setFilterOption] = useState('all'); + const [filterValue, setFilterValue] = useState([]); + const [isCursorInside, setIsCursorInside] = useState(true); + const [isFilterApplied, setIsFilterApplied] = useState(false); + const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); + const [dropDownOptions, setDropDownOptions] = useState(); + const selectedOrganisation = useSelector( + (state: RootState) => state?.authentication?.selectedOrganisation + ); + const stacks = useSelector((state: RootState) => state?.migration?.newMigrationData?.testStacks); + const isMigDone = useSelector((state: RootState) => state?.migration?.newMigrationData?.migration_execution?.migrationCompleted); + const label1 = useSelector((state: RootState) => state?.migration?.newMigrationData?.stackDetails?.label); + const value1 = useSelector((state: RootState) => state?.migration?.newMigrationData?.stackDetails?.value); + useEffect(() => { + if (stacks && stacks?.length > 0) { + const formattedOptions: StackOption[] = stacks.map((stack: any) => ({ + label: stack?.stackName, + value: stack?.stackUid, + ...stack + })); + if (isMigDone && label1 && value1) { + formattedOptions.push({ + label: label1, + value: value1, + ...stacks + }); + } + setStackOptions(formattedOptions); + if (!selectedStack) { + setSelectedStack(formattedOptions[stacks?.length - 1]); + updateFileOptionsForStack(formattedOptions[0]); + } + } + }, [stacks]); + const updateFileOptionsForStack = (stack: StackOption | null) => { + if (stack && selectedOrganisation?.value) { + const predefinedOptions: FileOption[] = [ + { label: 'Content Types', value: 'content-types' }, + { label: 'Global Fields', value: 'global-fields' }, + { label: 'Entries', value: 'Entries_Select_feild' } + ]; + setFileOptions(predefinedOptions); + } + }; + const handleStackChange = async (selectedOption: StackOption | null) => { + setSelectedStack(selectedOption); + resetFileSelection(); + if (selectedOption) { + updateFileOptionsForStack(selectedOption); + } + }; + const resetFileSelection = () => { + setSelectedFile(null); + setTableData([]); + setSearchText(''); + setTotalCounts(0); + setFilterValue([]); + setFilterOption('all'); + setIsFilterApplied(false); + }; + + const fetchTableData = async ({ + skip = 0, + limit = 30, + startIndex = 0, + stopIndex = 30, + searchText = 'null', + filter = filterOption + }) => { + if (!selectedStack || !selectedFile || !selectedOrganisation?.value) { + return { data: [], count: 0 }; + } + searchText = searchText === '' ? 'null' : searchText; + setLoading(true); + try { + const response = await getAuditData( + selectedOrganisation?.value, + params?.projectId ?? '', + selectedStack?.value, + selectedFile?.value, + skip, + limit, + startIndex, + stopIndex, + searchText, + filter + ); + if (response?.data) { + setTableData(response?.data?.data || []); + setTotalCounts(response?.data?.totalCount || 0); + return { + data: response?.data?.data || [], + count: response?.data?.totalCount || 0 + }; + } + return { data: [], count: 0 }; + } catch (error) { + console.error('Error fetching audit data:', error); + if (startIndex === 0) { + setTableData([]); + setTotalCounts(0); + } + return { data: [], count: 0 }; + } finally { + setLoading(false); + } + }; + const handleFileChange = async (selectedOption: FileOption | null) => { + setSelectedFile(selectedOption); + console.info('selectedOption', selectedOption); + setDropDownOptions(selectedOption?.value); + setSearchText(''); + setFilterValue([]); + setFilterOption('all'); + setIsFilterApplied(false); + if (selectedOption) { + setTableKey((prevKey) => prevKey + 1); + } + }; + const handleSearchChange = (value: string) => { + setSearchText(value); + setTableKey((prevKey) => prevKey + 1); + }; + const ColumnFilter = () => { + const closeModal = () => { + console.info(isFilterDropdownOpen); + setIsFilterDropdownOpen(false); + }; + const openFilterDropdown = () => { + if (!isFilterDropdownOpen) { + console.info('openFilterDropdown'); + setIsFilterDropdownOpen(true); + } + setIsFilterDropdownOpen(true); + }; + + const iconProps = { + className: isFilterApplied + ? auditLogsConstants?.filterIcon?.filterOn + : auditLogsConstants?.filterIcon?.filterOff, + withTooltip: true, + tooltipContent: 'Filter', + tooltipPosition: 'left' + }; + // Method to update filter value + const updateValue = ({ value, isChecked }: { value: FilterOption; isChecked: boolean }) => { + try { + let filterValueCopy = [...filterValue]; + if (!filterValueCopy.length && isChecked) { + filterValueCopy.push(value); + } else if (isChecked) { + const updatedFilter = filterValueCopy.filter((v) => v?.value !== value?.value); + filterValueCopy = [...updatedFilter, value]; + } else if (!isChecked) { + filterValueCopy = filterValueCopy.filter((v) => v?.value !== value?.value); + } + setFilterValue(filterValueCopy); + } catch (error) { + // console.error('Error updating filter value:', error); + } + }; + const handleClickOutside = () => { + if (!isCursorInside) { + closeModal && closeModal(); + } + }; + const onApply = () => { + try { + if (!filterValue?.length) { + const newFilter = 'all'; + setFilterOption(newFilter); + fetchTableData({ filter: newFilter }); + closeModal(); + setIsFilterApplied(false); + return; + } + const usersQueryArray = filterValue.map((item) => item.value); + const newFilter = + usersQueryArray?.length > 1 ? usersQueryArray.join('-') : usersQueryArray[0]; + setFilterOption(newFilter); + fetchTableData({ 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]); + return ( +
{ + setIsCursorInside(true); + }} + onMouseLeave={() => { + setIsCursorInside(false); + }}> +
+ ); + }; + const renderCell = (value: any) =>
{value ?? '-'}
; + const contentTypeHeader = [ + { + Header: 'Title', + accessor: (data: TableDataItem) => renderCell(data?.name), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 150 + }, + { + Header: 'Field Name', + accessor: (data: TableDataItem) => renderCell(data?.display_name), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + }, + { + Header: 'Field Type', + accessor: (data: TableDataItem) => renderCell(data?.data_type), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200, + filter: ColumnFilter + }, + { + Header: 'Missing Reference', + accessor: (data: TableDataItem) => { + const missing = Array.isArray(data?.missingRefs) + ? data.missingRefs.join(', ') + : typeof data?.missingRefs === 'string' + ? data?.missingRefs + : '-'; + + return renderCell(missing); + }, + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + }, + { + Header: 'Tree Structure', + accessor: (data: TableDataItem) => renderCell(data?.treeStr), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + }, + { + Header: 'Fix Status', + accessor: (data: TableDataItem) => renderCell(data?.fixStatus), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + } + ]; + const entryHeader = [ + { + Header: 'Entry UID', + accessor: (data: TableDataItem) => renderCell(data?.uid), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 350 + }, + { + Header: 'Name', + accessor: (data: TableDataItem) => renderCell(data?.name), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + }, + { + Header: 'Display Name', + accessor: (data: TableDataItem) => renderCell(data?.display_name), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + }, + { + Header: 'Display Type', + accessor: (data: TableDataItem) => renderCell(data?.display_type), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200, + filter: ColumnFilter + }, + { + Header: 'Missing Select Value', + accessor: (data: TableDataItem) => renderCell(data?.missingCTSelectFieldValues), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 200 + }, + { + Header: 'Tree Structure', + accessor: (data: TableDataItem) => renderCell(data?.treeStr ?? '-'), + addToColumnSelector: true, + disableSortBy: true, + disableResizing: false, + canDragDrop: true, + width: 250 + } + ]; + + const exportCtaComponent = ( +
+
+ +
+
+ ); + return ( +
+ + } + /> +
+ ); +} +export default AuditLogs; \ No newline at end of file diff --git a/ui/src/components/Common/Settings/Settings.scss b/ui/src/components/Common/Settings/Settings.scss index 8a54416db..32f4bc16e 100644 --- a/ui/src/components/Common/Settings/Settings.scss +++ b/ui/src/components/Common/Settings/Settings.scss @@ -3,18 +3,22 @@ #setting-page { display: flex; width: 100%; + .SectionHeader { color: $color-stepper-title; font-weight: $font-weight-semi-bold; margin-bottom: 2rem; } + .PageLayout { width: 100%; } + .PageLayout__head, .PageLayout__leftSidebar { - border-top: 0 none; + border-top: 1px solid $color-base-gray-40; } + .PageLayout--primary { .PageLayout__content { .PageLayout__body { @@ -24,10 +28,12 @@ } } } + .action-component-title { justify-content: space-between; width: calc(100vw - 326px); } + .content-block { margin-right: 0; padding: 1.5rem; @@ -97,6 +103,7 @@ .PageLayout__leftSidebar { background-color: #f7f9fc !important; } + .ListRowV2--active { background-color: #fff; border-left: 0.125rem solid; @@ -104,7 +111,6 @@ font-weight: 600; } - .back-button { cursor: pointer; margin-bottom: 20px; @@ -112,4 +118,4 @@ .PageLayout--primary--v2 .PageLayout__body--left-drawer { display: none; -} \ No newline at end of file +} \ 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 2040569e9..8e45c8081 100644 --- a/ui/src/components/Common/Settings/index.tsx +++ b/ui/src/components/Common/Settings/index.tsx @@ -34,6 +34,7 @@ import './Settings.scss'; import { updateNewMigrationData } from '../../../store/slice/migrationDataSlice'; import { DEFAULT_NEW_MIGRATION, INewMigration } from '../../../context/app/app.interface'; import ExecutionLog from '../../../components/ExecutionLogs'; +import AuditLogs from '../../AuditLogs'; /** * Renders the Settings component. @@ -130,7 +131,6 @@ const Settings = () => { }); } }; - const handleDeleteProject = async (closeModal: () => void): Promise => { const response = await deleteProject(selectedOrganisation?.value, params?.projectId ?? ''); @@ -155,9 +155,10 @@ const Settings = () => { const handleBack = () => { navigate(`/projects/${params?.projectId}/migration/steps/${currentStep}`); - dispatch(updateNewMigrationData({...newMigrationData, settings: DEFAULT_NEW_MIGRATION?.settings })); + dispatch(updateNewMigrationData({ ...newMigrationData, settings: DEFAULT_NEW_MIGRATION?.settings })); }; + const handleClick = () => { cbModal({ component: (props: ModalObj) => ( @@ -259,6 +260,10 @@ const Settings = () => { )} + {active_state === cmsData?.audit_logs?.title && + + + } {active_state === cmsData?.execution_logs?.title && } ) @@ -324,6 +329,23 @@ const Settings = () => { }} version="v2" /> + } + onClick={() => { + setCurrentHeader(cmsData?.audit_logs?.title); + const activeTabState: INewMigration = { + ...newMigrationData, + settings: { + active_state: cmsData?.audit_logs?.title ?? '' + } + }; + dispatch(updateNewMigrationData(activeTabState)); + }} + version="v2" + /> ) }; diff --git a/ui/src/services/api/project.service.ts b/ui/src/services/api/project.service.ts index 1ca18c813..6ac8af1cd 100644 --- a/ui/src/services/api/project.service.ts +++ b/ui/src/services/api/project.service.ts @@ -83,3 +83,14 @@ export const getMigratedStacks = async (orgId: string, projectId: string) => { } } }; +export const getAuditData = async (orgId: string, projectId: string, stackId: string, moduleName: string, skip: number, limit: number, startIndex: number, stopIndex: number, searchText: string, filter: string) => { + try { + return await getCall(`${API_VERSION}/migration/get_audit_data/${orgId}/${projectId}/${stackId}/${moduleName}/${skip}/${limit}/${startIndex}/${stopIndex}/${searchText}/${filter}`, options()); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Error in userSession: ${error?.message}`); + } else { + throw new Error('Unknown error in userSession'); + } + } +}; \ No newline at end of file diff --git a/ui/src/utilities/constants.ts b/ui/src/utilities/constants.ts index e92a2852d..d94771d41 100644 --- a/ui/src/utilities/constants.ts +++ b/ui/src/utilities/constants.ts @@ -119,6 +119,35 @@ export const VALIDATION_DOCUMENTATION_URL: { [key: string]: string } = { drupal: '' }; + +export const auditLogsConstants = { + executeTestMigration: 'Try executing Test Migration', + selectModuleMessage: 'Select Module to See the Logs', + queryChangeMessage: 'Try Changing the Search Query to find what you are looking for', + noResult: 'No Matching Result Found', + noLogs: 'No Logs Found', + filterIcon: { + filterOn: 'filterWithAppliedIcon Icon--v2 Icon--medium', + filterOff: 'filterWithAppliedIcon Icon--v2 Icon--medium Icon--disabled' + }, + + placeholders: { + selectStack: 'Select Stack', + selectModule: 'Select Module', + searchLogs: 'Search Audit Logs', + }, + + emptyStateIcon: { + noLogs: 'NoDataEmptyState', + noMatch: 'NoSearchResult' + }, + filterModal: { + noFilterAvailabe: 'No Filters Available', + clearAll: 'Clear All', + apply: 'Apply' + } +}; + export const HTTP_CODES = { OK: 200, FORBIDDEN: 403, @@ -152,7 +181,7 @@ export const EXECUTION_LOGS_UI_TEXT = { FILTER_ON: 'filterWithAppliedIcon Icon--v2 Icon--medium', FILTER_OFF: 'defaultFilterIcon Icon--v2 Icon--medium' }, - VIEW_LOG:{ + VIEW_LOG: { VIEW_ICON: 'Eye', VIEW_TEXT: 'View Log' } diff --git a/upload-api/.dockerignore b/upload-api/.dockerignore new file mode 100644 index 000000000..8c003391a --- /dev/null +++ b/upload-api/.dockerignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +extracted_files +build +combine.log \ No newline at end of file diff --git a/upload-api/Dockerfile b/upload-api/Dockerfile index 9fa0c76e3..3890502cf 100644 --- a/upload-api/Dockerfile +++ b/upload-api/Dockerfile @@ -14,7 +14,7 @@ RUN npm install COPY . . # Expose the port your app will run on -EXPOSE 3000 +EXPOSE 4002 # Define the command to run your application -CMD ["node", "index.js"] \ No newline at end of file +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/upload-api/src/config/index.ts b/upload-api/src/config/index.ts index 390883d31..c5fcc0cb1 100644 --- a/upload-api/src/config/index.ts +++ b/upload-api/src/config/index.ts @@ -2,7 +2,7 @@ export default { plan: { dropdown: { optionLimit: 100 } }, - cmsType: 'contentful', + cmsType: process.env.CMS_TYPE || 'cmsType', isLocalPath: true, awsData: { awsRegion: 'us-east-2', @@ -12,5 +12,5 @@ export default { bucketName: '', bucketKey: '' }, - localPath: 'your-local-legacy-cms-path', + localPath: process.env.CONTAINER_PATH || 'your-local-legacy-cms-path', };