diff --git a/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/utils.test.ts b/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/utils.test.ts index 97dd34759..a5ffcdf57 100644 --- a/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/utils.test.ts +++ b/apps/parsley/src/components/LogRow/BaseRow/SharingMenu/utils.test.ts @@ -26,14 +26,16 @@ describe("getLinesInProcessedLogLinesFromSelectedLines", () => { 2, { range: { end: 4, start: 3 }, rowType: RowType.SkippedLines }, { + functionID: "function-4", functionName: "test", isOpen: true, range: { end: 6, start: 4 }, rowType: RowType.SectionHeader, }, { + commandID: "command-4", commandName: "shell.exec", - functionName: "test", + functionID: "function-4", isOpen: true, range: { end: 6, start: 4 }, rowType: RowType.SubsectionHeader, diff --git a/apps/parsley/src/components/LogRow/RowRenderer/index.tsx b/apps/parsley/src/components/LogRow/RowRenderer/index.tsx index a0cde0e9c..490028b7f 100644 --- a/apps/parsley/src/components/LogRow/RowRenderer/index.tsx +++ b/apps/parsley/src/components/LogRow/RowRenderer/index.tsx @@ -31,7 +31,7 @@ const ParsleyRow: RowRendererFunction = ({ processedLogLines }) => { searchState, sectioning, } = useLogContext(); - const { openSection } = sectioning; + const { toggleFunctionSection } = sectioning; const { prettyPrint, wordWrapFormat, wrap } = preferences; const { searchTerm } = searchState; @@ -76,9 +76,10 @@ const ParsleyRow: RowRendererFunction = ({ processedLogLines }) => { if (isSectionHeaderRow(processedLogLine)) { return ( diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx index d39d327ad..adab0149c 100644 --- a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx +++ b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx @@ -40,7 +40,8 @@ const Container = styled.div` `; const sectionHeaderProps = { - onOpen: () => {}, + functionID: "function-4", + onToggle: () => {}, open: true, status: SectionStatus.Pass, }; diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx index 31e61d8a5..393cd8f64 100644 --- a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx +++ b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx @@ -42,12 +42,15 @@ describe("SectionHeader", () => { it("should call onOpen function when 'open' button is clicked", async () => { const user = userEvent.setup(); - const onOpen = vi.fn(); - render(); + const onToggle = vi.fn(); + render(); const openButton = screen.getByDataCy("caret-toggle"); await user.click(openButton); - expect(onOpen).toHaveBeenCalledTimes(1); - expect(onOpen).toHaveBeenCalledWith("load_data", true); + expect(onToggle).toHaveBeenCalledTimes(1); + expect(onToggle).toHaveBeenCalledWith({ + functionID: "function-4", + isOpen: true, + }); }); it("open and close state is controlled by the 'open' prop", async () => { @@ -72,9 +75,10 @@ describe("SectionHeader", () => { }); const sectionHeaderProps = { + functionID: "function-4", functionName: "load_data", lineIndex: 0, - onOpen: vi.fn(), + onToggle: vi.fn(), open: false, status: SectionStatus.Pass, }; diff --git a/apps/parsley/src/components/LogRow/SectionHeader/index.tsx b/apps/parsley/src/components/LogRow/SectionHeader/index.tsx index 48e0d2dc2..b4dbe69db 100644 --- a/apps/parsley/src/components/LogRow/SectionHeader/index.tsx +++ b/apps/parsley/src/components/LogRow/SectionHeader/index.tsx @@ -7,21 +7,23 @@ import { useLogWindowAnalytics } from "analytics"; import { Row } from "components/LogRow/types"; import { SectionStatus } from "constants/logs"; import { size } from "constants/tokens"; -import { OpenSection } from "hooks/useSections"; +import { ToggleFunctionSection } from "hooks/useSections"; import { CaretToggle } from "../CaretToggle"; const { gray } = palette; interface SectionHeaderProps extends Row { functionName: string; - onOpen: OpenSection; + functionID: string; + onToggle: ToggleFunctionSection; open: boolean; status: SectionStatus; } const SectionHeader: React.FC = ({ + functionID, functionName, - onOpen, + onToggle, open, status, }) => { @@ -39,7 +41,7 @@ const SectionHeader: React.FC = ({ sectionName: functionName, sectionType: "function", }); - onOpen(functionName, !open); + onToggle({ functionID, isOpen: !open }); }} open={open} /> diff --git a/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.stories.tsx b/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.stories.tsx index caaafea61..62130dcfa 100644 --- a/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.stories.tsx +++ b/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.stories.tsx @@ -15,7 +15,7 @@ const SubsectionHeaderStory = () => { {...SubsectionHeaderProps} commandName="shell.exec" lineIndex={0} - onOpen={({ isOpen }) => setOpen(isOpen)} + onToggle={({ isOpen }) => setOpen(isOpen)} open={open} /> diff --git a/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.test.tsx b/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.test.tsx index ad49ec5dd..13fdbc787 100644 --- a/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.test.tsx +++ b/apps/parsley/src/components/LogRow/SubsectionHeader/SubsectionHeader.test.tsx @@ -25,12 +25,12 @@ describe("SubsectionHeader", () => { it("should call onOpen function when 'open' button is clicked", async () => { const user = userEvent.setup(); - const onOpen = vi.fn(); - render(); + const onToggle = vi.fn(); + render(); const openButton = screen.getByDataCy("caret-toggle"); await user.click(openButton); - expect(onOpen).toHaveBeenCalledTimes(1); - expect(onOpen).toHaveBeenCalledWith({ + expect(onToggle).toHaveBeenCalledTimes(1); + expect(onToggle).toHaveBeenCalledWith({ commandID: "command-1", functionID: "function-1", isOpen: true, @@ -63,6 +63,6 @@ const sectionHeaderProps = { commandName: "shell.exec", functionID: "function-1", lineIndex: 0, - onOpen: vi.fn(), + onToggle: vi.fn(), open: false, }; diff --git a/apps/parsley/src/components/LogRow/SubsectionHeader/index.tsx b/apps/parsley/src/components/LogRow/SubsectionHeader/index.tsx index f1138b5a4..7fbfc58ac 100644 --- a/apps/parsley/src/components/LogRow/SubsectionHeader/index.tsx +++ b/apps/parsley/src/components/LogRow/SubsectionHeader/index.tsx @@ -4,6 +4,7 @@ import { Body } from "@leafygreen-ui/typography"; import { useLogWindowAnalytics } from "analytics"; import { Row } from "components/LogRow/types"; import { size } from "constants/tokens"; +import { ToggleCommandSection } from "hooks/useSections"; import { CaretToggle } from "../CaretToggle"; const { gray } = palette; @@ -12,11 +13,7 @@ interface SectionHeaderProps extends Row { commandName: string; functionID: string; commandID: string; - onOpen: (v: { - commandID: string; - functionID: string; - isOpen: boolean; - }) => void; + onToggle: ToggleCommandSection; open: boolean; } @@ -24,7 +21,7 @@ const SubsectionHeader: React.FC = ({ commandID, commandName, functionID, - onOpen, + onToggle, open, }) => { const { sendEvent } = useLogWindowAnalytics(); @@ -39,7 +36,7 @@ const SubsectionHeader: React.FC = ({ sectionName: commandName, sectionType: "command", }); - onOpen({ commandID, functionID, isOpen: !open }); + onToggle({ commandID, functionID, isOpen: !open }); }} open={open} /> diff --git a/apps/parsley/src/hooks/useSections/index.ts b/apps/parsley/src/hooks/useSections/index.ts index 2823a9158..d2ce7f74a 100644 --- a/apps/parsley/src/hooks/useSections/index.ts +++ b/apps/parsley/src/hooks/useSections/index.ts @@ -4,14 +4,30 @@ import { useToastContext } from "context/toast"; import { useParsleySettings } from "hooks/useParsleySettings"; import { reportError } from "utils/errorReporting"; import { releaseSectioning } from "utils/featureFlag"; -import { SectionEntry, parseSections } from "./utils"; +import { SectionData, parseSections } from "./utils"; -export type SectionState = { [functionName: string]: { isOpen: boolean } }; -export type OpenSection = (functionName: string, isOpen: boolean) => void; +export type SectionState = { + [functionID: string]: { + isOpen: boolean; + commands: { [commandID: string]: { isOpen: boolean } }; + }; +}; + +export type ToggleCommandSection = (props: { + functionID: string; + commandID: string; + isOpen: boolean; +}) => void; + +export type ToggleFunctionSection = (props: { + functionID: string; + isOpen: boolean; +}) => void; export interface UseSectionsResult { - sectionData: SectionEntry[] | undefined; - openSection: OpenSection; + sectionData: SectionData | undefined; + toggleCommandSection: ToggleCommandSection; + toggleFunctionSection: ToggleFunctionSection; sectionState: SectionState | undefined; sectioningEnabled: boolean; } @@ -27,7 +43,7 @@ export const useSections = ({ renderingType, }: Props): UseSectionsResult => { const dispatchToast = useToastContext(); - const [sectionData, setSectionData] = useState(); + const [sectionData, setSectionData] = useState(); const [sectionState, setSectionState] = useState(); const { settings } = useParsleySettings(); @@ -56,30 +72,53 @@ export const useSections = ({ useEffect(() => { if (sectionData && sectionState === undefined) { - setSectionState(sectionData.reduce(closeAllSectionsReducer, {})); + setSectionState(populateSectionState(sectionData)); } }, [sectionData, sectionState]); - const openSection = useCallback( - (functionName: string, isOpen: boolean) => { - if (sectionState) { - setSectionState((currentState) => ({ - ...currentState, - [functionName]: { isOpen }, - })); - } + const toggleFunctionSection: ToggleFunctionSection = useCallback( + ({ functionID, isOpen }) => { + setSectionState((currentState) => { + if (currentState) { + const nextState = { ...currentState }; + nextState[functionID].isOpen = isOpen; + return nextState; + } + return currentState; + }); + }, + [sectionState], + ); + const toggleCommandSection: ToggleCommandSection = useCallback( + ({ commandID, functionID, isOpen }) => { + setSectionState((currentState) => { + if (currentState) { + const nextState = { ...currentState }; + nextState[functionID].commands[commandID].isOpen = isOpen; + return nextState; + } + return currentState; + }); }, [sectionState], ); return { - openSection, sectionData, sectionState, sectioningEnabled, + toggleCommandSection, + toggleFunctionSection, }; }; -const closeAllSectionsReducer = ( - accum: SectionState, - { functionName }: SectionEntry, -) => ({ ...accum, ...{ [functionName]: { isOpen: false } } }); +const populateSectionState = (sectionData: SectionData): SectionState => { + const { commands, functions } = sectionData; + const sectionState: SectionState = {}; + functions.forEach(({ functionID }) => { + sectionState[functionID] = { commands: {}, isOpen: false }; + }); + commands.forEach(({ commandID, functionID }) => { + sectionState[functionID].commands[commandID] = { isOpen: false }; + }); + return sectionState; +}; diff --git a/apps/parsley/src/hooks/useSections/useSections.test.tsx b/apps/parsley/src/hooks/useSections/useSections.test.tsx index ff5d8a987..fde67885a 100644 --- a/apps/parsley/src/hooks/useSections/useSections.test.tsx +++ b/apps/parsley/src/hooks/useSections/useSections.test.tsx @@ -33,7 +33,10 @@ describe("useSections", () => { expect(sectionUtils.parseSections).toHaveBeenCalledOnce(); }); await waitFor(() => { - expect(result.current.sectionData).toStrictEqual([]); + expect(result.current.sectionData).toStrictEqual({ + commands: [], + functions: [], + }); }); }); @@ -85,9 +88,29 @@ describe("useSections", () => { expect(sectionUtils.parseSections).toHaveBeenCalledOnce(); }); await waitFor(() => { - expect(result.current.sectionData).toStrictEqual([ - { functionName: "f-1", range: { end: 3, start: 1 } }, - ]); + expect(result.current.sectionData).toStrictEqual({ + commands: [ + { + commandID: "command-1", + commandName: "c1", + functionID: "function-1", + range: { + end: 3, + start: 1, + }, + }, + ], + functions: [ + { + functionID: "function-1", + functionName: "f-1", + range: { + end: 3, + start: 1, + }, + }, + ], + }); }); }); @@ -143,7 +166,7 @@ describe("useSections", () => { }); describe("opening and closing sections", () => { - it("openSection function toggles the open state", async () => { + it("toggleFunctionSection function toggles the open state", async () => { RenderFakeToastContext(); const { result } = renderHook(() => useSections({ logs, ...metadata }), { wrapper, @@ -155,52 +178,175 @@ describe("useSections", () => { expect(result.current.sectionState).toStrictEqual(initialSectionState); }); act(() => { - result.current.openSection("f-1", true); + result.current.toggleFunctionSection({ + functionID: "function-1", + isOpen: true, + }); }); await waitFor(() => { expect(result.current.sectionState).toStrictEqual({ ...initialSectionState, - "f-1": { isOpen: true }, + "function-1": { ...initialSectionState["function-1"], isOpen: true }, }); }); act(() => { - result.current.openSection("f-2", true); + result.current.toggleFunctionSection({ + functionID: "function-9", + isOpen: true, + }); }); await waitFor(() => { expect(result.current.sectionState).toStrictEqual({ ...initialSectionState, - "f-1": { isOpen: true }, - "f-2": { isOpen: true }, + "function-1": { ...initialSectionState["function-1"], isOpen: true }, + "function-9": { ...initialSectionState["function-9"], isOpen: true }, }); }); act(() => { - result.current.openSection("f-1", false); + result.current.toggleFunctionSection({ + functionID: "function-1", + isOpen: false, + }); }); await waitFor(() => { expect(result.current.sectionState).toStrictEqual({ ...initialSectionState, - "f-2": { isOpen: true }, + "function-9": { ...initialSectionState["function-9"], isOpen: true }, + }); + }); + }); + + it("toggleCommandSection toggles the open state", async () => { + RenderFakeToastContext(); + const { result } = renderHook(() => useSections({ logs, ...metadata }), { + wrapper, + }); + await waitFor(() => { + expect(result.current.sectionData).toStrictEqual(sectionData); + }); + await waitFor(() => { + expect(result.current.sectionState).toStrictEqual(initialSectionState); + }); + act(() => { + result.current.toggleCommandSection({ + commandID: "command-9", + functionID: "function-9", + isOpen: true, + }); + }); + await waitFor(() => { + expect(result.current.sectionState).toStrictEqual({ + ...initialSectionState, + "function-9": { + commands: { + ...initialSectionState["function-9"].commands, + "command-9": { isOpen: true }, + }, + isOpen: false, + }, }); }); }); const logs = [ "normal log line", "Running command 'c1' in function 'f-1'.", + "normal log line", + "normal log line", + "normal log line", "Finished command 'c1' in function 'f-1'.", - "Running command 'c1' in function 'f-2'.", - "Finished command 'c1' in function 'f-2'.", - "Running command 'c1' in function 'f-3'.", - "Finished command 'c1' in function 'f-3'.", - ]; - const sectionData = [ - { functionName: "f-1", range: { end: 3, start: 1 } }, - { functionName: "f-2", range: { end: 5, start: 3 } }, - { functionName: "f-3", range: { end: 7, start: 5 } }, + "Running command 'c2' in function 'f-1'.", + "Finished command 'c2' in function 'f-1'.", + "normal log line", + "Running command 'c3' in function 'f-2'.", + "normal log line", + "Finished command 'c3' in function 'f-2'.", + "Running command 'c4' in function 'f-2'.", + "Finished command 'c4' in function 'f-2'.", + "normal log line", + "normal log line", + "normal log line", ]; + const sectionData: sectionUtils.SectionData = { + commands: [ + { + commandID: "command-1", + commandName: "c1", + functionID: "function-1", + range: { + end: 6, + start: 1, + }, + }, + { + commandID: "command-6", + commandName: "c2", + functionID: "function-1", + range: { + end: 8, + start: 6, + }, + }, + { + commandID: "command-9", + commandName: "c3", + functionID: "function-9", + range: { + end: 12, + start: 9, + }, + }, + { + commandID: "command-12", + commandName: "c4", + functionID: "function-9", + range: { + end: 14, + start: 12, + }, + }, + ], + functions: [ + { + functionID: "function-1", + functionName: "f-1", + range: { + end: 8, + start: 1, + }, + }, + { + functionID: "function-9", + functionName: "f-2", + range: { + end: 14, + start: 9, + }, + }, + ], + }; const initialSectionState = { - "f-1": { isOpen: false }, - "f-2": { isOpen: false }, - "f-3": { isOpen: false }, + "function-1": { + commands: { + "command-1": { + isOpen: false, + }, + "command-6": { + isOpen: false, + }, + }, + isOpen: false, + }, + "function-9": { + commands: { + "command-9": { + isOpen: false, + }, + "command-12": { + isOpen: false, + }, + }, + isOpen: false, + }, }; }); const metadata = { diff --git a/apps/parsley/src/hooks/useSections/utils.test.ts b/apps/parsley/src/hooks/useSections/utils.test.ts index 4ad4f1af6..822880e6f 100644 --- a/apps/parsley/src/hooks/useSections/utils.test.ts +++ b/apps/parsley/src/hooks/useSections/utils.test.ts @@ -1,24 +1,26 @@ -import { SectionEntry, parseSections, processLine, reduceFn } from "./utils"; +import { SectionData, parseSections, processLine, reduceFn } from "./utils"; describe("processLine", () => { it("should correctly parse a log line indicating a running section", () => { const logLine = "Running command 'ec2.assume_role' in function 'assume-ec2-role' (step 1 of 4) in block 'pre'."; const expectedMetadata = { + commandName: "ec2.assume_role", functionName: "assume-ec2-role", status: "Running", }; - expect(processLine(logLine)).toEqual(expectedMetadata); + expect(processLine(logLine)).toStrictEqual(expectedMetadata); }); it("should correctly parse a log line indicating a finished section", () => { const logLine = "Finished command 'shell.exec' in function 'yarn-preview' (step 6 of 9) in 415.963µs."; const expectedMetadata = { + commandName: "shell.exec", functionName: "yarn-preview", status: "Finished", }; - expect(processLine(logLine)).toEqual(expectedMetadata); + expect(processLine(logLine)).toStrictEqual(expectedMetadata); }); it("should return null for a log line that does not indicate a section", () => { @@ -29,35 +31,75 @@ describe("processLine", () => { describe("reduceFn", () => { it("accumulate section data for starting a section", () => { - const accum: SectionEntry[] = []; + const accum: SectionData = { commands: [], functions: [] }; const line = "Running command 'shell.exec' in function 'yarn-preview'."; const logIndex = 0; - const expectedSections = [ - { - functionName: "yarn-preview", - range: { end: -1, start: 0 }, - }, - ]; - expect(reduceFn(accum, line, logIndex)).toEqual(expectedSections); + expect(reduceFn(accum, line, logIndex)).toEqual({ + commands: [ + { + commandID: "command-0", + commandName: "shell.exec", + functionID: "function-0", + range: { end: -1, start: 0 }, + }, + ], + functions: [ + { + functionID: "function-0", + functionName: "yarn-preview", + range: { end: -1, start: 0 }, + }, + ], + }); }); it("accumulate section data for finishing a section", () => { - const accum: SectionEntry[] = [ - { functionName: "yarn-preview", range: { end: -1, start: 0 } }, - ]; + const accum: SectionData = { + commands: [ + { + commandID: "command-0", + commandName: "shell.exec", + functionID: "function-0", + range: { end: -1, start: 0 }, + }, + ], + functions: [ + { + functionID: "function-0", + functionName: "yarn-preview", + range: { end: -1, start: 0 }, + }, + ], + }; const line = "Finished command 'shell.exec' in function 'yarn-preview' (step 6 of 9) in 415.963µs."; const logIndex = 4; - const expectedSections = [ - { - functionName: "yarn-preview", - range: { end: 5, start: 0 }, - }, - ]; - expect(reduceFn(accum, line, logIndex)).toEqual(expectedSections); + expect(reduceFn(accum, line, logIndex)).toEqual({ + commands: [ + { + commandID: "command-0", + commandName: "shell.exec", + functionID: "function-0", + range: { + end: 5, + start: 0, + }, + }, + ], + functions: [ + { + functionID: "function-0", + functionName: "yarn-preview", + range: { + end: 5, + start: 0, + }, + }, + ], + }); }); it("should throw an error if a finished section appears before a running section", () => { - const accum: SectionEntry[] = []; + const accum: SectionData = { commands: [], functions: [] }; const line = "Finished command 'shell.exec' in function 'yarn-preview' (step 6 of 9) in 415.963µs."; const logIndex = 0; @@ -69,9 +111,23 @@ describe("reduceFn", () => { }); it("should throw an error if a new running section starts without finishing the previous section", () => { - const accum = [ - { functionName: "yarn-preview", range: { end: -1, start: 0 } }, - ]; + const accum = { + commands: [ + { + commandID: "command-0", + commandName: "shell.exec", + functionID: "function-0", + range: { end: -1, start: 0 }, + }, + ], + functions: [ + { + functionID: "function-0", + functionName: "yarn-preview", + range: { end: -1, start: 0 }, + }, + ], + }; const line = "Running command 'attach.xunit_results' in function 'attach-cypress-results' (step 3.3 of 8) in block 'post'."; const logIndex = 1; @@ -102,11 +158,81 @@ describe("parseSections", () => { "normal log line", "normal log line", ]; - const expectedSections = [ - { functionName: "f-1", range: { end: 5, start: 1 } }, - { functionName: "f-2", range: { end: 11, start: 6 } }, - { functionName: "f-3", range: { end: 15, start: 11 } }, - ]; + const expectedSections = { + commands: [ + { + commandID: "command-1", + commandName: "c1", + functionID: "function-1", + range: { + end: 3, + start: 1, + }, + }, + { + commandID: "command-3", + commandName: "c2", + functionID: "function-1", + range: { + end: 5, + start: 3, + }, + }, + { + commandID: "command-6", + commandName: "c3", + functionID: "function-6", + range: { + end: 9, + start: 6, + }, + }, + { + commandID: "command-9", + commandName: "c4", + functionID: "function-6", + range: { + end: 11, + start: 9, + }, + }, + { + commandID: "command-11", + commandName: "c5", + functionID: "function-11", + range: { + end: 15, + start: 11, + }, + }, + ], + functions: [ + { + functionID: "function-1", + functionName: "f-1", + range: { + end: 5, + start: 1, + }, + }, + { + functionID: "function-6", + functionName: "f-2", + range: { + end: 11, + start: 6, + }, + }, + { + functionID: "function-11", + functionName: "f-3", + range: { + end: 15, + start: 11, + }, + }, + ], + }; expect(parseSections(logs)).toEqual(expectedSections); }); @@ -114,6 +240,9 @@ describe("parseSections", () => { const logs = [ "normal log line", "Running command 'c1' in function 'f-1'.", + "normal log line", + "normal log line", + "normal log line", "Finished command 'c1' in function 'f-1'.", "Running command 'c2' in function 'f-1'.", "Finished command 'c2' in function 'f-1'.", @@ -127,11 +256,64 @@ describe("parseSections", () => { "normal log line", "normal log line", ]; - const expectedSections = [ - { functionName: "f-1", range: { end: 5, start: 1 } }, - { functionName: "f-2", range: { end: 11, start: 6 } }, - ]; - expect(parseSections(logs)).toEqual(expectedSections); + expect(parseSections(logs)).toEqual({ + commands: [ + { + commandID: "command-1", + commandName: "c1", + functionID: "function-1", + range: { + end: 6, + start: 1, + }, + }, + { + commandID: "command-6", + commandName: "c2", + functionID: "function-1", + range: { + end: 8, + start: 6, + }, + }, + { + commandID: "command-9", + commandName: "c3", + functionID: "function-9", + range: { + end: 12, + start: 9, + }, + }, + { + commandID: "command-12", + commandName: "c4", + functionID: "function-9", + range: { + end: 14, + start: 12, + }, + }, + ], + functions: [ + { + functionID: "function-1", + functionName: "f-1", + range: { + end: 8, + start: 1, + }, + }, + { + functionID: "function-9", + functionName: "f-2", + range: { + end: 14, + start: 9, + }, + }, + ], + }); }); it("should return an error when there is a finished section without a running section before it", () => { @@ -159,8 +341,8 @@ describe("parseSections", () => { ); }); - it("should return an empty array if the logs array is empty", () => { + it("should return empty arrays if the logs array is empty", () => { const logs: string[] = []; - expect(parseSections(logs)).toEqual([]); + expect(parseSections(logs)).toEqual({ commands: [], functions: [] }); }); }); diff --git a/apps/parsley/src/hooks/useSections/utils.ts b/apps/parsley/src/hooks/useSections/utils.ts index 6dd456ca0..40d7843f0 100644 --- a/apps/parsley/src/hooks/useSections/utils.ts +++ b/apps/parsley/src/hooks/useSections/utils.ts @@ -7,6 +7,7 @@ enum SectionStatus { } interface SectionLineMetadata { + commandName: string; functionName: string; status: SectionStatus; } @@ -17,20 +18,34 @@ interface SectionLineMetadata { * or null if the input line does not indicate the start or end of a section. */ export const processLine = (str: string): SectionLineMetadata | null => { - const regex = /(Running|Finished) command '[^']+' in function '([^']+)'.*/; + const regex = /(Running|Finished) command '([^']+)' in function '([^']+)'.*/; const match = trimSeverity(str).match(regex); if (match) { return { - functionName: match[2], + commandName: match[2], + functionName: match[3], status: match[1] as SectionStatus, }; } return null; }; -interface SectionEntry { - range: Range; +interface FunctionEntry { + functionID: string; functionName: string; + range: Range; +} + +interface CommandEntry { + commandID: string; + commandName: string; + functionID: string; + range: Range; +} + +interface SectionData { + functions: FunctionEntry[]; + commands: CommandEntry[]; } /** @@ -41,51 +56,69 @@ interface SectionEntry { * @returns The updated accumulated section data after processing the current line. */ const reduceFn = ( - accum: SectionEntry[], + accum: SectionData, line: string, logIndex: number, -): SectionEntry[] => { +): SectionData => { const currentLine = processLine(line); // Skip if the current line does not indicate a section if (!currentLine) { return accum; } - const sections = [...accum]; + const { commands, functions } = accum; if (currentLine.status === SectionStatus.Finished) { - if (sections.length === 0) { + if (functions.length === 0 || commands.length === 0) { throw new Error( "Log file is showing a finished section without a running section before it.", ); } // Update the end line number exclusive of the last section in the accumulator - sections[sections.length - 1].range.end = logIndex + 1; - return sections; + functions[functions.length - 1].range.end = logIndex + 1; + commands[commands.length - 1].range.end = logIndex + 1; + return { commands, functions }; } - const isNewSection = - sections.length === 0 || - sections[sections.length - 1].functionName !== currentLine.functionName; /** - * @description - ONGOING_SECTION is used to indicate that the section is still running and has not finished yet in the log file. + * @description - ONGOING_ENTRY is used to indicate that the section or command is still running and has not finished yet in the log file. * The section parsing function will temporarily assign this value until the corresponding finished section or EOF is found. */ - const ONGOING_SECTION = -1; + const ONGOING_ENTRY = -1; + if (currentLine.status === SectionStatus.Running) { + const isNewSection = + functions.length === 0 || + functions[functions.length - 1].functionName !== currentLine.functionName; + if (isNewSection) { + const isPreviousSectionRunning = + functions.length && + functions[functions.length - 1].range.end === ONGOING_ENTRY; - if (currentLine.status === SectionStatus.Running && isNewSection) { - const isPreviousSectionRunning = - sections.length && - sections[sections.length - 1].range.end === ONGOING_SECTION; - if (isPreviousSectionRunning) { + if (isPreviousSectionRunning) { + throw new Error( + "Log file is showing a new running section without finishing the previous section.", + ); + } + functions.push({ + functionID: `function-${logIndex}`, + functionName: currentLine.functionName, + range: { end: ONGOING_ENTRY, start: logIndex }, + }); + } + const isPreviousCommandRunning = + commands.length && + commands[commands.length - 1].range.end === ONGOING_ENTRY; + if (isPreviousCommandRunning) { throw new Error( - "Log file is showing a new running section without finishing the previous section.", + "Log file is showing a new running command without finishing the previous command.", ); } - sections.push({ - functionName: currentLine.functionName, - range: { end: ONGOING_SECTION, start: logIndex }, + commands.push({ + commandID: `command-${logIndex}`, + commandName: currentLine.commandName, + functionID: functions[functions.length - 1].functionID, + range: { end: ONGOING_ENTRY, start: logIndex }, }); } - return sections; + return { commands, functions }; }; /** @@ -93,16 +126,24 @@ const reduceFn = ( * @param logs - The array of log lines to be parsed. * @returns An array of section entries representing the parsed sections. */ -const parseSections = (logs: string[]): SectionEntry[] => { - const result = logs.reduce(reduceFn, [] as SectionEntry[]); +const parseSections = (logs: string[]): SectionData => { + const { commands, functions } = logs.reduce(reduceFn, { + commands: [], + functions: [], + } as SectionData); const lastSectionIsRunning = - result.length && result[result.length - 1].range.end === -1; + functions.length && functions[functions.length - 1].range.end === -1; // Close the last section if it is still running if (lastSectionIsRunning) { - result[result.length - 1].range.end = logs.length; + functions[functions.length - 1].range.end = logs.length; + } + const lastCommandIsRunning = + commands.length && commands[commands.length - 1].range.end === -1; + if (lastCommandIsRunning) { + commands[commands.length - 1].range.end = logs.length; } - return result; + return { commands, functions }; }; export { parseSections, reduceFn }; -export type { SectionEntry }; +export type { FunctionEntry, SectionData, CommandEntry }; diff --git a/apps/parsley/src/types/logs.ts b/apps/parsley/src/types/logs.ts index 8cb8012a0..2e6f9e549 100644 --- a/apps/parsley/src/types/logs.ts +++ b/apps/parsley/src/types/logs.ts @@ -21,18 +21,20 @@ interface SkippedLinesRow { } interface SectionHeaderRow { - rowType: RowType.SectionHeader; + functionID: string; functionName: string; - range: Range; isOpen: boolean; + range: Range; + rowType: RowType.SectionHeader; } interface SubsectionHeaderRow { - rowType: RowType.SubsectionHeader; - functionName: string; + commandID: string; commandName: string; - range: Range; + functionID: string; isOpen: boolean; + range: Range; + rowType: RowType.SubsectionHeader; } type ProcessedLogLine = diff --git a/apps/parsley/src/utils/filterLogs/filterLogs.test.ts b/apps/parsley/src/utils/filterLogs/filterLogs.test.ts index 95300001c..7a82aa95a 100644 --- a/apps/parsley/src/utils/filterLogs/filterLogs.test.ts +++ b/apps/parsley/src/utils/filterLogs/filterLogs.test.ts @@ -159,7 +159,10 @@ describe("filterLogs", () => { logLines: logsWithSections, matchingLines: undefined, sectionData, - sectionState: { "f-1": { isOpen: true }, "f-2": { isOpen: true } }, + sectionState: { + "function-1": { commands: {}, isOpen: true }, + "function-6": { commands: {}, isOpen: true }, + }, sectioningEnabled: true, shareLine: undefined, }), @@ -176,7 +179,10 @@ describe("filterLogs", () => { logLines: logsWithSections, matchingLines: undefined, sectionData, - sectionState: { "f-1": { isOpen: false }, "f-2": { isOpen: true } }, + sectionState: { + "function-1": { commands: {}, isOpen: false }, + "function-6": { commands: {}, isOpen: true }, + }, sectioningEnabled: true, shareLine: undefined, }), @@ -193,7 +199,10 @@ describe("filterLogs", () => { logLines: logsWithSections, matchingLines: undefined, sectionData, - sectionState: { "f-1": { isOpen: false }, "f-2": { isOpen: false } }, + sectionState: { + "f-1": { commands: {}, isOpen: false }, + "f-2": { commands: {}, isOpen: false }, + }, sectioningEnabled: true, shareLine: undefined, }), @@ -255,14 +264,26 @@ const logsWithSections = [ "normal log line", ]; -const sectionData = [ - { functionName: "f-1", range: { end: 5, start: 1 } }, - { functionName: "f-2", range: { end: 11, start: 6 } }, -]; +const sectionData = { + commands: [], + functions: [ + { + functionID: "function-1", + functionName: "f-1", + range: { end: 5, start: 1 }, + }, + { + functionID: "function-6", + functionName: "f-2", + range: { end: 11, start: 6 }, + }, + ], +}; const allSectionsOpen = [ 0, { + functionID: "function-1", functionName: "f-1", isOpen: true, range: { @@ -277,6 +298,7 @@ const allSectionsOpen = [ 4, 5, { + functionID: "function-6", functionName: "f-2", isOpen: true, range: { @@ -297,6 +319,7 @@ const allSectionsOpen = [ const someSectionsOpen = [ 0, { + functionID: "function-1", functionName: "f-1", isOpen: false, range: { @@ -307,6 +330,7 @@ const someSectionsOpen = [ }, 5, { + functionID: "function-6", functionName: "f-2", isOpen: true, range: { @@ -328,6 +352,7 @@ const someSectionsOpen = [ const allSectionsClosed = [ 0, { + functionID: "function-1", functionName: "f-1", isOpen: false, range: { @@ -338,6 +363,7 @@ const allSectionsClosed = [ }, 5, { + functionID: "function-6", functionName: "f-2", isOpen: false, range: { diff --git a/apps/parsley/src/utils/filterLogs/index.ts b/apps/parsley/src/utils/filterLogs/index.ts index f68df7f6e..332d2a2c4 100644 --- a/apps/parsley/src/utils/filterLogs/index.ts +++ b/apps/parsley/src/utils/filterLogs/index.ts @@ -1,5 +1,5 @@ import { SectionState } from "hooks/useSections"; -import { SectionEntry } from "hooks/useSections/utils"; +import { SectionData } from "hooks/useSections/utils"; import { ExpandedLines, ProcessedLogLines, RowType } from "types/logs"; import { isExpanded } from "utils/expandedLines"; import { newSkippedLinesRow } from "utils/logRow"; @@ -13,7 +13,7 @@ type FilterLogsParams = { expandedLines: ExpandedLines; expandableRows: boolean; failingLine: number | undefined; - sectionData: SectionEntry[] | undefined; + sectionData: SectionData | undefined; sectioningEnabled: boolean; sectionState: SectionState | undefined; }; @@ -49,25 +49,24 @@ const filterLogs = (options: FilterLogsParams): ProcessedLogLines => { } = options; // If there are no filters or expandable rows is not enabled, then we only have to process sections if they exist and are enabled. if (matchingLines === undefined) { - if (sectioningEnabled && sectionData?.length) { + if (sectioningEnabled && sectionData?.functions.length) { const filteredLines: ProcessedLogLines = []; let sectionIndex = 0; for (let idx = 0; idx < logLines.length; idx++) { - const section = sectionData[sectionIndex]; - const isSectionStart = section && idx === section.range.start; + const func = sectionData.functions[sectionIndex]; + const isSectionStart = func && idx === func.range.start; if (isSectionStart && sectionState) { - const isOpen = sectionState[section.functionName]?.isOpen ?? false; + const isOpen = sectionState[func.functionID]?.isOpen ?? false; filteredLines.push({ - functionName: section.functionName, + ...func, isOpen, - range: section.range, rowType: RowType.SectionHeader, }); sectionIndex += 1; if (isOpen) { filteredLines.push(idx); } else { - idx = section.range.end - 1; + idx = func.range.end - 1; } } else { filteredLines.push(idx); diff --git a/apps/parsley/src/utils/findLineIndex/findLineIndex.test.ts b/apps/parsley/src/utils/findLineIndex/findLineIndex.test.ts index 8a37d35f2..27f5352c7 100644 --- a/apps/parsley/src/utils/findLineIndex/findLineIndex.test.ts +++ b/apps/parsley/src/utils/findLineIndex/findLineIndex.test.ts @@ -9,39 +9,45 @@ const processedLines: ProcessedLogLines = [ 6, { range: { end: 10, start: 7 }, rowType: RowType.SkippedLines }, { + functionID: "function-10", functionName: "f-1", isOpen: true, range: { end: 11, start: 10 }, rowType: RowType.SectionHeader, }, { + commandID: "command-10", commandName: "shell.exec", - functionName: "f-1", + functionID: "function-10", isOpen: false, range: { end: 11, start: 10 }, rowType: RowType.SubsectionHeader, }, 12, { + functionID: "function-13", functionName: "f-2", isOpen: false, range: { end: 15, start: 13 }, rowType: RowType.SectionHeader, }, { + functionID: "function-15", functionName: "f-3", isOpen: true, range: { end: 17, start: 15 }, rowType: RowType.SectionHeader, }, { + commandID: "command-15", commandName: "shell.exec", - functionName: "f-3", + functionID: "function-15", isOpen: false, range: { end: 17, start: 15 }, rowType: RowType.SubsectionHeader, }, { + functionID: "function-17", functionName: "f-4", isOpen: true, range: { end: 19, start: 17 }, @@ -50,14 +56,16 @@ const processedLines: ProcessedLogLines = [ 17, 18, { + functionID: "function-19", functionName: "f-5", isOpen: true, range: { end: 23, start: 19 }, rowType: RowType.SectionHeader, }, { + commandID: "command-19", commandName: "shell.exec", - functionName: "f-5", + functionID: "function-19", isOpen: true, range: { end: 23, start: 19 }, rowType: RowType.SubsectionHeader, diff --git a/apps/parsley/src/utils/searchLogs/searchLogs.test.ts b/apps/parsley/src/utils/searchLogs/searchLogs.test.ts index b88cc9daf..c935d8d5d 100644 --- a/apps/parsley/src/utils/searchLogs/searchLogs.test.ts +++ b/apps/parsley/src/utils/searchLogs/searchLogs.test.ts @@ -81,6 +81,7 @@ describe("searchLogs", () => { const processedLogLines: ProcessedLogLines = [ 0, { + functionID: "function-1", functionName: "test", isOpen: true, range: { end: 2, start: 1 },