import { parseBoolean } from '../../lib/utils/common_utils'; /** * Adds the line number property * @param Object line * @param Number lineNumber */ export const parseLine = (line = {}, lineNumber) => ({ ...line, lineNumber, }); /** * When a line has `section_header` set to true, we create a new * structure to allow to nest the lines that belong to the * collapsible section * * @param Object line * @param Number lineNumber */ export const parseHeaderLine = (line = {}, lineNumber) => ({ isClosed: parseBoolean(line.section_options?.collapsed), isHeader: true, line: parseLine(line, lineNumber), lines: [], }); /** * Finds the matching header section * for the section_duration object and adds it to it * * { * isHeader: true, * line: { * content: [], * lineNumber: 0, * section_duration: "", * }, * lines: [] * } * * @param Array data * @param Object durationLine */ export function addDurationToHeader(data, durationLine) { data.forEach((el) => { if (el.line && el.line.section === durationLine.section) { el.line.section_duration = durationLine.section_duration; } }); } /** * Check is the current section belongs to a collapsible section * * @param Array acc * @param Object last * @param Object section * * @returns Boolean */ export const isCollapsibleSection = (acc = [], last = {}, section = {}) => acc.length > 0 && last.isHeader === true && !section.section_duration && section.section === last.line.section; /** * Returns the lineNumber of the last line in * a parsed log * * @param Array acc * @returns Number */ export const getIncrementalLineNumber = (acc) => { let lineNumberValue; const lastIndex = acc.length - 1; const lastElement = acc[lastIndex]; const nestedLines = lastElement.lines; if (lastElement.isHeader && !nestedLines.length && lastElement.line) { lineNumberValue = lastElement.line.lineNumber; } else if (lastElement.isHeader && nestedLines.length) { lineNumberValue = nestedLines[nestedLines.length - 1].lineNumber; } else { lineNumberValue = lastElement.lineNumber; } return lineNumberValue === 0 ? 1 : lineNumberValue + 1; }; /** * Parses the job log content into a structure usable by the template * * For collaspible lines (section_header = true): * - creates a new array to hold the lines that are collapsible, * - adds a isClosed property to handle toggle * - adds a isHeader property to handle template logic * - adds the section_duration * For each line: * - adds the index as lineNumber * * @param Array lines * @param Array accumulator * @returns Array parsed log lines */ export const logLinesParserLegacy = (lines = [], accumulator = []) => lines.reduce( (acc, line, index) => { const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index; const last = acc[acc.length - 1]; // If the object is an header, we parse it into another structure if (line.section_header) { acc.push(parseHeaderLine(line, lineNumber)); } else if (isCollapsibleSection(acc, last, line)) { // if the object belongs to a nested section, we append it to the new `lines` array of the // previously formatted header last.lines.push(parseLine(line, lineNumber)); } else if (line.section_duration) { // if the line has section_duration, we look for the correct header to add it addDurationToHeader(acc, line); } else { // otherwise it's a regular line acc.push(parseLine(line, lineNumber)); } return acc; }, [...accumulator], ); export const logLinesParser = (lines = [], previousJobLogState = {}, prevParsedLines = []) => { let currentLineCount = previousJobLogState?.prevLineCount ?? 0; let currentHeader = previousJobLogState?.currentHeader; let isPreviousLineHeader = previousJobLogState?.isPreviousLineHeader ?? false; const parsedLines = prevParsedLines.length > 0 ? prevParsedLines : []; const sectionsQueue = previousJobLogState?.sectionsQueue ?? []; for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; // First run we can use the current index, later runs we have to retrieve the last number of lines currentLineCount = previousJobLogState?.prevLineCount ? currentLineCount + 1 : i + 1; if (line.section_header && !isPreviousLineHeader) { // If there's no previous line header that means we're at the root of the log isPreviousLineHeader = true; parsedLines.push(parseHeaderLine(line, currentLineCount)); currentHeader = { index: parsedLines.length - 1 }; } else if (line.section_header && isPreviousLineHeader) { // If there's a current section, we can't push to the parsedLines array sectionsQueue.push(currentHeader); currentHeader = parseHeaderLine(line, currentLineCount); // Let's parse the incoming header line } else if (line.section && !line.section_duration) { // We're inside a collapsible section and want to parse a standard line if (currentHeader?.index) { // If the current section header is only an index, add the line as part of the lines // array of the current collapsible section parsedLines[currentHeader.index].lines.push(parseLine(line, currentLineCount)); } else { // Otherwise add it to the innermost collapsible section lines array currentHeader.lines.push(parseLine(line, currentLineCount)); } } else if (line.section && line.section_duration) { // NOTE: This marks the end of a section_header const previousSection = sectionsQueue.pop(); // Add the duration to section header // If at the root, just push the end to the current parsedLine, // otherwise, push it to the previous sections queue if (currentHeader?.index) { parsedLines[currentHeader.index].line.section_duration = line.section_duration; isPreviousLineHeader = false; currentHeader = null; } else if (currentHeader?.isHeader) { currentHeader.line.section_duration = line.section_duration; if (previousSection && previousSection?.index) { // Is the previous section on root? parsedLines[previousSection.index].lines.push(currentHeader); } else if (previousSection && !previousSection?.index) { previousSection.lines.push(currentHeader); } currentHeader = previousSection; } else { // On older job logs, there's no `section_header: true` response, it's just an object // with the `section_duration` and `section` props, so we just parse it // as a standard line parsedLines.push(parseLine(line, currentLineCount)); } } else { parsedLines.push(parseLine(line, currentLineCount)); } } return { parsedLines, auxiliaryPartialJobLogHelpers: { isPreviousLineHeader, currentHeader, sectionsQueue, prevLineCount: currentLineCount, }, }; }; /** * Finds the repeated offset, removes the old one * * Returns a new array with the updated log without * the repeated offset * * @param Array newLog * @param Array oldParsed * @returns Array * */ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => { const cloneOldLog = [...oldParsed]; const lastIndex = cloneOldLog.length - 1; const last = cloneOldLog[lastIndex]; const firstNew = newLog[0]; if (last && firstNew) { if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) { cloneOldLog.splice(lastIndex); } else if (last.lines && last.lines.length) { const lastNestedIndex = last.lines.length - 1; const lastNested = last.lines[lastNestedIndex]; if (lastNested.offset === firstNew.offset) { last.lines.splice(lastNestedIndex); } } } return cloneOldLog; }; /** * When the job log is not complete, backend may send the last received line * in the new response. * * We need to check if that is the case by looking for the offset property * before parsing the incremental part * * @param array oldLog * @param array newLog */ export const updateIncrementalJobLog = (newLog = [], oldParsed = []) => { const parsedLog = findOffsetAndRemove(newLog, oldParsed); return logLinesParserLegacy(newLog, parsedLog); };