Skip to content

Commit bc9b6f2

Browse files
Aleh YablonskiJablDouble
authored andcommitted
feat(log-autoscroll): add scrolling to the end of the page
1 parent 718332d commit bc9b6f2

File tree

7 files changed

+110
-7
lines changed

7 files changed

+110
-7
lines changed

src/ui/components/layouts/Page/Page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ interface PageProps {
99
children: React.ReactNode;
1010
header?: string | React.ReactNode;
1111
className?: string;
12+
id?: string;
1213
}
1314

14-
export const Page = ({children, header, className}: PageProps) => {
15+
export const Page = ({children, header, className, id}: PageProps) => {
1516
return (
16-
<Flex direction="column" gap={4} className={classNames(styles.page, className)}>
17+
<Flex direction="column" gap={4} className={classNames(styles.page, className)} id={id}>
1718
{header && <Text variant="header-1">{header}</Text>}
1819
{children}
1920
</Flex>

src/ui/containers/Instance/pages/BuildLogs/BuildLogs.module.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@
1919
.output {
2020
margin: 0;
2121
}
22+
23+
.scrollToUpButton {
24+
position: fixed;
25+
bottom: 60px;
26+
right: var(--g-spacing-4);
27+
color: var(--g-color-text-misc);
28+
}

src/ui/containers/Instance/pages/BuildLogs/BuildLogs.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react';
22

33
import {idle, useQueryData} from '@gravity-ui/data-source';
4-
import {Flex, Text, sp} from '@gravity-ui/uikit';
4+
import {ArrowUp} from '@gravity-ui/icons';
5+
import {Button, Flex, Icon, Text, Tooltip, sp} from '@gravity-ui/uikit';
56
import {useParams} from 'react-router-dom';
67

78
import type {ListLogsResponse} from '../../../../../shared/api/listLogs';
@@ -14,6 +15,8 @@ import {i18nInstance} from '../../../../i18n-common/i18nInstance';
1415
import {generateInstanceHref} from '../../../../utils/common';
1516
import {InstanceLayout} from '../../layouts/InstanceLayout';
1617

18+
import {BUILD_LOGS_PAGE_ID} from './constants';
19+
import {useAutoscrollingBehavior} from './hooks';
1720
import {i18n} from './i18n';
1821

1922
import * as styles from './BuildLogs.module.scss';
@@ -41,8 +44,14 @@ interface LogsContentProps {
4144
}
4245

4346
const LogsContent = ({instance, listLogs}: LogsContentProps) => {
47+
const logsContainerRef = React.useRef<HTMLDivElement>(null);
48+
const LogsBottomRef = React.useRef<HTMLAnchorElement | HTMLDivElement>(null);
49+
50+
const {isScrollTopButtonVisible} = useAutoscrollingBehavior(listLogs, LogsBottomRef);
51+
4452
const renderLog = (item: Output, index: number) => {
4553
const {command, duration, stdout, stderr} = item;
54+
const isLastLog = index === (listLogs?.logs?.length || 0) - 1;
4655

4756
return (
4857
<React.Fragment key={`log-${index}`}>
@@ -80,6 +89,7 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => {
8089
<hr />
8190
</div>
8291
)}
92+
{isLastLog && <div ref={LogsBottomRef as React.RefObject<HTMLDivElement>} />}
8393
</React.Fragment>
8494
);
8595
};
@@ -105,16 +115,48 @@ const LogsContent = ({instance, listLogs}: LogsContentProps) => {
105115
});
106116

107117
return (
108-
<a className={styles.instanceLink} href={href}>
118+
<a
119+
ref={LogsBottomRef as React.RefObject<HTMLAnchorElement>}
120+
className={styles.instanceLink}
121+
href={href}
122+
>
109123
{i18nInstance('go-to-instance')}
110124
</a>
111125
);
112126
};
113127

128+
const renderScrollToTopButton = () => {
129+
const handleScrollToTop = () => {
130+
logsContainerRef.current?.scrollIntoView({
131+
behavior: 'smooth',
132+
block: 'start',
133+
});
134+
};
135+
136+
if (!isScrollTopButtonVisible) {
137+
return null;
138+
}
139+
140+
return (
141+
<Tooltip content={i18n('return-to-start-of-logs')} placement="left" openDelay={0}>
142+
<Button
143+
view="raised"
144+
pin="circle-circle"
145+
size="xl"
146+
className={styles.scrollToUpButton}
147+
onClick={handleScrollToTop}
148+
>
149+
<Icon data={ArrowUp} size={24} />
150+
</Button>
151+
</Tooltip>
152+
);
153+
};
154+
114155
return (
115-
<div>
156+
<div ref={logsContainerRef} id="logs">
116157
{listLogs?.logs?.map(renderLog)}
117158
{renderInstanceLink()}
159+
{renderScrollToTopButton()}
118160
</div>
119161
);
120162
};
@@ -135,6 +177,7 @@ export const InstanceBuildLogsPage = () => {
135177
listLogs: listLogsQuery.data,
136178
})}
137179
className={styles.buildLogs}
180+
id={BUILD_LOGS_PAGE_ID}
138181
>
139182
<DataLoader
140183
status={instanceQuery.status}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const BUILD_LOGS_PAGE_ID = 'build-logs-page';
2+
3+
export const SCROLL_TO_TOP_THRESHOLD = 400;
4+
5+
// Scroll behavior constants
6+
export const SCROLL_BOTTOM_TOLERANCE = 20;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
3+
import type {ListLogsResponse} from '../../../../../shared/api/listLogs';
4+
5+
import {BUILD_LOGS_PAGE_ID, SCROLL_BOTTOM_TOLERANCE, SCROLL_TO_TOP_THRESHOLD} from './constants';
6+
7+
export const useAutoscrollingBehavior = (
8+
listLogs: ListLogsResponse | undefined,
9+
logsBottomRef: React.RefObject<HTMLAnchorElement | HTMLDivElement>,
10+
) => {
11+
const [isScrollTopButtonVisible, setIsScrollTopButtonVisible] = React.useState(false);
12+
const [shouldAutoscroll, setShouldAutoscroll] = React.useState(true);
13+
14+
// Автоскролл к концу логов при появлении новых
15+
React.useEffect(() => {
16+
if (logsBottomRef.current && shouldAutoscroll) {
17+
logsBottomRef.current.scrollIntoView({behavior: 'auto', block: 'center'});
18+
}
19+
}, [listLogs?.logs, shouldAutoscroll, logsBottomRef]);
20+
21+
React.useEffect(() => {
22+
const page = document.getElementById(BUILD_LOGS_PAGE_ID);
23+
if (!page) return;
24+
25+
const handleScroll = () => {
26+
const {scrollTop, scrollHeight, clientHeight} = page;
27+
const logsPageHeight = scrollHeight - clientHeight;
28+
// проверяем, находится ли пользователь в самом низу страницы с погрешностью SCROLL_BOTTOM_TOLERANCE
29+
const isAtBottom = logsPageHeight - scrollTop < SCROLL_BOTTOM_TOLERANCE;
30+
31+
// отображать кнопку возврата к началу страницы если пользователь не в самом начале страницы
32+
setIsScrollTopButtonVisible(scrollTop > SCROLL_TO_TOP_THRESHOLD);
33+
setShouldAutoscroll(isAtBottom);
34+
};
35+
36+
page.addEventListener('scroll', handleScroll);
37+
38+
return () => page.removeEventListener('scroll', handleScroll);
39+
}, []);
40+
41+
return {
42+
isScrollTopButtonVisible,
43+
};
44+
};
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"title": "Build logs",
3-
"command-finished": "Command finished in {{duration}}s"
3+
"command-finished": "Command finished in {{duration}}s",
4+
"return-to-start-of-logs": "Return to start of logs"
45
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"title": "Логи сборки",
3-
"command-finished": "Команда завершена за {{duration}}с"
3+
"command-finished": "Команда завершена за {{duration}}с",
4+
"return-to-start-of-logs": "Вернуться к началу логов"
45
}

0 commit comments

Comments
 (0)