From bc9b6f2b69dd90dd526cb91b7ed766e94d1fbff2 Mon Sep 17 00:00:00 2001 From: Aleh Yablonski Date: Tue, 21 Oct 2025 21:12:17 +0300 Subject: [PATCH 1/3] feat(log-autoscroll): add scrolling to the end of the page --- src/ui/components/layouts/Page/Page.tsx | 5 +- .../pages/BuildLogs/BuildLogs.module.scss | 7 +++ .../Instance/pages/BuildLogs/BuildLogs.tsx | 49 +++++++++++++++++-- .../Instance/pages/BuildLogs/constants.ts | 6 +++ .../Instance/pages/BuildLogs/hooks.ts | 44 +++++++++++++++++ .../Instance/pages/BuildLogs/i18n/en.json | 3 +- .../Instance/pages/BuildLogs/i18n/ru.json | 3 +- 7 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 src/ui/containers/Instance/pages/BuildLogs/constants.ts create mode 100644 src/ui/containers/Instance/pages/BuildLogs/hooks.ts diff --git a/src/ui/components/layouts/Page/Page.tsx b/src/ui/components/layouts/Page/Page.tsx index b76e951..aada18c 100644 --- a/src/ui/components/layouts/Page/Page.tsx +++ b/src/ui/components/layouts/Page/Page.tsx @@ -9,11 +9,12 @@ interface PageProps { children: React.ReactNode; header?: string | React.ReactNode; className?: string; + id?: string; } -export const Page = ({children, header, className}: PageProps) => { +export const Page = ({children, header, className, id}: PageProps) => { return ( - + {header && {header}} {children} diff --git a/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.module.scss b/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.module.scss index 9238851..495c450 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.module.scss +++ b/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.module.scss @@ -19,3 +19,10 @@ .output { margin: 0; } + +.scrollToUpButton { + position: fixed; + bottom: 60px; + right: var(--g-spacing-4); + color: var(--g-color-text-misc); +} diff --git a/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx b/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx index 32a3bf7..d905c0b 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx +++ b/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx @@ -1,7 +1,8 @@ import React from 'react'; import {idle, useQueryData} from '@gravity-ui/data-source'; -import {Flex, Text, sp} from '@gravity-ui/uikit'; +import {ArrowUp} from '@gravity-ui/icons'; +import {Button, Flex, Icon, Text, Tooltip, sp} from '@gravity-ui/uikit'; import {useParams} from 'react-router-dom'; import type {ListLogsResponse} from '../../../../../shared/api/listLogs'; @@ -14,6 +15,8 @@ import {i18nInstance} from '../../../../i18n-common/i18nInstance'; import {generateInstanceHref} from '../../../../utils/common'; import {InstanceLayout} from '../../layouts/InstanceLayout'; +import {BUILD_LOGS_PAGE_ID} from './constants'; +import {useAutoscrollingBehavior} from './hooks'; import {i18n} from './i18n'; import * as styles from './BuildLogs.module.scss'; @@ -41,8 +44,14 @@ interface LogsContentProps { } const LogsContent = ({instance, listLogs}: LogsContentProps) => { + const logsContainerRef = React.useRef(null); + const LogsBottomRef = React.useRef(null); + + const {isScrollTopButtonVisible} = useAutoscrollingBehavior(listLogs, LogsBottomRef); + const renderLog = (item: Output, index: number) => { const {command, duration, stdout, stderr} = item; + const isLastLog = index === (listLogs?.logs?.length || 0) - 1; return ( @@ -80,6 +89,7 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => {
)} + {isLastLog &&
} />} ); }; @@ -105,16 +115,48 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => { }); return ( - + } + className={styles.instanceLink} + href={href} + > {i18nInstance('go-to-instance')} ); }; + const renderScrollToTopButton = () => { + const handleScrollToTop = () => { + logsContainerRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }; + + if (!isScrollTopButtonVisible) { + return null; + } + + return ( + + + + ); + }; + return ( -
+
{listLogs?.logs?.map(renderLog)} {renderInstanceLink()} + {renderScrollToTopButton()}
); }; @@ -135,6 +177,7 @@ export const InstanceBuildLogsPage = () => { listLogs: listLogsQuery.data, })} className={styles.buildLogs} + id={BUILD_LOGS_PAGE_ID} > , +) => { + const [isScrollTopButtonVisible, setIsScrollTopButtonVisible] = React.useState(false); + const [shouldAutoscroll, setShouldAutoscroll] = React.useState(true); + + // Автоскролл к концу логов при появлении новых + React.useEffect(() => { + if (logsBottomRef.current && shouldAutoscroll) { + logsBottomRef.current.scrollIntoView({behavior: 'auto', block: 'center'}); + } + }, [listLogs?.logs, shouldAutoscroll, logsBottomRef]); + + React.useEffect(() => { + const page = document.getElementById(BUILD_LOGS_PAGE_ID); + if (!page) return; + + const handleScroll = () => { + const {scrollTop, scrollHeight, clientHeight} = page; + const logsPageHeight = scrollHeight - clientHeight; + // проверяем, находится ли пользователь в самом низу страницы с погрешностью SCROLL_BOTTOM_TOLERANCE + const isAtBottom = logsPageHeight - scrollTop < SCROLL_BOTTOM_TOLERANCE; + + // отображать кнопку возврата к началу страницы если пользователь не в самом начале страницы + setIsScrollTopButtonVisible(scrollTop > SCROLL_TO_TOP_THRESHOLD); + setShouldAutoscroll(isAtBottom); + }; + + page.addEventListener('scroll', handleScroll); + + return () => page.removeEventListener('scroll', handleScroll); + }, []); + + return { + isScrollTopButtonVisible, + }; +}; diff --git a/src/ui/containers/Instance/pages/BuildLogs/i18n/en.json b/src/ui/containers/Instance/pages/BuildLogs/i18n/en.json index 07da19f..884c47d 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/i18n/en.json +++ b/src/ui/containers/Instance/pages/BuildLogs/i18n/en.json @@ -1,4 +1,5 @@ { "title": "Build logs", - "command-finished": "Command finished in {{duration}}s" + "command-finished": "Command finished in {{duration}}s", + "return-to-start-of-logs": "Return to start of logs" } \ No newline at end of file diff --git a/src/ui/containers/Instance/pages/BuildLogs/i18n/ru.json b/src/ui/containers/Instance/pages/BuildLogs/i18n/ru.json index 586f1b8..c8ba71f 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/i18n/ru.json +++ b/src/ui/containers/Instance/pages/BuildLogs/i18n/ru.json @@ -1,4 +1,5 @@ { "title": "Логи сборки", - "command-finished": "Команда завершена за {{duration}}с" + "command-finished": "Команда завершена за {{duration}}с", + "return-to-start-of-logs": "Вернуться к началу логов" } \ No newline at end of file From 4fbd523c388e8cff65efa6dba8da353b9e5a8cb8 Mon Sep 17 00:00:00 2001 From: Aleh Yablonski Date: Fri, 24 Oct 2025 18:25:08 +0300 Subject: [PATCH 2/3] fix(logs-autoscroll): translate comments & add mock logs for docker project --- src/ui/containers/Instance/pages/BuildLogs/constants.ts | 1 - src/ui/containers/Instance/pages/BuildLogs/hooks.ts | 6 +++--- test-docker-app/test-app.Dockerfile | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ui/containers/Instance/pages/BuildLogs/constants.ts b/src/ui/containers/Instance/pages/BuildLogs/constants.ts index c47dfe2..9762158 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/constants.ts +++ b/src/ui/containers/Instance/pages/BuildLogs/constants.ts @@ -2,5 +2,4 @@ export const BUILD_LOGS_PAGE_ID = 'build-logs-page'; export const SCROLL_TO_TOP_THRESHOLD = 400; -// Scroll behavior constants export const SCROLL_BOTTOM_TOLERANCE = 20; diff --git a/src/ui/containers/Instance/pages/BuildLogs/hooks.ts b/src/ui/containers/Instance/pages/BuildLogs/hooks.ts index 76002bc..a738be1 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/hooks.ts +++ b/src/ui/containers/Instance/pages/BuildLogs/hooks.ts @@ -11,7 +11,7 @@ export const useAutoscrollingBehavior = ( const [isScrollTopButtonVisible, setIsScrollTopButtonVisible] = React.useState(false); const [shouldAutoscroll, setShouldAutoscroll] = React.useState(true); - // Автоскролл к концу логов при появлении новых + // Auto-scroll to the end of logs when new ones appear React.useEffect(() => { if (logsBottomRef.current && shouldAutoscroll) { logsBottomRef.current.scrollIntoView({behavior: 'auto', block: 'center'}); @@ -25,10 +25,10 @@ export const useAutoscrollingBehavior = ( const handleScroll = () => { const {scrollTop, scrollHeight, clientHeight} = page; const logsPageHeight = scrollHeight - clientHeight; - // проверяем, находится ли пользователь в самом низу страницы с погрешностью SCROLL_BOTTOM_TOLERANCE + // check if user is at the bottom of the page with SCROLL_BOTTOM_TOLERANCE tolerance const isAtBottom = logsPageHeight - scrollTop < SCROLL_BOTTOM_TOLERANCE; - // отображать кнопку возврата к началу страницы если пользователь не в самом начале страницы + // show scroll to top button if user is not at the very beginning of the page setIsScrollTopButtonVisible(scrollTop > SCROLL_TO_TOP_THRESHOLD); setShouldAutoscroll(isAtBottom); }; diff --git a/test-docker-app/test-app.Dockerfile b/test-docker-app/test-app.Dockerfile index 7eb66cd..7313b71 100644 --- a/test-docker-app/test-app.Dockerfile +++ b/test-docker-app/test-app.Dockerfile @@ -1,6 +1,6 @@ FROM crccheck/hello-world -RUN sleep 60 +RUN echo "1" && sleep 4 && echo "2" && sleep 4 && echo "3" && sleep 4 && echo "4" && sleep 4 && echo "5" && sleep 4 && echo "6" && sleep 4 && echo "7" && sleep 4 && echo "8" && sleep 4 && echo "9" && sleep 4 && echo "10" ENV PORT=80 From 627bc251861fe1251d35cebc2abab40310cf60b1 Mon Sep 17 00:00:00 2001 From: Aleh Yablonski Date: Mon, 27 Oct 2025 12:10:38 +0300 Subject: [PATCH 3/3] fix(logs-autoscroll): make universal ref for the end of logs --- .../containers/Instance/pages/BuildLogs/BuildLogs.tsx | 11 +++-------- src/ui/containers/Instance/pages/BuildLogs/hooks.ts | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx b/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx index d905c0b..3b3a47a 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx +++ b/src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx @@ -45,13 +45,12 @@ interface LogsContentProps { const LogsContent = ({instance, listLogs}: LogsContentProps) => { const logsContainerRef = React.useRef(null); - const LogsBottomRef = React.useRef(null); + const LogsBottomRef = React.useRef(null); const {isScrollTopButtonVisible} = useAutoscrollingBehavior(listLogs, LogsBottomRef); const renderLog = (item: Output, index: number) => { const {command, duration, stdout, stderr} = item; - const isLastLog = index === (listLogs?.logs?.length || 0) - 1; return ( @@ -89,7 +88,6 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => {
)} - {isLastLog &&
} />} ); }; @@ -115,11 +113,7 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => { }); return ( - } - className={styles.instanceLink} - href={href} - > + {i18nInstance('go-to-instance')} ); @@ -157,6 +151,7 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => { {listLogs?.logs?.map(renderLog)} {renderInstanceLink()} {renderScrollToTopButton()} +
); }; diff --git a/src/ui/containers/Instance/pages/BuildLogs/hooks.ts b/src/ui/containers/Instance/pages/BuildLogs/hooks.ts index a738be1..55bf570 100644 --- a/src/ui/containers/Instance/pages/BuildLogs/hooks.ts +++ b/src/ui/containers/Instance/pages/BuildLogs/hooks.ts @@ -6,7 +6,7 @@ import {BUILD_LOGS_PAGE_ID, SCROLL_BOTTOM_TOLERANCE, SCROLL_TO_TOP_THRESHOLD} fr export const useAutoscrollingBehavior = ( listLogs: ListLogsResponse | undefined, - logsBottomRef: React.RefObject, + logsBottomRef: React.RefObject, ) => { const [isScrollTopButtonVisible, setIsScrollTopButtonVisible] = React.useState(false); const [shouldAutoscroll, setShouldAutoscroll] = React.useState(true);