diff --git a/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.test.tsx b/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.test.tsx index ff00f1cab3..91954cc3d5 100644 --- a/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.test.tsx +++ b/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.test.tsx @@ -1,15 +1,64 @@ import { fireEvent, render, screen } from "@testing-library/react"; +import { act } from "react-test-renderer"; +import { TextEncoder, TextDecoder } from "util"; import { PodLogs } from "./index"; +Object.assign(global, { TextDecoder, TextEncoder }); + describe("PodLogs", () => { + let originFetch: any; + beforeEach(() => { + originFetch = (global as any).fetch; + }); + afterEach(() => { + (global as any).fetch = originFetch; + }); + it("Load PodLogs screen", async () => { - const { container } = render( - - ); + const mRes = { + body: new ReadableStream({ + start(controller) { + controller.enqueue( + Buffer.from( + `{"level":"info","ts":"2023-09-04T11:50:19.712416709Z","logger":"numaflow.Source-processor","caller":"publish/publisher.go:180","msg":"Skip publishing the new watermark because it's older than the current watermark","pipeline":"simple-pipeline","vertex":"in","entityID":"simple-pipeline-in-0","otStore":"default-simple-pipeline-in-cat_OT","hbStore":"default-simple-pipeline-in-cat_PROCESSORS","toVertexPartitionIdx":0,"entity":"simple-pipeline-in-0","head":1693828217394,"new":-1}` + ) + ); + controller.enqueue( + Buffer.from( + `{"level":"error","ts":"2023-09-04T11:50:19.712416709Z","logger":"numaflow.Source-processor","caller":"publish/publisher.go:180","msg":"Skip publishing the new watermark because it's older than the current watermark","pipeline":"simple-pipeline","vertex":"in","entityID":"simple-pipeline-in-0","otStore":"default-simple-pipeline-in-cat_OT","hbStore":"default-simple-pipeline-in-cat_PROCESSORS","toVertexPartitionIdx":0,"entity":"simple-pipeline-in-0","head":1693828217394,"new":-1}` + ) + ); + controller.enqueue( + Buffer.from( + `{"level":"warn","ts":"2023-09-04T11:50:19.712416709Z","logger":"numaflow.Source-processor","caller":"publish/publisher.go:180","msg":"Skip publishing the new watermark because it's older than the current watermark","pipeline":"simple-pipeline","vertex":"in","entityID":"simple-pipeline-in-0","otStore":"default-simple-pipeline-in-cat_OT","hbStore":"default-simple-pipeline-in-cat_PROCESSORS","toVertexPartitionIdx":0,"entity":"simple-pipeline-in-0","head":1693828217394,"new":-1}` + ) + ); + controller.enqueue( + Buffer.from( + `{"level":"debug","ts":"2023-09-04T11:50:19.712416709Z","logger":"numaflow.Source-processor","caller":"publish/publisher.go:180","msg":"Skip publishing the new watermark because it's older than the current watermark","pipeline":"simple-pipeline","vertex":"in","entityID":"simple-pipeline-in-0","otStore":"default-simple-pipeline-in-cat_OT","hbStore":"default-simple-pipeline-in-cat_PROCESSORS","toVertexPartitionIdx":0,"entity":"simple-pipeline-in-0","head":1693828217394,"new":-1}` + ) + ); + controller.close(); + }, + }), + ok: true, + }; + const mockedFetch = jest.fn().mockResolvedValue(mRes as any); + (global as any).fetch = mockedFetch; + let container; + await act(async () => { + const { container: cont } = render( + + ); + container = cont; + }); + + expect(mockedFetch).toBeCalledTimes(1); + //search for logs fireEvent.change( container.getElementsByClassName( @@ -17,6 +66,14 @@ describe("PodLogs", () => { )[0], { target: { value: "load" } } ); + //search for logs not present + fireEvent.change( + container.getElementsByClassName( + "MuiInputBase-input css-yz9k0d-MuiInputBase-input" + )[0], + { target: { value: "xyz" } } + ); + expect(screen.getByText("No logs matching search.")).toBeVisible(); //negate logs search fireEvent.click( container.getElementsByClassName( @@ -29,9 +86,41 @@ describe("PodLogs", () => { fireEvent.click(screen.getByTestId("clear-button")); //pause logs expect(screen.getByTestId("pause-button")).toBeVisible(); - //pause - fireEvent.click(screen.getByTestId("pause-button")); - //unpause - fireEvent.click(screen.getByTestId("pause-button")); + act(() => { + fireEvent.click(screen.getByTestId("pause-button")); + //play logs + fireEvent.click(screen.getByTestId("pause-button")); + }); + //toggle theme + expect(screen.getByTestId("color-mode-button")).toBeVisible(); + fireEvent.click(screen.getByTestId("color-mode-button")); + //toggle logs order + expect(screen.getByTestId("order-button")).toBeVisible(); + fireEvent.click(screen.getByTestId("order-button")); + }); + + it("Trigger PodLogs parsing error", async () => { + const mRes = { + body: new ReadableStream({ + start(controller) { + controller.enqueue(Buffer.from("something")); + controller.close(); + }, + }), + ok: true, + }; + const mockedFetch = jest.fn().mockResolvedValueOnce(mRes as any); + (global as any).fetch = mockedFetch; + await act(async () => { + render( + + ); + }); + + expect(mockedFetch).toBeCalledTimes(1); }); }); diff --git a/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.tsx b/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.tsx index 98fc1822aa..04f1ef1988 100644 --- a/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.tsx +++ b/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/index.tsx @@ -6,6 +6,11 @@ import IconButton from "@mui/material/IconButton"; import ClearIcon from "@mui/icons-material/Clear"; import PauseIcon from "@mui/icons-material/Pause"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import ArrowUpward from "@mui/icons-material/ArrowUpward"; +import ArrowDownward from "@mui/icons-material/ArrowDownward"; +import LightMode from "@mui/icons-material/LightMode"; +import DarkMode from "@mui/icons-material/DarkMode"; +import Tooltip from "@mui/material/Tooltip"; import FormControlLabel from "@mui/material/FormControlLabel"; import Checkbox from "@mui/material/Checkbox"; import Highlighter from "react-highlight-words"; @@ -23,9 +28,6 @@ const parsePodLogs = (value: string): string[] => { try { const obj = JSON.parse(raw); let msg = ``; - if (obj?.level) { - msg = `${msg}${obj.level.toUpperCase()} `; - } if (obj?.ts) { const date = obj.ts.split(/[-T:.Z]/); const ds = @@ -40,7 +42,10 @@ const parsePodLogs = (value: string): string[] => { date[4] + ":" + date[5]; - msg = `${msg}${ds} `; + msg = `${msg}${ds} | `; + } + if (obj?.level) { + msg = `${msg}${obj.level.toUpperCase()} | `; } msg = `${msg}${raw}`; return msg; @@ -50,6 +55,19 @@ const parsePodLogs = (value: string): string[] => { }); }; +const logColor = (log: string, colorMode: string): string => { + if (log.startsWith("ERROR", 22)) { + return "#B80000"; + } + if (log.startsWith("WARN", 22)) { + return "#FFAD00"; + } + if (log.startsWith("DEBUG", 22)) { + return "#81b8ef"; + } + return colorMode === "light" ? "black" : "white"; +}; + export function PodLogs({ namespaceId, podName, containerName }: PodLogsProps) { const [logs, setLogs] = useState([]); const [filteredLogs, setFilteredLogs] = useState([]); @@ -60,6 +78,8 @@ export function PodLogs({ namespaceId, podName, containerName }: PodLogsProps) { const [search, setSearch] = useState(""); const [negateSearch, setNegateSearch] = useState(false); const [paused, setPaused] = useState(false); + const [colorMode, setColorMode] = useState("light"); + const [logsOrder, setLogsOrder] = useState("asc"); useEffect(() => { // reset logs in memory on any log source change @@ -102,10 +122,10 @@ export function PodLogs({ namespaceId, podName, containerName }: PodLogsProps) { } if (value) { setLogs((logs) => { - const latestLogs = parsePodLogs(value).reverse(); - let updated = [...latestLogs, ...logs]; + const latestLogs = parsePodLogs(value); + let updated = [...logs, ...latestLogs]; if (updated.length > MAX_LOGS) { - updated = updated.slice(0, MAX_LOGS); + updated = updated.slice(updated.length - MAX_LOGS); } return updated; }); @@ -160,6 +180,14 @@ export function PodLogs({ namespaceId, podName, containerName }: PodLogsProps) { } }, [paused, reader]); + const handleColorMode = useCallback(() => { + setColorMode(colorMode === "light" ? "dark" : "light"); + }, [colorMode]); + + const handleOrder = useCallback(() => { + setLogsOrder(logsOrder === "asc" ? "desc" : "asc"); + }, [logsOrder]); + return ( @@ -193,14 +221,50 @@ export function PodLogs({ namespaceId, podName, containerName }: PodLogsProps) { } label="Negate search" /> - - {paused ? : } - + + {paused ? "Play" : "Pause"} logs + + } + placement={"top"} + arrow + > + + {paused ? : } + + + + {colorMode === "light" ? "Dark" : "Light"} mode + + } + placement={"top"} + arrow + > + + {colorMode === "light" ? : } + + + + {logsOrder === "asc" ? "Descending" : "Ascending"} order + + } + placement={"top"} + arrow + > + + {logsOrder === "asc" ? : } + + - {filteredLogs.map((l: string, idx) => ( - - - - ))} + {logsOrder === "asc" && + filteredLogs.map((l: string, idx) => ( + + + + ))} + {logsOrder === "desc" && + filteredLogs + .slice() + .reverse() + .map((l: string, idx) => ( + + + + ))} ); diff --git a/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/style.css b/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/style.css index 4aa8d7f612..37341b7b89 100644 --- a/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/style.css +++ b/ui/src/components/pages/Pipeline/partials/Graph/partials/NodeInfo/partials/Pods/partials/PodDetails/partials/PodLogs/style.css @@ -1,3 +1,7 @@ .PodLogs-search { margin-right: 1rem; } + +.icon-tooltip { + font-size: 0.8125rem; +}