From e341e393663e96a6a9519c0367a723617f1aa219 Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Mon, 3 Jun 2024 10:16:30 +0000 Subject: [PATCH 1/3] sync commit,laf b2f01152f95def1f3dcbf0dfca66a88bb8412fa9 829c5eb861de84d008dc94a801f673f58841da0b 6e5da1a608b6cc718683a43cb6a9e43f1e9226fa --- .gitignore | 1 + .vscode/settings.json | 8 +- server/src/application/pod.service.ts | 37 ++++ server/src/log/log.controller.ts | 102 +++++++--- web/package-lock.json | 39 +++- web/package.json | 1 + web/src/components/Editor/TSEditor.tsx | 26 ++- web/src/layouts/Function.tsx | 16 +- .../app/mods/StatusBar/LogsModal/index.scss | 14 ++ .../app/mods/StatusBar/LogsModal/index.tsx | 185 +++++++++++------- .../app/mods/StatusBar/LogsModal/initLog.scss | 45 +++++ .../app/mods/StatusBar/LogsModal/initLog.tsx | 155 +++++++++++++++ .../setting/SysSetting/AppInfoList/index.tsx | 10 +- 13 files changed, 512 insertions(+), 127 deletions(-) create mode 100644 web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss create mode 100644 web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx diff --git a/.gitignore b/.gitignore index 86ea7bc..f2877f4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ update-changelog.sh runtimes/nodejs-esm yarn.lock deploy/logs/sealos.log +deploy/registry diff --git a/.vscode/settings.json b/.vscode/settings.json index 084d436..0ec8d0c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -149,5 +149,11 @@ "i18n-ally.keysInUse": [ "description.part2_whatever" ], - "jest.rootPath": "e2e" + "jest.rootPath": "e2e", + "[typescriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, } \ No newline at end of file diff --git a/server/src/application/pod.service.ts b/server/src/application/pod.service.ts index 0faf835..f81d385 100644 --- a/server/src/application/pod.service.ts +++ b/server/src/application/pod.service.ts @@ -6,6 +6,15 @@ import { PodNameListDto, ContainerNameListDto } from './dto/pod.dto' import { LABEL_KEY_APP_ID } from 'src/constants' import { UserWithKubeconfig } from 'src/user/entities/user' +export type PodStatus = { + appid: string + podStatus: { + name: string + podStatus: string + initContainerId?: string + }[] +} + @Injectable() export class PodService { private readonly logger = new Logger(PodService.name) @@ -48,4 +57,32 @@ export class PodService { return containerNames } + + async getPodStatusListByAppid( + user: UserWithKubeconfig, + appid: string, + ): Promise { + const coreV1Api = this.cluster.makeCoreV1Api(user) + const res: { response: http.IncomingMessage; body: V1PodList } = + await coreV1Api.listNamespacedPod( + user.namespace, + undefined, + undefined, + undefined, + undefined, + `${LABEL_KEY_APP_ID}=${appid}`, + ) + const podStatus: PodStatus = { + appid: appid, + podStatus: [], + } + for (const item of res.body.items) { + podStatus.podStatus.push({ + name: item.metadata.name, + podStatus: item.status.phase, + initContainerId: item.status.initContainerStatuses[0]?.containerID, + }) + } + return podStatus + } } diff --git a/server/src/log/log.controller.ts b/server/src/log/log.controller.ts index a3098de..d9dae2c 100644 --- a/server/src/log/log.controller.ts +++ b/server/src/log/log.controller.ts @@ -5,15 +5,14 @@ import { Query, UseGuards, Sse, + MessageEvent, } from '@nestjs/common' import http from 'http' import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' -import { FunctionService } from '../function/function.service' import { JwtAuthGuard } from 'src/authentication/jwt.auth.guard' import { ApplicationAuthGuard } from 'src/authentication/application.auth.guard' import { PassThrough } from 'nodemailer/lib/xoauth2' import { Log } from '@kubernetes/client-node' -import { RegionService } from 'src/region/region.service' import { ClusterService } from 'src/region/cluster/cluster.service' import { Observable } from 'rxjs' import { PodService } from 'src/application/pod.service' @@ -44,23 +43,36 @@ export class LogController { containerName = appid } - let podNameList: string[] = ( - await this.podService.getPodNameListByAppid(user, appid) - ).podNameList + const podStatus = await this.podService.getPodStatusListByAppid(user, appid) - if (!podNameList.includes(podName) && podName !== 'all') { + if (!podStatus.podStatus[0]) { return new Observable((subscriber) => { - subscriber.next( - JSON.stringify({ - error: 'podName not exist', - }) as unknown as MessageEvent, - ) - subscriber.complete() + subscriber.error(new Error('pod not exist')) }) } + const podNameList = podStatus.podStatus.map((pod) => pod.name) + + const initContainerId = podStatus.podStatus.map( + (pod) => pod.initContainerId, + ) + + if (containerName === 'init') { + for (const containerId of initContainerId) { + if (!containerId) { + return new Observable((subscriber) => { + subscriber.error(new Error('init container not exist')) + }) + } + } + } + if (podName !== 'all') { - podNameList = undefined + if (!podNameList.includes(podName)) { + return new Observable((subscriber) => { + subscriber.error(new Error('podName not exist')) + }) + } } const kc = this.clusterService.loadKubeConfig(user) @@ -70,14 +82,34 @@ export class LogController { const logs = new Log(kc) const streamsEnded = new Set() + const k8sLogResponses: http.IncomingMessage[] = [] + const podLogStreams: PassThrough[] = [] const destroyStream = () => { - combinedLogStream?.removeAllListeners() - combinedLogStream?.destroy() + combinedLogStream.removeAllListeners() + combinedLogStream.destroy() + + k8sLogResponses.forEach((response) => { + response.removeAllListeners() + response.destroy() + }) + + podLogStreams.forEach((stream) => { + stream.removeAllListeners() + stream.destroy() + }) } + let idCounter = 1 combinedLogStream.on('data', (chunk) => { - subscriber.next(chunk.toString() as MessageEvent) + const dataString = chunk.toString() + const messageEvent: MessageEvent = { + id: idCounter.toString(), + data: dataString, + type: 'log', + } + idCounter++ + subscriber.next(messageEvent) }) combinedLogStream.on('error', (error) => { @@ -86,18 +118,18 @@ export class LogController { destroyStream() }) - combinedLogStream.on('end', () => { + combinedLogStream.on('close', () => { subscriber.complete() destroyStream() }) const fetchLog = async (podName: string) => { - let k8sResponse: http.IncomingMessage | undefined const podLogStream = new PassThrough() streamsEnded.add(podName) + podLogStreams.push(podLogStream) try { - k8sResponse = await logs.log( + const k8sResponse: http.IncomingMessage = await logs.log( user.namespace, podName, containerName, @@ -110,39 +142,49 @@ export class LogController { tailLines: 1000, }, ) + + k8sLogResponses.push(k8sResponse) + podLogStream.pipe(combinedLogStream, { end: false }) podLogStream.on('error', (error) => { - combinedLogStream.emit('error', error) - podLogStream.removeAllListeners() - podLogStream.destroy() + subscriber.error(error) + this.logger.error(`podLogStream error for pod ${podName}`, error) + destroyStream() }) - podLogStream.once('end', () => { + k8sResponse.on('close', () => { streamsEnded.delete(podName) if (streamsEnded.size === 0) { - combinedLogStream.end() + combinedLogStream.emit('close') + } + }) + + podLogStream.on('close', () => { + streamsEnded.delete(podName) + if (streamsEnded.size === 0) { + combinedLogStream.emit('close') } }) } catch (error) { - this.logger.error(`Failed to get logs for pod ${podName}`, error) subscriber.error(error) - k8sResponse?.destroy() - podLogStream.removeAllListeners() - podLogStream.destroy() + this.logger.error(`Failed to get logs for pod ${podName}`, error) destroyStream() } } - if (podNameList && podNameList.length > 0) { + if (podName === 'all' && podNameList.length > 0) { podNameList.forEach((podName) => { fetchLog(podName) }) } else { fetchLog(podName) } + // Clean up when the client disconnects - return () => destroyStream() + return () => { + destroyStream() + } }) } } diff --git a/web/package-lock.json b/web/package-lock.json index 6d1e291..a4a7fd8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -18,6 +18,7 @@ "@codingame/monaco-vscode-typescript-basics-default-extension": "~1.82.3", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", + "@microsoft/fetch-event-source": "^2.0.1", "@monaco-editor/react": "^4.6.0", "@patternfly/react-log-viewer": "^5.0.0", "@sentry/integrations": "^7.73.0", @@ -3797,6 +3798,11 @@ "undici-types": "~5.26.4" } }, + "node_modules/@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==" + }, "node_modules/@monaco-editor/loader": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", @@ -5416,10 +5422,24 @@ "integrity": "sha512-ceOhN1DL7Y4O6M0j9ICgmTYziV89WMd96SvSl0REd8PMgrY0B/WBOPoed5S1KUmJqXgUXh8gzSe6E3ae27upsQ==" }, "node_modules/caniuse-lite": { - "version": "1.0.30001469", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz", - "integrity": "sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==", - "dev": true + "version": "1.0.30001626", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001626.tgz", + "integrity": "sha512-JRW7kAH8PFJzoPCJhLSHgDgKg5348hsQ68aqb+slnzuB5QFERv846oA/mRChmlLAOdEDeOkRn3ynb1gSFnjt3w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] }, "node_modules/caseless": { "version": "0.12.0", @@ -15663,6 +15683,11 @@ } } }, + "@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==" + }, "@monaco-editor/loader": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", @@ -16827,9 +16852,9 @@ "integrity": "sha512-ceOhN1DL7Y4O6M0j9ICgmTYziV89WMd96SvSl0REd8PMgrY0B/WBOPoed5S1KUmJqXgUXh8gzSe6E3ae27upsQ==" }, "caniuse-lite": { - "version": "1.0.30001469", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz", - "integrity": "sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==", + "version": "1.0.30001626", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001626.tgz", + "integrity": "sha512-JRW7kAH8PFJzoPCJhLSHgDgKg5348hsQ68aqb+slnzuB5QFERv846oA/mRChmlLAOdEDeOkRn3ynb1gSFnjt3w==", "dev": true }, "caseless": { diff --git a/web/package.json b/web/package.json index 06f2a69..7c9df22 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,7 @@ "@codingame/monaco-vscode-typescript-basics-default-extension": "~1.82.3", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", + "@microsoft/fetch-event-source": "^2.0.1", "@monaco-editor/react": "^4.6.0", "@patternfly/react-log-viewer": "^5.0.0", "@sentry/integrations": "^7.73.0", diff --git a/web/src/components/Editor/TSEditor.tsx b/web/src/components/Editor/TSEditor.tsx index 25fd915..560894e 100644 --- a/web/src/components/Editor/TSEditor.tsx +++ b/web/src/components/Editor/TSEditor.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef } from "react"; +import { useState } from "react"; import { useCompletionFeature } from "react-monaco-copilot"; import { Spinner } from "@chakra-ui/react"; import { Editor, Monaco } from "@monaco-editor/react"; @@ -25,6 +26,8 @@ export default function TSEditor(props: { }) { const { value, path, fontSize, onChange, colorMode } = props; + const [isEditorMounted, setIsEditorMounted] = useState(false); + const functionCache = useFunctionCache(); const { currentFunction, allFunctionList } = useFunctionStore((state) => state); const { commonSettings } = useCustomSettingStore(); @@ -66,23 +69,30 @@ export default function TSEditor(props: { loadModelsRef.current(monacoRef.current!); autoImportTypings.loadDefaults(monacoRef.current); }, 10); + + setIsEditorMounted(true); } useEffect(() => { - if (monacoRef.current) { + if (isEditorMounted && monacoRef.current) { loadModelsRef.current(monacoRef.current!); } - }, [allFunctionList]); + }, [allFunctionList, isEditorMounted]); useEffect(() => { - const pos = JSON.parse(functionCache.getPositionCache(path) || "{}"); - if (pos.lineNumber && pos.column) { - editorRef.current?.setPosition(pos); - editorRef.current?.revealPositionInCenter(pos); + if (isEditorMounted) { + const pos = JSON.parse(functionCache.getPositionCache(path) || "{}"); + if (pos.lineNumber && pos.column) { + editorRef.current?.setPosition(pos); + editorRef.current?.revealPositionInCenter(pos); + } + + if (monacoRef.current) { + autoImportTypings.parse(value, monacoRef.current); + } } - autoImportTypings.parse(value, monacoRef.current); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [path]); + }, [path, isEditorMounted]); const options = { minimap: { diff --git a/web/src/layouts/Function.tsx b/web/src/layouts/Function.tsx index 12c95af..7bd4bfe 100644 --- a/web/src/layouts/Function.tsx +++ b/web/src/layouts/Function.tsx @@ -4,9 +4,10 @@ import { Badge, Center, Spinner, useColorMode } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; import clsx from "clsx"; -import { APP_PHASE_STATUS, COLOR_MODE, Pages } from "@/constants/index"; +import { APP_PHASE_STATUS, APP_STATUS, COLOR_MODE, Pages } from "@/constants/index"; import { ApplicationControllerFindOne } from "@/apis/v1/applications"; +import InitLog from "@/pages/app/mods/StatusBar/LogsModal/initLog"; import useGlobalStore from "@/pages/globalStore"; export default function FunctionLayout() { @@ -64,12 +65,17 @@ export default function FunctionLayout() { ) : ( <> - {currentApp?.phase !== APP_PHASE_STATUS.Started && - currentApp?.phase !== APP_PHASE_STATUS.Stopped && - currentApp?.phase !== APP_PHASE_STATUS.Deleted ? ( + {currentApp.phase === APP_PHASE_STATUS.Starting && + currentApp.state !== APP_STATUS.Restarting ? ( + + ) : [ + APP_PHASE_STATUS.Creating, + APP_PHASE_STATUS.Deleting, + APP_PHASE_STATUS.Stopping, + ].includes(currentApp.phase) || currentApp.state === APP_STATUS.Restarting ? (
diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/index.scss b/web/src/pages/app/mods/StatusBar/LogsModal/index.scss index 882ae53..6e98594 100644 --- a/web/src/pages/app/mods/StatusBar/LogsModal/index.scss +++ b/web/src/pages/app/mods/StatusBar/LogsModal/index.scss @@ -1,3 +1,4 @@ +/* stylelint-disable selector-class-pattern */ #log-viewer-container { .pf-v5-c-text-input-group__icon { visibility: hidden; @@ -29,3 +30,16 @@ background: #2b7873 !important; } } + +.log-viewer-container-hide-scrollbar { + &, + & * { + &::-webkit-scrollbar { + width: 0 !important; + height: 0 !important; + } + + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/index.tsx b/web/src/pages/app/mods/StatusBar/LogsModal/index.tsx index dfd873a..5f8d550 100644 --- a/web/src/pages/app/mods/StatusBar/LogsModal/index.tsx +++ b/web/src/pages/app/mods/StatusBar/LogsModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, @@ -15,61 +15,66 @@ import { useColorMode, useDisclosure, } from "@chakra-ui/react"; +import { EventStreamContentType, fetchEventSource } from "@microsoft/fetch-event-source"; import { LogViewer, LogViewerSearch } from "@patternfly/react-log-viewer"; import { useQuery } from "@tanstack/react-query"; import clsx from "clsx"; -import { debounce } from "lodash"; import { DownIcon, RefreshIcon } from "@/components/CommonIcon"; -import { formatDate } from "@/utils/format"; -import { streamFetch } from "@/utils/streamFetch"; import "./index.scss"; import { PodControllerGetContainerNameList, PodControllerGetPodNameList } from "@/apis/v1/apps"; +import useSessionStore from "@/pages/auth/store"; import useCustomSettingStore from "@/pages/customSetting"; import useGlobalStore from "@/pages/globalStore"; +type Log = { + data: string; + event: string; + id: string; + retry?: number; +}; + +const MAX_RETRIES = 5; + export default function LogsModal(props: { children: React.ReactElement }) { const { children } = props; const { isOpen, onOpen, onClose } = useDisclosure(); const { t } = useTranslation(); const settingStore = useCustomSettingStore(); + const { showWarning } = useGlobalStore(({ showWarning }) => ({ showWarning })); const { currentApp } = useGlobalStore((state) => state); - - const [logs, setLogs] = useState(""); const [podName, setPodName] = useState(""); const [containerName, setContainerName] = useState(""); const [isLoading, setIsLoading] = useState(true); - const [rowNumber, setRowNumber] = useState(0); - const [isPaused, setIsPaused] = useState(false); - const [pausedRowNumber, setPausedRowNumber] = useState(0); + const [rowCount, setRowCount] = useState(0); + const [paused, setPaused] = useState(false); + const [logs, setLogs] = useState([]); const [renderLogs, setRenderLogs] = useState(""); const [refresh, setRefresh] = useState(true); + const retryCountRef = useRef(0); const darkMode = useColorMode().colorMode === "dark"; - useEffect(() => { - const resizeHandler = debounce(() => { - if (!isPaused) { - setRefresh((pre) => !pre); - } - }, 200); - - window.addEventListener("resize", resizeHandler); + const addOrUpdateLog = (newLog: Log) => { + setLogs((pre) => { + const existingLogIndex = pre.findIndex((existingLog) => existingLog.id === newLog.id); - return () => { - window.removeEventListener("resize", resizeHandler); - }; - }, [isPaused]); - - useEffect(() => { - if (!isPaused) { - setRenderLogs(logs.trim()); - } - }, [isPaused, logs]); + if (existingLogIndex !== -1) { + const updatedLogs = [...pre]; + updatedLogs[existingLogIndex] = { + ...updatedLogs[existingLogIndex], + data: newLog.data, + }; + return updatedLogs; + } else { + return [...pre, newLog]; + } + }); + }; const { data: podData } = useQuery( ["GetPodQuery"], @@ -103,46 +108,78 @@ export default function LogsModal(props: { children: React.ReactElement }) { ); const fetchLogs = useCallback(() => { - if (!podName && !containerName) return; - const controller = new AbortController(); - streamFetch({ - url: `/v1/apps/${currentApp.appid}/logs/${podName}?containerName=${containerName}`, - abortSignal: controller, - firstResponse() { - setIsLoading(false); - }, - onMessage(text) { - const regex = /id:\s\d+\s+data:\s(.*)\s+data:/g; - const logs = [...text.matchAll(regex)]; - const regexTime = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)/g; + if (!podName || !containerName) return; + const ctrl = new AbortController(); + + fetchEventSource( + `/v1/apps/${currentApp.appid}/logs/${podName}?containerName=${containerName}`, + { + method: "GET", + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + Credential: btoa(useSessionStore.getState().getKubeconfig()), + }, + signal: ctrl.signal, + async onopen(response) { + if (response.ok && response.headers.get("content-type") === EventStreamContentType) { + setIsLoading(false); + } else { + throw new Error(`Unexpected response: ${response.status} ${response.statusText}`); + } + }, + + onmessage(msg) { + if (msg.event === "error") { + showWarning(msg.data); + } + + if (msg.event === "log") { + addOrUpdateLog(msg); + retryCountRef.current = 0; + } + }, - const logStr = logs - .map((log) => - log[1].replace(regexTime, (str) => formatDate(str, "YYYY-MM-DD HH:mm:ss.SSS")), - ) - .join("\n"); + onclose() { + // if the server closes the connection unexpectedly, retry: + if (retryCountRef.current < MAX_RETRIES) { + retryCountRef.current += 1; + throw new Error("connect closed unexpectedly, retrying..."); + } + }, - setRowNumber((pre) => pre + logs.length); - setLogs((pre) => pre + logStr + "\n"); + onerror(err) { + showWarning(err.message); + // auto retry fetch + }, }, - }).catch((e) => { - if (e.includes("BodyStreamBuffer was aborted")) { - return; - } - throw e; - }); - return controller; - }, [podName, containerName, currentApp.appid]); + ); + return ctrl; + }, [podName, containerName, currentApp.appid, showWarning]); useEffect(() => { if (!isOpen) return; - setLogs(""); + setRowCount(0); + setLogs([]); setIsLoading(true); - const controller = fetchLogs(); + setPaused(false); + const ctrl = fetchLogs(); + return () => { - controller?.abort(); + ctrl?.abort(); }; - }, [podName, containerName, isOpen, refresh]); + }, [podName, containerName, isOpen, refresh, fetchLogs]); + + useEffect(() => { + const sortedLogs = logs.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + const concatenatedLogs = sortedLogs.map((log) => log.data).join(""); + setRenderLogs(concatenatedLogs); + const totalRows = concatenatedLogs.split("\n").length; + setRowCount(totalRows); + }, [logs]); + + useEffect(() => { + retryCountRef.current = 0; + }, [isOpen]); return ( <> @@ -163,8 +200,6 @@ export default function LogsModal(props: { children: React.ReactElement }) { className="ml-4 !h-8 !w-64" onChange={(e) => { setPodName(e.target.value); - setIsLoading(true); - setLogs(""); }} value={podName} > @@ -185,8 +220,6 @@ export default function LogsModal(props: { children: React.ReactElement }) { className="ml-1 !h-8 !w-32" onChange={(e) => { setContainerName(e.target.value); - setIsLoading(true); - setLogs(""); }} value={containerName} > @@ -205,7 +238,7 @@ export default function LogsModal(props: { children: React.ReactElement }) { px={2} onClick={() => { setRefresh((pre) => !pre); - setIsPaused(false); + setPaused(false); }} > {t("Refresh")} @@ -221,24 +254,26 @@ export default function LogsModal(props: { children: React.ReactElement }) { ) : (
{ - if (e.deltaY < 0 && !isPaused) { - setIsPaused(true); - setPausedRowNumber(rowNumber); - } - }} > { if (e.scrollOffsetToBottom <= 0) { - setIsPaused(false); + setPaused(false); + return; + } + if (!e.scrollUpdateWasRequested) { + setPaused(true); + return; } + setPaused(false); }} toolbar={
@@ -251,10 +286,10 @@ export default function LogsModal(props: { children: React.ReactElement }) { } />
- {isPaused && ( + {paused && ( { - setIsPaused(false); + setPaused(false); }} className={clsx( "flex w-full cursor-pointer items-center justify-center", diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss new file mode 100644 index 0000000..02e7189 --- /dev/null +++ b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.scss @@ -0,0 +1,45 @@ +/* stylelint-disable selector-class-pattern */ +#log-viewer-cover-container { + .pf-v5-c-text-input-group__icon { + visibility: hidden; + } + + .pf-v5-c-text-input-group__text-input:focus { + outline: none !important; + color: #000; + } + + .pf-m-current { + background: #91ded9 !important; + } + + .pf-m-match { + background: #daf4f2 !important; + } + + [data-theme="dark"] & .pf-v5-c-text-input-group__text-input:focus { + outline: none !important; + color: #fff; + } + + [data-theme="dark"] & .pf-m-current { + background: #47c8bf !important; + } + + [data-theme="dark"] & .pf-m-match { + background: #2b7873 !important; + } +} + +.log-viewer-cover-container-hide-scrollbar { + &, + & * { + &::-webkit-scrollbar { + width: 0 !important; + height: 0 !important; + } + + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx new file mode 100644 index 0000000..7f7003f --- /dev/null +++ b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx @@ -0,0 +1,155 @@ +import { useCallback, useEffect, useState } from "react"; +import { Badge, Center, Spinner, useColorMode } from "@chakra-ui/react"; +import { EventStreamContentType, fetchEventSource } from "@microsoft/fetch-event-source"; +import { LogViewer } from "@patternfly/react-log-viewer"; +import clsx from "clsx"; + +import "./initLog.scss"; + +import useSessionStore from "@/pages/auth/store"; +import useGlobalStore from "@/pages/globalStore"; + +type Log = { + data: string; + event: string; + id: string; + retry?: number; +}; + +export default function InitLog() { + const { currentApp } = useGlobalStore((state) => state); + const [isLoading, setIsLoading] = useState(true); + const [rowCount, setRowCount] = useState(0); + const [paused, setPaused] = useState(false); + + const [logs, setLogs] = useState([]); + const [renderLogs, setRenderLogs] = useState(""); + + const darkMode = useColorMode().colorMode === "dark"; + + const addOrUpdateLog = (newLog: Log) => { + setLogs((pre) => { + const existingLogIndex = pre.findIndex((existingLog) => existingLog.id === newLog.id); + + if (existingLogIndex !== -1) { + const updatedLogs = [...pre]; + updatedLogs[existingLogIndex] = { + ...updatedLogs[existingLogIndex], + data: newLog.data, + }; + return updatedLogs; + } else { + return [...pre, newLog]; + } + }); + }; + + const fetchLogs = useCallback(() => { + const ctrl = new AbortController(); + + fetchEventSource(`/v1/apps/${currentApp.appid}/logs/all?containerName=init`, { + method: "GET", + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + Credential: btoa(useSessionStore.getState().getKubeconfig()), + }, + signal: ctrl.signal, + async onopen(response) { + if (response.ok && response.headers.get("content-type") === EventStreamContentType) { + setIsLoading(false); + } else { + throw new Error(`Unexpected response: ${response.status} ${response.statusText}`); + } + }, + + onmessage(msg) { + if (msg.event === "error") { + throw new Error(msg.data); + } + + if (msg.event === "log") { + addOrUpdateLog(msg); + } + }, + + onclose() { + throw new Error("connect closed unexpectedly, retrying..."); + }, + + onerror(err) { + // auto retry fetch + }, + }); + return ctrl; + }, [currentApp.appid]); + + useEffect(() => { + setRowCount(0); + setLogs([]); + setIsLoading(true); + setPaused(false); + const ctrl = fetchLogs(); + + return () => { + ctrl?.abort(); + }; + }, [fetchLogs]); + + useEffect(() => { + const sortedLogs = logs.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + const concatenatedLogs = sortedLogs.map((log) => log.data).join(""); + setRenderLogs(concatenatedLogs); + const totalRows = concatenatedLogs.split("\n").length; + setRowCount(totalRows); + }, [logs]); + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ { + if (e.scrollOffsetToBottom <= 0) { + setPaused(false); + return; + } + if (!e.scrollUpdateWasRequested) { + setPaused(true); + return; + } + setPaused(false); + }} + /> +
+
+ + {currentApp.phase}... +
+ + )} + + ); +} diff --git a/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx b/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx index 69ae5e0..a1eb7d8 100644 --- a/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx +++ b/web/src/pages/app/setting/SysSetting/AppInfoList/index.tsx @@ -13,7 +13,12 @@ import InfoDetail from "./InfoDetail"; import useGlobalStore from "@/pages/globalStore"; import DeleteAppModal from "@/pages/home/mods/DeleteAppModal"; import StatusBadge from "@/pages/home/mods/StatusBadge"; -const AppEnvList = () => { +interface AppEnvListProps { + onClose?: () => void; +} + +const AppEnvList: React.FC = (props = {}) => { + const { onClose } = props; const { t } = useTranslation(); const navigate = useNavigate(); @@ -67,6 +72,9 @@ const AppEnvList = () => { ? APP_STATUS.Running : APP_STATUS.Restarting, ); + if (currentApp?.phase === APP_PHASE_STATUS.Stopped && onClose) { + onClose(); + } }} > {currentApp?.phase === APP_PHASE_STATUS.Stopped ? ( From 4a378eb8bb7de6d2b6172df7eda619e36e8f7c3a Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Mon, 3 Jun 2024 10:45:24 +0000 Subject: [PATCH 2/3] style --- web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx index 7f7003f..edc112e 100644 --- a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx +++ b/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx @@ -114,7 +114,7 @@ export default function InitLog() {
Date: Wed, 5 Jun 2024 02:42:20 +0000 Subject: [PATCH 3/3] chore --- web/src/layouts/Function.tsx | 2 +- .../app/mods/StatusBar/LogsModal/{initLog.tsx => InitLog.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename web/src/pages/app/mods/StatusBar/LogsModal/{initLog.tsx => InitLog.tsx} (100%) diff --git a/web/src/layouts/Function.tsx b/web/src/layouts/Function.tsx index 7bd4bfe..448c348 100644 --- a/web/src/layouts/Function.tsx +++ b/web/src/layouts/Function.tsx @@ -7,7 +7,7 @@ import clsx from "clsx"; import { APP_PHASE_STATUS, APP_STATUS, COLOR_MODE, Pages } from "@/constants/index"; import { ApplicationControllerFindOne } from "@/apis/v1/applications"; -import InitLog from "@/pages/app/mods/StatusBar/LogsModal/initLog"; +import InitLog from "@/pages/app/mods/StatusBar/LogsModal/InitLog"; import useGlobalStore from "@/pages/globalStore"; export default function FunctionLayout() { diff --git a/web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx b/web/src/pages/app/mods/StatusBar/LogsModal/InitLog.tsx similarity index 100% rename from web/src/pages/app/mods/StatusBar/LogsModal/initLog.tsx rename to web/src/pages/app/mods/StatusBar/LogsModal/InitLog.tsx