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;
+}