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..3b3a47a 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,6 +44,11 @@ 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; @@ -111,10 +119,39 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => { ); }; + 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 +172,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); + + // Auto-scroll to the end of logs when new ones appear + 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; + // 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); + }; + + 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 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