diff --git a/cyclops-ctrl/internal/handler/handler.go b/cyclops-ctrl/internal/handler/handler.go index eb5d6cea..accfa4d2 100644 --- a/cyclops-ctrl/internal/handler/handler.go +++ b/cyclops-ctrl/internal/handler/handler.go @@ -1,10 +1,11 @@ package handler import ( + "net/http" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/controller/sse" "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/integrations/helm" "github.com/gin-gonic/gin" - "net/http" "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/controller" "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/prometheus" @@ -96,6 +97,8 @@ func (h *Handler) Start() error { h.router.GET("/resources/pods/:namespace/:name/:container/logs", modulesController.GetLogs) h.router.GET("/resources/pods/:namespace/:name/:container/logs/stream", sse.HeadersMiddleware(), modulesController.GetLogsStream) h.router.GET("/resources/pods/:namespace/:name/:container/logs/download", modulesController.DownloadLogs) + h.router.GET("/resources/deployments/:namespace/:deployment/:container/logs", modulesController.GetDeploymentLogs) + h.router.GET("/resources/statefulsets/:namespace/:name/:container/logs", modulesController.GetStatefulSetsLogs) h.router.GET("/manifest", modulesController.GetManifest) h.router.GET("/resources", modulesController.GetResource) diff --git a/cyclops-ui/src/components/k8s-resources/Deployment.tsx b/cyclops-ui/src/components/k8s-resources/Deployment.tsx index e2c44813..93744e34 100644 --- a/cyclops-ui/src/components/k8s-resources/Deployment.tsx +++ b/cyclops-ui/src/components/k8s-resources/Deployment.tsx @@ -1,8 +1,21 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { Col, Divider, Row, Alert, Spin } from "antd"; +import { useCallback, useEffect, useState, useRef } from "react"; +import { + Col, + Divider, + Row, + Alert, + Spin, + TabsProps, + Button, + Tabs, + Modal, +} from "antd"; import { mapResponseError } from "../../utils/api/errors"; import PodTable from "./common/PodTable/PodTable"; import { isStreamingEnabled } from "../../utils/api/common"; +import { logStream } from "../../utils/api/sse/logs"; +import ReactAce from "react-ace/lib/ace"; +import { DownloadOutlined, ReadOutlined } from "@ant-design/icons"; import { useResourceListActions } from "./ResourceList/ResourceListActionsContext"; interface Props { @@ -11,7 +24,7 @@ interface Props { workload: any; } -const Deployment = ({ name, namespace, workload }: Props) => { +export const Deployment = ({ name, namespace, workload }: Props) => { const { fetchResource, streamingDisabled } = useResourceListActions(); const [loading, setLoading] = useState(true); @@ -107,4 +120,260 @@ const Deployment = ({ name, namespace, workload }: Props) => { ); }; -export default Deployment; +export const DeploymentLogsButton = ({ name, namespace, workload }: Props) => { + const { streamingDisabled, getPodLogs, downloadPodLogs, streamPodLogs } = + useResourceListActions(); + const [logs, setLogs] = useState([]); + const [logsModal, setLogsModal] = useState({ + on: false, + containers: [], + initContainers: [], + }); + + const logsSignalControllerRef = useRef(null); + + const [error, setError] = useState({ + message: "", + description: "", + }); + + const handleCancelLogs = () => { + setLogsModal({ + on: false, + containers: [], + initContainers: [], + }); + setLogs([]); + + // send the abort signal + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + }; + + const getTabItems = () => { + let items: TabsProps["items"] = []; + + let container: any; + + if (logsModal.containers !== null) { + for (container of logsModal.containers) { + items.push({ + key: container.name, + label: container.name, + children: ( + + {downloadPodLogs ? ( +
+ + +
+ ) : ( + <> + )} + + + ), + }); + } + } + + if (logsModal.initContainers !== null) { + for (container of logsModal.initContainers) { + items.push({ + key: container.name, + label: "(init container) " + container.name, + children: ( + + {downloadPodLogs ? ( +
+ + +
+ ) : ( + <> + )} + + + ), + }); + } + } + + return items; + }; + + const onLogsTabsChange = () => { + const controller = new AbortController(); + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + logsSignalControllerRef.current = controller; // store the controller to be able to abort the request + setLogs(() => []); + + if (!streamingDisabled) { + logStream( + namespace, + name, + workload.pods[0].containers[0].name, + (log, isReset = false) => { + if (isReset) { + setLogs(() => []); + } else { + setLogs((prevLogs) => { + return [...prevLogs, log]; + }); + } + }, + (err, isReset = false) => { + if (isReset) { + setError({ + message: "", + description: "", + }); + } else { + setError(mapResponseError(err)); + } + }, + controller, + streamPodLogs, + ); + } else { + getPodLogs(namespace, name, workload.pods[0].containers[0].name) + .then((res) => { + if (res) { + setLogs(res); + } else { + setLogs(() => []); + } + }) + .catch((error) => { + setError(mapResponseError(error)); + }); + } + }; + + const downloadLogs = (container: string) => { + return () => downloadPodLogs(namespace, workload.pods[0].name, container); + }; + + return ( + <> + + + {error.message.length !== 0 && ( + { + setError({ + message: "", + description: "", + }); + }} + style={{ marginBottom: "20px" }} + /> + )} + + + + ); +}; diff --git a/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx b/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx index dddafaa9..d77622ca 100644 --- a/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx +++ b/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx @@ -19,11 +19,11 @@ import { ResourceRef, resourceRefKey, } from "../../../utils/resourceRef"; -import Deployment from "../Deployment"; +import { Deployment, DeploymentLogsButton } from "../Deployment"; import CronJob from "../CronJob"; import Job from "../Job"; import DaemonSet from "../DaemonSet"; -import StatefulSet from "../StatefulSet"; +import { StatefulSet, StatefulSetLogsButton } from "../StatefulSet"; import Pod from "../Pod"; import Service from "../Service"; import ClusterRole from "../ClusterRole"; @@ -574,6 +574,24 @@ const ResourceList = ({ /> )} + {resource.kind === "Deployment" && ( + + + + )} + {resource.kind === "StatefulSet" && ( + + + + )} {resourceDetails} , diff --git a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx index 3cf38ce5..65a5c8ce 100644 --- a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx +++ b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx @@ -1,8 +1,21 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { Col, Divider, Row, Alert, Spin } from "antd"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + Col, + Divider, + Row, + Alert, + Spin, + TabsProps, + Button, + Modal, + Tabs, +} from "antd"; import { mapResponseError } from "../../utils/api/errors"; import PodTable from "./common/PodTable/PodTable"; import { isStreamingEnabled } from "../../utils/api/common"; +import ReactAce from "react-ace/lib/ace"; +import { logStream } from "../../utils/api/sse/logs"; +import { DownloadOutlined, ReadOutlined } from "@ant-design/icons"; import { useResourceListActions } from "./ResourceList/ResourceListActionsContext"; interface Props { @@ -11,15 +24,15 @@ interface Props { workload: any; } -const StatefulSet = ({ name, namespace, workload }: Props) => { +export const StatefulSet = ({ name, namespace, workload }: Props) => { const { fetchResource, streamingDisabled } = useResourceListActions(); const [loading, setLoading] = useState(true); + const [statefulSet, setStatefulSet] = useState({ status: "", pods: [], }); - const [error, setError] = useState({ message: "", description: "", @@ -107,4 +120,259 @@ const StatefulSet = ({ name, namespace, workload }: Props) => { ); }; -export default StatefulSet; +export const StatefulSetLogsButton = ({ name, namespace, workload }: Props) => { + const { streamingDisabled, getPodLogs, downloadPodLogs, streamPodLogs } = + useResourceListActions(); + const [logs, setLogs] = useState([]); + const [logsModal, setLogsModal] = useState({ + on: false, + containers: [], + initContainers: [], + }); + + const logsSignalControllerRef = useRef(null); + + const [error, setError] = useState({ + message: "", + description: "", + }); + + const handleCancelLogs = () => { + setLogsModal({ + on: false, + containers: [], + initContainers: [], + }); + setLogs([]); + + // send the abort signal + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + }; + + const getTabItems = () => { + let items: TabsProps["items"] = []; + + let container: any; + + if (logsModal.containers !== null) { + for (container of logsModal.containers) { + items.push({ + key: container.name, + label: container.name, + children: ( + + {downloadPodLogs ? ( +
+ + +
+ ) : ( + <> + )} + + + ), + }); + } + } + + if (logsModal.initContainers !== null) { + for (container of logsModal.initContainers) { + items.push({ + key: container.name, + label: "(init container) " + container.name, + children: ( + + {downloadPodLogs ? ( +
+ + +
+ ) : ( + <> + )} + + + ), + }); + } + } + + return items; + }; + + const onLogsTabsChange = () => { + const controller = new AbortController(); + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + logsSignalControllerRef.current = controller; // store the controller to be able to abort the request + setLogs(() => []); + + if (!streamingDisabled) { + logStream( + namespace, + name, + workload.pods[0].containers[0].name, + (log, isReset = false) => { + if (isReset) { + setLogs(() => []); + } else { + setLogs((prevLogs) => { + return [...prevLogs, log]; + }); + } + }, + (err, isReset = false) => { + if (isReset) { + setError({ + message: "", + description: "", + }); + } else { + setError(mapResponseError(err)); + } + }, + controller, + streamPodLogs, + ); + } else { + getPodLogs(namespace, name, workload.pods[0].containers[0].name) + .then((res) => { + if (res) { + setLogs(res); + } else { + setLogs(() => []); + } + }) + .catch((error) => { + setError(mapResponseError(error)); + }); + } + }; + + const downloadLogs = (container: string) => { + return () => downloadPodLogs(namespace, workload.pods[0].name, container); + }; + + return ( + <> + + + {error.message.length !== 0 && ( + { + setError({ + message: "", + description: "", + }); + }} + style={{ marginBottom: "20px" }} + /> + )} + + + + ); +};