1- import React , { useEffect , useRef , useState } from 'react' ;
1+ import React , { useCallback , useEffect , useLayoutEffect , useRef , useState } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import classNames from 'classnames' ;
4- import { Mode } from '@cloudscape-design/global-styles' ;
5- import { FitAddon } from '@xterm/addon-fit' ;
6- import { Terminal } from '@xterm/xterm' ;
74
8- import { Container , Header , ListEmptyMessage , Loader , TextContent } from 'components' ;
5+ import { Code , Container , Header , ListEmptyMessage , Loader , TextContent } from 'components' ;
96
10- import { useAppSelector } from 'hooks' ;
117import { useLazyGetProjectLogsQuery } from 'services/project' ;
128
13- import { selectSystemMode } from 'App/slice ' ;
9+ import { decodeLogs } from './helpers ' ;
1410
1511import { IProps } from './types' ;
1612
1713import styles from './styles.module.scss' ;
1814
1915const LIMIT_LOG_ROWS = 1000 ;
16+ const LOADING_SCROLL_GAP = 300 ;
2017
2118export const Logs : React . FC < IProps > = ( { className, projectName, runName, jobSubmissionId } ) => {
2219 const { t } = useTranslation ( ) ;
23- const appliedTheme = useAppSelector ( selectSystemMode ) ;
20+ const codeRef = useRef < HTMLDivElement > ( null ) ;
21+ const nextTokenRef = useRef < string | undefined > ( undefined ) ;
22+ const scrollPositionByBottom = useRef < number > ( 0 ) ;
2423
25- const terminalInstance = useRef < Terminal > ( new Terminal ( { scrollback : 10000000 } ) ) ;
26- const fitAddonInstance = useRef < FitAddon > ( new FitAddon ( ) ) ;
2724 const [ logsData , setLogsData ] = useState < ILogItem [ ] > ( [ ] ) ;
2825 const [ isLoading , setIsLoading ] = useState ( false ) ;
29-
3026 const [ getProjectLogs ] = useLazyGetProjectLogsQuery ( ) ;
3127
32- const writeDataToTerminal = ( logs : ILogItem [ ] ) => {
33- logs . forEach ( ( logItem ) => {
34- terminalInstance . current . write ( logItem . message . replace ( / (?< ! \r ) \n / g, '\r\n' ) ) ;
35- } ) ;
28+ const saveScrollPositionByBottom = ( ) => {
29+ if ( ! codeRef . current ) return ;
30+
31+ const { clientHeight, scrollHeight, scrollTop } = codeRef . current ;
32+ scrollPositionByBottom . current = scrollHeight - clientHeight - scrollTop ;
33+ } ;
34+
35+ const restoreScrollPositionByBottom = ( ) => {
36+ if ( ! codeRef . current ) return ;
37+
38+ const { clientHeight, scrollHeight } = codeRef . current ;
39+ codeRef . current . scrollTo ( 0 , scrollHeight - clientHeight - scrollPositionByBottom . current ) ;
40+ } ;
41+
42+ const checkNeedMoreLoadingData = ( ) => {
43+ if ( ! codeRef . current ) return ;
3644
37- fitAddonInstance . current . fit ( ) ;
45+ const { clientHeight, scrollHeight } = codeRef . current ;
46+
47+ if ( scrollHeight - clientHeight <= LOADING_SCROLL_GAP ) {
48+ getLogItems ( ) ;
49+ }
3850 } ;
3951
40- const getNextLogItems = ( nextToken ?: string ) => {
52+ const getLogItems = ( nextToken ?: string ) => {
4153 setIsLoading ( true ) ;
4254
4355 if ( ! jobSubmissionId ) {
@@ -47,64 +59,69 @@ export const Logs: React.FC<IProps> = ({ className, projectName, runName, jobSub
4759 getProjectLogs ( {
4860 project_name : projectName ,
4961 run_name : runName ,
50- descending : false ,
51- job_submission_id : jobSubmissionId ?? '' ,
62+ descending : true ,
63+ job_submission_id : jobSubmissionId ,
5264 next_token : nextToken ,
5365 limit : LIMIT_LOG_ROWS ,
5466 } )
5567 . unwrap ( )
5668 . then ( ( response ) => {
57- setLogsData ( ( old ) => [ ...old , ...response . logs ] ) ;
58-
59- writeDataToTerminal ( response . logs ) ;
60-
61- if ( response . next_token ) {
62- getNextLogItems ( response . next_token ) ;
63- } else {
64- setIsLoading ( false ) ;
65- }
69+ saveScrollPositionByBottom ( ) ;
70+ const reversed = response . logs . toReversed ( ) ;
71+ setLogsData ( ( old ) => [ ...decodeLogs ( reversed ) , ...old ] ) ;
72+ nextTokenRef . current = response . next_token ;
73+ setIsLoading ( false ) ;
6674 } )
6775 . catch ( ( ) => setIsLoading ( false ) ) ;
6876 } ;
6977
78+ const getNextLogItems = ( ) => {
79+ if ( nextTokenRef . current ) {
80+ getLogItems ( nextTokenRef . current ) ;
81+ }
82+ } ;
83+
7084 useEffect ( ( ) => {
71- if ( appliedTheme === Mode . Light ) {
72- terminalInstance . current . options . theme = {
73- foreground : '#000716' ,
74- background : '#ffffff' ,
75- selectionBackground : '#B4D5FE' ,
76- } ;
85+ getLogItems ( ) ;
86+ } , [ ] ) ;
87+
88+ useLayoutEffect ( ( ) => {
89+ if ( logsData . length && logsData . length <= LIMIT_LOG_ROWS ) {
90+ scrollToBottom ( ) ;
7791 } else {
78- terminalInstance . current . options . theme = {
79- foreground : '#b6bec9' ,
80- background : '#161d26' ,
81- } ;
92+ restoreScrollPositionByBottom ( ) ;
8293 }
83- } , [ appliedTheme ] ) ;
8494
85- useEffect ( ( ) => {
86- terminalInstance . current . loadAddon ( fitAddonInstance . current ) ;
95+ if ( logsData . length ) checkNeedMoreLoadingData ( ) ;
96+ } , [ logsData ] ) ;
8797
88- getNextLogItems ( ) ;
98+ const onScroll = useCallback < EventListener > (
99+ ( event ) => {
100+ const element = event . target as HTMLDivElement ;
89101
90- const onResize = ( ) => {
91- fitAddonInstance . current . fit ( ) ;
92- } ;
102+ if ( element . scrollTop <= LOADING_SCROLL_GAP && ! isLoading ) {
103+ getNextLogItems ( ) ;
104+ }
105+ } ,
106+ [ isLoading , logsData ] ,
107+ ) ;
93108
94- window . addEventListener ( 'resize' , onResize ) ;
109+ useEffect ( ( ) => {
110+ if ( ! codeRef . current ) return ;
111+
112+ codeRef . current . addEventListener ( 'scroll' , onScroll ) ;
95113
96114 return ( ) => {
97- window . removeEventListener ( 'resize ' , onResize ) ;
115+ if ( codeRef . current ) codeRef . current . removeEventListener ( 'scroll ' , onScroll ) ;
98116 } ;
99- } , [ ] ) ;
117+ } , [ codeRef . current , onScroll ] ) ;
100118
101- useEffect ( ( ) => {
102- const element = document . getElementById ( 'terminal' ) ;
119+ const scrollToBottom = ( ) => {
120+ if ( ! codeRef . current ) return ;
103121
104- if ( terminalInstance . current && element ) {
105- terminalInstance . current . open ( element ) ;
106- }
107- } , [ ] ) ;
122+ const { clientHeight, scrollHeight } = codeRef . current ;
123+ codeRef . current . scrollTo ( 0 , scrollHeight - clientHeight ) ;
124+ } ;
108125
109126 return (
110127 < div className = { classNames ( styles . logs , className ) } >
@@ -126,7 +143,11 @@ export const Logs: React.FC<IProps> = ({ className, projectName, runName, jobSub
126143 />
127144 ) }
128145
129- < div className = { styles . terminal } id = "terminal" />
146+ < Code className = { styles . terminal } ref = { codeRef } >
147+ { logsData . map ( ( log , i ) => (
148+ < p key = { i } > { log . message } </ p >
149+ ) ) }
150+ </ Code >
130151 </ TextContent >
131152 </ Container >
132153 </ div >
0 commit comments