diff --git a/src/components/LogsView/LogsView.tsx b/src/components/LogsView/LogsView.tsx index 7a11e9f0..44efc991 100644 --- a/src/components/LogsView/LogsView.tsx +++ b/src/components/LogsView/LogsView.tsx @@ -4,86 +4,102 @@ import React, { FC, useState, useEffect, useRef } from "react"; import "./LogsView.scss"; import LogsViewEntry from "./LogsViewEntry"; -const LogsView: FC<{ runId: string, endPolling?: boolean, oneTime?: boolean, open?: boolean }> = ({ runId, endPolling = false, oneTime = false, open = false }) => { - const [showLogs, setShowLogs] = useState(false); - const bottomRef = useRef(null); - const logContentRef = useRef(null); +const LogsView: FC<{ + runId: string; + endPolling?: boolean; + oneTime?: boolean; + open?: boolean; + latestTestingProgress?: (progress: any) => any; +}> = ({ + runId, + endPolling = false, + oneTime = false, + open = false, + latestTestingProgress, +}) => { + const [showLogs, setShowLogs] = useState(false); + const bottomRef = useRef(null); + const logContentRef = useRef(null); + const lastLog = useRef(null); - const showLogView = () => { - setShowLogs(true); - const timeout = setTimeout(() => { - clearTimeout(timeout) - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); // scroll to bottom - }, 2); - } - - const hideLogView = () => { - setShowLogs(false) - } + const showLogView = () => { + setShowLogs(true); + const timeout = setTimeout(() => { + clearTimeout(timeout); + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); // scroll to bottom + }, 2); + }; + const hideLogView = () => { + setShowLogs(false); + }; - const { logInfo: logs } = useLogs( - runId, - oneTime || endPolling, - !oneTime - ) + const { logInfo: logs } = useLogs(runId, oneTime || endPolling, !oneTime); + useEffect(() => { + if (endPolling && latestTestingProgress) { + lastLog.current = logs[logs.length - 2] + if (lastLog.current?.Source.indexOf('run-certify') !== -1) { + // is 2nd last entry in logs when Source has run-certify + latestTestingProgress(JSON.parse(lastLog.current.Text)); + } + } + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); // scroll to bottom + }, [logs, endPolling, latestTestingProgress]); - useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); // scroll to bottom - }, [logs]) - - useEffect(() => { - open && showLogView() - }, [open]) + useEffect(() => { + open && showLogView(); + }, [open]); - return ( - <> -
- - View logs - -
-
-
Logs
- - - - -
-
- {logs.map((item: any, index: number) => { - let logData = '' - try { - const data = JSON.parse(item.Text) - const attr = data[Object.keys(data)[0]] - if (attr?.fields?.hasOwnProperty('launch-config')) { - logData = attr['fields']['launch-config'] - } - if (attr?.fields?.hasOwnProperty('chunk-data')) { - logData = attr['fields']['chunk-data'] - } - } catch (e) { - // do nothing - if (typeof item.Text == 'string' && item.Text.length) { - logData = item.Text - } - } - logData = !logData.length ? item.Text : logData - return logData.length ? ( - - ) : null - })} -
-
-
-
- - ); + return ( + <> +
+ + View logs + +
+
+
Logs
+ + - + +
+
+ {logs.map((item: any, index: number) => { + let logData = ""; + try { + const data = JSON.parse(item.Text); + const attr = data[Object.keys(data)[0]]; + if (attr?.fields?.hasOwnProperty("launch-config")) { + logData = attr["fields"]["launch-config"]; + } + if (attr?.fields?.hasOwnProperty("chunk-data")) { + logData = attr["fields"]["chunk-data"]; + } + } catch (e) { + // do nothing + if (typeof item.Text == "string" && item.Text.length) { + logData = item.Text; + } + } + logData = !logData.length ? item.Text : logData; + return logData.length ? ( + + ) : null; + })} +
+
+
+
+ + ); }; export default LogsView; diff --git a/src/components/StatusIcon/StatusIcon.tsx b/src/components/StatusIcon/StatusIcon.tsx new file mode 100644 index 00000000..657d4c3c --- /dev/null +++ b/src/components/StatusIcon/StatusIcon.tsx @@ -0,0 +1,18 @@ +const StatusIcon: React.FC<{ + iconName: string; + altText?: string; + }> = ({ + iconName, + altText, + ...props + }) => { + + return {altText +} + +export default StatusIcon \ No newline at end of file diff --git a/src/components/TimelineItem/timeline.helper.tsx b/src/components/TimelineItem/timeline.helper.tsx index 0f4646d3..e9a1b9bb 100644 --- a/src/components/TimelineItem/timeline.helper.tsx +++ b/src/components/TimelineItem/timeline.helper.tsx @@ -58,19 +58,6 @@ export const processTimeLineConfig = ( const currentState = status === "finished" ? "passed" : state || "running"; let returnObj: any = { ...item, state: currentState }; - // if ( - // status === "certifying" && - // currentState === "running" && - // res.data.progress && - // res.data.plan - // ) { - // const plannedTasksCount = getPlannedCertificationTaskCount(res.data.plan); - // if (plannedTasksCount > 0) { - // returnObj["progress"] = Math.trunc((res.data.progress["finished-tasks"].length / plannedTasksCount) * 100); - // } else { - // returnObj["progress"] = 0; - // } - // } return returnObj; } // Set the previously executed states as passed diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index de3158b4..9ff5de6c 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,15 +1,6 @@ /* eslint-disable no-useless-escape */ import { Dispatch, SetStateAction, useEffect, useState } from "react"; -function isValidJSON(text: string) { - try { - const result = JSON.parse(text); - return typeof result === "object" && result !== null; - } catch (e) { - return false; - } -} - // - string | null | boolean + to fix type errors while using the value function useLocalStorage( key: string, diff --git a/src/hooks/useLogs.ts b/src/hooks/useLogs.ts index fa72bbfc..444ea3c3 100644 --- a/src/hooks/useLogs.ts +++ b/src/hooks/useLogs.ts @@ -6,8 +6,6 @@ import { useDelayedApi } from "hooks/useDelayedApi"; import { fetchData } from "api/api"; import { Log } from '../pages/certification/Certification.helper' import { setStates, setEnded, setBuildInfo } from "pages/certification/slices/logRunTime.slice"; -// import { LocalStorageKeys } from "constants/constants"; -// import useLocalStorage from "./useLocalStorage"; const TIME_OFFSET = 1000; @@ -22,8 +20,6 @@ export const useLogs = ( const [fetchingLogs, setFetchingLogs] = useState(false); const [refetchLogsOffset] = useState(1); - // const [, setCertificationRunTime] = useLocalStorage(LocalStorageKeys.certificationRunTime, null) - const { startTime, endTime, runState, ended } = useAppSelector((state) => state.runTime) const enabled = !fetchingLogs && !(ended > 1) && !!uuid diff --git a/src/pages/certification/Certification.helper.tsx b/src/pages/certification/Certification.helper.tsx index 1fb37b00..a3d9bad7 100644 --- a/src/pages/certification/Certification.helper.tsx +++ b/src/pages/certification/Certification.helper.tsx @@ -1,6 +1,53 @@ import { formatToTitleCase } from "utils/utils"; -export const CertificationTasks = [{ +export interface ICertificationTask { + label: string; + key: string; + type: string; + name: string; + runTimeTaken?: any; +} + +export interface IFinishedTestingTaskDetails { + "qc-result": { + discarded: number; + expected: number; + failures: number; + succeeded: number; + }, + succeeded: boolean, + task: { + name: string; + index: number; + } +} + +export interface ITestingProgress { + "current-task": { + name: string; + index: number; + }, + "finished-tasks": Array, + "progress-index": number, + "qc-progress": { + discarded: number; + failures: number; + succeeded: number; + expected?: number; + } +} + +export interface PlanObj { + key: string; + name: string; + label: string; + discarded: number; + progress: number | 'On Going'; + expected?: number; + completed?: number; +} + +export const CertificationTasks: ICertificationTask[] = [{ label: 'UnitTests', key: '_certRes_unitTestResults', type: 'array', @@ -53,6 +100,18 @@ export const isAnyTaskFailure = (result: any) => { return flag ? true : false; } +export const isTaskSuccess = (result: any, taskName: string) => { + let failed = false; + if (taskName === '_certRes_unitTestResults') { + failed = result.filter((item: any) => (typeof item !== 'string' && item.resultOutcome.tag === 'Failure'))?.length ? true : false + } else if (taskName === '_certRes_DLTests') { + failed = result.filter((item: any) => item[1].tag === 'Failure')?.length ? true : false + } else { + failed = result.tag === "Failure" + } + return !failed +} + export const processTablesDataForChart = (resultObj: any, tableAttr: string) => { let totalCount = 0 try { @@ -93,4 +152,74 @@ export const getCertificationTaskName = (key: string) => { export const taskKeys = () => { return CertificationTasks.map(item => item.key) +} + +export const parseTestCount = (resultText: string) => { + if (resultText.indexOf("+++ OK, passed ") !== -1) { + // has the num of tests ran + return parseInt(resultText.split("+++ OK, passed ")[1].split(" tests")[0], 10) + } else { + return 1; + } +} + +export const calculateCurrentProgress = (plannedTasks: PlanObj[]) => { + return plannedTasks.reduce((accumulator, task) => { + return accumulator + (task.completed || 0) + }, 0) +} + +export const calculateExpectedProgress = (plannedTasks: PlanObj[]) => { + return plannedTasks.reduce((accumulator, task) => { + return accumulator + (task.expected || 0) + }, 0) +} + +const DEFAULT_TESTS_COUNT: number = 100; +// calculate the expected and completed to populate the Progress Card +export const getProgressCardInfo = (keyResult: any, currentTask: PlanObj) => { + const item = {...currentTask} + if (isTaskSuccess(keyResult, item.key)) { + if (item.name === 'dl-tests') { + keyResult.forEach((dlTest: any) => { + item.expected = (item.expected || 0) + (dlTest[1].numTests - dlTest[1].numDiscarded) + }) + } else if (item.name === 'unit-tests') { + keyResult.forEach((unitTest: any) => { + item.expected = (item.expected || 0) + parseTestCount(unitTest.resultDescription) + }) + } else { + item.expected = keyResult.numTests - keyResult.numDiscarded + } + item.completed = item.expected + } else { + if (!item.hasOwnProperty('expected')) { + // expected not already calculated from 'finished-tasks' + if (item.name === 'unit-tests') { + keyResult.forEach((unitTest: any) => { + if (unitTest.resultOutcome.tag === 'Failure') { + item.expected = (item.expected || 0) + 1 + item.completed = 0 + } else if (unitTest.resultOutcome.tag === 'Success') { + item.expected = (item.expected || 0) + parseTestCount(unitTest.resultDescription) + item.completed = (item.completed || 0) + parseTestCount(unitTest.resultDescription) + } + }) + } else if (item.name === 'dl-tests') { + keyResult.forEach((dlTest: any) => { + if (dlTest[1].tag === 'Success') { + item.expected = (item.expected || 0) + (dlTest[1].numTests - dlTest[1].numDiscarded) + item.completed = (item.completed || 0) + (dlTest[1].numTests - dlTest[1].numDiscarded) + } else if (dlTest[1].tag === 'Failure') { + item.expected = (item.expected || 0) + DEFAULT_TESTS_COUNT + item.completed = (item.completed || 0) + (dlTest[1].numTests || 0) + } + }) + } else { + item.expected = DEFAULT_TESTS_COUNT + item.completed = keyResult.numTests || 0 + } + } + } + return item } \ No newline at end of file diff --git a/src/pages/certification/certification-result/CertificationResult.tsx b/src/pages/certification/certification-result/CertificationResult.tsx index a7911e1a..65d90ab7 100644 --- a/src/pages/certification/certification-result/CertificationResult.tsx +++ b/src/pages/certification/certification-result/CertificationResult.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { useParams } from "react-router"; import { useLocation } from "react-router-dom"; @@ -21,7 +21,7 @@ import "../Certification.scss"; import DownloadResult from "../components/DownloadResult/DownloadResult"; import ProgressCard from "components/ProgressCard/ProgressCard"; import FullReportTable from "./FullReportTable"; -import { isAnyTaskFailure } from "../Certification.helper"; +import { calculateCurrentProgress, calculateExpectedProgress, CertificationTasks, getProgressCardInfo, ICertificationTask, isAnyTaskFailure, PlanObj } from "../Certification.helper"; const CertificationResult = () => { const param = useParams<{ uuid: string }>(); @@ -32,6 +32,8 @@ const CertificationResult = () => { const [errorToast, setErrorToast] = useState(false); const [timelineConfig, setTimelineConfig] = useState(TIMELINE_CONFIG); + const testTaskProgress = useRef([]) + useEffect(() => { (async () => { try { @@ -52,6 +54,27 @@ const CertificationResult = () => { const unitTestResult = processFinishedJson(resultJson); setUnitTestSuccess(unitTestResult); setResultData(resultJson); + const resultTaskKeys = Object.keys(resultJson) + resultTaskKeys.forEach((key: any) => { + + const TaskConfig: ICertificationTask | undefined = CertificationTasks.find((task) => task.key === key) + if (!TaskConfig || !resultJson[key]) { + // do nothing + } else { + const taskEntry = { + key: TaskConfig.key, + name: TaskConfig.name, + label: TaskConfig.label, + discarded: 0, + progress: 0 + } + + testTaskProgress.current.push({ + ...taskEntry, + ...getProgressCardInfo(resultJson[key], taskEntry) + }) + } + }) } } catch (error) { handleErrorScenario(); @@ -101,8 +124,11 @@ const CertificationResult = () => { result={resultData} coverageFile={coverageFile} />} - {/* */} - + ) : null} diff --git a/src/pages/certification/certification-result/FullReportTable.tsx b/src/pages/certification/certification-result/FullReportTable.tsx index 1cc342de..cfa94cdd 100644 --- a/src/pages/certification/certification-result/FullReportTable.tsx +++ b/src/pages/certification/certification-result/FullReportTable.tsx @@ -4,6 +4,7 @@ import TableComponent from "components/Table/Table" import { generateCollapsibleContent, processData } from "./fullReportTable.helper"; import './fullReportTable.css'; +import StatusIcon from "components/StatusIcon/StatusIcon"; const columns = [ { @@ -17,17 +18,9 @@ const columns = [ disableSortBy: true, Cell: (props: any) => { if (props.row.original.status === 'success') { - return success + return } else if (props.row.original.status === 'failure') { - return failure + return } else { return null; } } } diff --git a/src/pages/certification/components/FileCoverageContainer.tsx b/src/pages/certification/components/FileCoverageContainer.tsx index 237b35d3..8cd66c9a 100644 --- a/src/pages/certification/components/FileCoverageContainer.tsx +++ b/src/pages/certification/components/FileCoverageContainer.tsx @@ -97,7 +97,7 @@ const FileCoverageContainer: React.FC<{ return (<> {coverageIndexFiles ? (
-
    {renderRows()}
+ {renderRows()}
) : null} ); diff --git a/src/pages/certification/components/TimelineView/TimelineView.tsx b/src/pages/certification/components/TimelineView/TimelineView.tsx index d73aed2b..03c8608c 100644 --- a/src/pages/certification/components/TimelineView/TimelineView.tsx +++ b/src/pages/certification/components/TimelineView/TimelineView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useConfirm } from "material-ui-confirm"; @@ -14,8 +14,16 @@ import Timeline from "compositions/Timeline/Timeline"; import { TIMELINE_CONFIG } from "compositions/Timeline/timeline.config"; import { processFinishedJson, processTimeLineConfig } from "components/TimelineItem/timeline.helper"; import { + calculateCurrentProgress, + calculateExpectedProgress, CertificationTasks, + getProgressCardInfo, + ICertificationTask, + IFinishedTestingTaskDetails, isAnyTaskFailure, + isTaskSuccess, + ITestingProgress, + PlanObj } from "./../../Certification.helper"; import LogsView from "components/LogsView/LogsView"; import ProgressCard from "components/ProgressCard/ProgressCard"; @@ -24,16 +32,12 @@ import DownloadResult from "../DownloadResult/DownloadResult"; import FileCoverageContainer from "../FileCoverageContainer"; import { clearPersistentStates } from "../AuditorRunTestForm/utils"; import Loader from "components/Loader/Loader"; +import StatusIcon from "components/StatusIcon/StatusIcon"; const TIMEOFFSET = 1000; - -interface PlanObj { - name: string; - label: string; - discarded: number; - progress: number; -} +const PROGRESS_PASS = 100; +const PROGRESS_FAIL = -1; const TimelineView: React.FC<{ uuid: string; @@ -85,41 +89,56 @@ const TimelineView: React.FC<{ if (!plannedTestingTasks.length && resPlan.length) { setPlannedTestingTasks( resPlan.map((item: { index: number; name: string }) => { + const TaskConfig: ICertificationTask | undefined = CertificationTasks.find((task) => task.name === item.name) + if (!TaskConfig) { + return null; + } return { + key: TaskConfig.key, name: item.name, - label: CertificationTasks.find((task) => task.name === item.name) - ?.label, + label: TaskConfig.label, discarded: 0, progress: 0, }; }) ); } else if (plannedTestingTasks.length && resProgress) { - // planned tasks are already defined setPlannedTestingTasks( plannedTestingTasks.map((item: PlanObj) => { const currentTask = resProgress["current-task"]; if (currentTask && item.name === currentTask["name"]) { const currentProgressStats = resProgress["qc-progress"]; item.discarded = currentProgressStats["discarded"]; - item.progress = Math.trunc( - ((currentProgressStats["successes"] + - currentProgressStats["failures"]) / - currentProgressStats["expected"]) * - 100 - ); + item.progress = currentProgressStats["expected"] ? Math.trunc( + ((currentProgressStats["successes"] + currentProgressStats["failures"]) / + // currentProgressStats["expected"]) * 100 + (currentProgressStats["expected"] - currentProgressStats["discarded"])) * 100 + ) : "On Going"; } - if (resProgress["finished-tasks"].length) { - resProgress["finished-tasks"].forEach((entry: any) => { - if (item.name === entry["task"]["name"] && entry.succeeded) { - item.progress = 100; - } - }); + const isInFinished = resProgress["finished-tasks"].find((task: IFinishedTestingTaskDetails) => task.task.name === item.name) + if (isInFinished) { + item.discarded = isInFinished["qc-result"].discarded + item.progress = isInFinished.succeeded ? PROGRESS_PASS : PROGRESS_FAIL } - return item; + return item }) - ); + ) } + } else if (status === 'finished') { + const isArrayResult = Array.isArray(res.data.result); + const resultJson = isArrayResult + ? res.data.result[0] + : res.data.result; + setPlannedTestingTasks( + plannedTestingTasks.map((item: PlanObj) => { + if (isTaskSuccess(resultJson[item.key], item.key)) { + item.progress = PROGRESS_PASS + } else { + item.progress = PROGRESS_FAIL; + } + return {...item, ...getProgressCardInfo(resultJson[item.key], item)} + }) + ) } }; @@ -164,6 +183,42 @@ const TimelineView: React.FC<{ } }; + const processLatestTestingProgressFromLogs = (response: {status: any}) => { + if (plannedTestingTasks && response.status) { + const progress: ITestingProgress = response.status; + + plannedTestingTasks.map((task:PlanObj) => { + const finishedTask = progress["finished-tasks"].find(entry => task.name === entry.task.name) + if (finishedTask) { + if (!task.expected) { + task.expected = finishedTask["qc-result"].expected; + } + task.progress = finishedTask.succeeded ? PROGRESS_PASS : PROGRESS_FAIL; + } else if (progress['current-task'].name === task.name) { + if (!task.expected && progress['qc-progress'].hasOwnProperty('expected')) { + task.expected = progress['qc-progress'].expected + } + } + return task; + }) + } + } + + const getTaskProgress = (task: PlanObj, index: number) => { + return (runStatus === "finished" && (plannedTestingTasks.length - 1 === index)) + ? (isTaskSuccess(resultData[task.key], task.key) ? + + : + ) + : (task.progress === PROGRESS_PASS ? + + : task.progress === PROGRESS_FAIL ? + + : {task.progress}{typeof task.progress === "number" ? "%" : ""} + ) + + } + useEffect(() => { if (uuid.length) { triggerFetchRunStatus(); @@ -247,18 +302,24 @@ const TimelineView: React.FC<{ runId={uuid} endPolling={runStatus === "finished" || runState === "failed"} open={runState === "failed"} + latestTestingProgress={processLatestTestingProgressFromLogs} /> {runStatus === "certifying" || runStatus === "finished" ? ( <> -
- {runStatus === "finished" && + {runStatus === "finished" && (<> + - } - {/* */} + + )}
@@ -277,7 +338,7 @@ const TimelineView: React.FC<{ - {plannedTestingTasks.map((task: PlanObj) => { + {plannedTestingTasks.map((task: PlanObj, index: number) => { return ( - {task.progress === 100 ? ( - complete - ) : ( - {task.progress}% - )} + {getTaskProgress(task, index)} );