Skip to content

Commit cc77d48

Browse files
[UI] Query logs using descending (#2915)
- [x] Allowed `descending` in `PollLogsRequest` - [x] Supported `descending` in `FileLogStorage` (implemented an algorithm that reads lines from the end of t he file) - [x] Updated `CloudWatchLogStorage` to support `MAX_RETRIES` (to skip empty pages) - [x] Re-implemented to make `_read_lines_reversed` fast regardless of `start_offset` --------- Co-authored-by: Oleg Vavilov <vavilovolegik@gmail.com>
1 parent e403418 commit cc77d48

File tree

12 files changed

+1053
-220
lines changed

12 files changed

+1053
-220
lines changed

frontend/package-lock.json

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@
105105
"@cloudscape-design/global-styles": "^1.0.33",
106106
"@hookform/resolvers": "^2.9.10",
107107
"@reduxjs/toolkit": "^1.9.1",
108-
"@xterm/addon-fit": "^0.10.0",
109-
"@xterm/xterm": "^5.5.0",
110108
"ace-builds": "^1.36.3",
111109
"classnames": "^2.5.1",
112110
"css-minimizer-webpack-plugin": "^4.2.2",
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { forwardRef } from 'react';
22
import classNames from 'classnames';
33
import Box from '@cloudscape-design/components/box';
44

@@ -8,12 +8,12 @@ export interface Props extends React.PropsWithChildren {
88
className?: string;
99
}
1010

11-
export const Code: React.FC<Props> = ({ children, className }) => {
11+
export const Code = forwardRef<HTMLDivElement, Props>(({ children, className }, ref) => {
1212
return (
13-
<div className={classNames(styles.code, className)}>
13+
<div ref={ref} className={classNames(styles.code, className)}>
1414
<Box variant="code" color="text-status-inactive">
1515
{children}
1616
</Box>
1717
</div>
1818
);
19-
};
19+
});

frontend/src/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export { default as PropertyFilter } from '@cloudscape-design/components/propert
5656
export type { PropertyFilterProps } from '@cloudscape-design/components/property-filter';
5757
export type { LineChartProps } from '@cloudscape-design/components/line-chart/interfaces';
5858
export type { ModalProps } from '@cloudscape-design/components/modal';
59-
export type { TilesProps } from '@cloudscape-design/components/tiles';
6059

6160
// custom components
6261
export { NavigateLink } from './NavigateLink';

frontend/src/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import 'ace-builds/css/ace.css';
1212
import 'ace-builds/css/theme/cloud_editor.css';
1313
import 'ace-builds/css/theme/cloud_editor_dark.css';
1414
import 'assets/css/index.css';
15-
import '@xterm/xterm/css/xterm.css';
1615

1716
import 'locale';
1817

frontend/src/pages/Runs/Details/Logs/helpers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,18 @@ export const getJobSubmissionId = (run?: IRun): string | undefined => {
77

88
return lastJob.job_submissions[lastJob.job_submissions.length - 1]?.id;
99
};
10+
11+
export const decodeLogs = (logs: ILogItem[]): ILogItem[] => {
12+
return logs.map((log: ILogItem) => {
13+
let { message } = log;
14+
15+
try {
16+
message = atob(message);
17+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
18+
} catch (e) {
19+
return log;
20+
}
21+
22+
return { ...log, message };
23+
});
24+
};

frontend/src/pages/Runs/Details/Logs/index.tsx

Lines changed: 130 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,66 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import 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 { Box, Button, Code, Container, Header, ListEmptyMessage, Loader, TextContent } from 'components';
96

10-
import { useAppSelector } from 'hooks';
7+
import { useLocalStorageState } from 'hooks/useLocalStorageState';
118
import { useLazyGetProjectLogsQuery } from 'services/project';
129

13-
import { selectSystemMode } from 'App/slice';
10+
import { decodeLogs } from './helpers';
1411

1512
import { IProps } from './types';
1613

1714
import styles from './styles.module.scss';
1815

19-
const LIMIT_LOG_ROWS = 1000;
16+
const LIMIT_LOG_ROWS = 100;
17+
const LOADING_SCROLL_GAP = 300;
2018

2119
export const Logs: React.FC<IProps> = ({ className, projectName, runName, jobSubmissionId }) => {
2220
const { t } = useTranslation();
23-
const appliedTheme = useAppSelector(selectSystemMode);
21+
const codeRef = useRef<HTMLDivElement>(null);
22+
const nextTokenRef = useRef<string | undefined>(undefined);
23+
const scrollPositionByBottom = useRef<number>(0);
2424

25-
const terminalInstance = useRef<Terminal>(new Terminal({ scrollback: 10000000 }));
26-
const fitAddonInstance = useRef<FitAddon>(new FitAddon());
2725
const [logsData, setLogsData] = useState<ILogItem[]>([]);
2826
const [isLoading, setIsLoading] = useState(false);
29-
3027
const [getProjectLogs] = useLazyGetProjectLogsQuery();
28+
const [isEnabledDecoding, setIsEnabledDecoding] = useLocalStorageState('enable-encode-logs', false);
29+
// const [isShowTimestamp, setIsShowTimestamp] = useLocalStorageState('enable-showing-timestamp-logs', false);
30+
31+
const logsForView = useMemo(() => {
32+
if (isEnabledDecoding) {
33+
return decodeLogs(logsData);
34+
}
35+
36+
return logsData;
37+
}, [logsData, isEnabledDecoding]);
38+
39+
const saveScrollPositionByBottom = () => {
40+
if (!codeRef.current) return;
3141

32-
const writeDataToTerminal = (logs: ILogItem[]) => {
33-
logs.forEach((logItem) => {
34-
terminalInstance.current.write(logItem.message.replace(/(?<!\r)\n/g, '\r\n'));
35-
});
42+
const { clientHeight, scrollHeight, scrollTop } = codeRef.current;
43+
scrollPositionByBottom.current = scrollHeight - clientHeight - scrollTop;
44+
};
45+
46+
const restoreScrollPositionByBottom = () => {
47+
if (!codeRef.current) return;
3648

37-
fitAddonInstance.current.fit();
49+
const { clientHeight, scrollHeight } = codeRef.current;
50+
codeRef.current.scrollTo(0, scrollHeight - clientHeight - scrollPositionByBottom.current);
3851
};
3952

40-
const getNextLogItems = (nextToken?: string) => {
53+
const checkNeedMoreLoadingData = () => {
54+
if (!codeRef.current) return;
55+
56+
const { clientHeight, scrollHeight } = codeRef.current;
57+
58+
if (scrollHeight - clientHeight <= LOADING_SCROLL_GAP) {
59+
getLogItems();
60+
}
61+
};
62+
63+
const getLogItems = (nextToken?: string) => {
4164
setIsLoading(true);
4265

4366
if (!jobSubmissionId) {
@@ -47,86 +70,131 @@ export const Logs: React.FC<IProps> = ({ className, projectName, runName, jobSub
4770
getProjectLogs({
4871
project_name: projectName,
4972
run_name: runName,
50-
descending: false,
51-
job_submission_id: jobSubmissionId ?? '',
73+
descending: true,
74+
job_submission_id: jobSubmissionId,
5275
next_token: nextToken,
5376
limit: LIMIT_LOG_ROWS,
5477
})
5578
.unwrap()
5679
.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-
}
80+
saveScrollPositionByBottom();
81+
const reversed = response.logs.toReversed();
82+
setLogsData((old) => [...reversed, ...old]);
83+
nextTokenRef.current = response.next_token;
84+
setIsLoading(false);
6685
})
6786
.catch(() => setIsLoading(false));
6887
};
6988

89+
const getNextLogItems = () => {
90+
if (nextTokenRef.current) {
91+
getLogItems(nextTokenRef.current);
92+
}
93+
};
94+
95+
const toggleDecodeLogs = () => {
96+
saveScrollPositionByBottom();
97+
setIsEnabledDecoding(!isEnabledDecoding);
98+
};
99+
70100
useEffect(() => {
71-
if (appliedTheme === Mode.Light) {
72-
terminalInstance.current.options.theme = {
73-
foreground: '#000716',
74-
background: '#ffffff',
75-
selectionBackground: '#B4D5FE',
76-
};
101+
getLogItems();
102+
}, []);
103+
104+
useLayoutEffect(() => {
105+
if (logsForView.length && logsForView.length <= LIMIT_LOG_ROWS) {
106+
scrollToBottom();
77107
} else {
78-
terminalInstance.current.options.theme = {
79-
foreground: '#b6bec9',
80-
background: '#161d26',
81-
};
108+
restoreScrollPositionByBottom();
82109
}
83-
}, [appliedTheme]);
84110

85-
useEffect(() => {
86-
terminalInstance.current.loadAddon(fitAddonInstance.current);
111+
if (logsForView.length) checkNeedMoreLoadingData();
112+
}, [logsForView]);
87113

88-
getNextLogItems();
114+
const onScroll = useCallback<EventListener>(
115+
(event) => {
116+
const element = event.target as HTMLDivElement;
89117

90-
const onResize = () => {
91-
fitAddonInstance.current.fit();
92-
};
118+
if (element.scrollTop <= LOADING_SCROLL_GAP && !isLoading) {
119+
getNextLogItems();
120+
}
121+
},
122+
[isLoading, logsForView],
123+
);
124+
125+
useEffect(() => {
126+
if (!codeRef.current) return;
93127

94-
window.addEventListener('resize', onResize);
128+
codeRef.current.addEventListener('scroll', onScroll);
95129

96130
return () => {
97-
window.removeEventListener('resize', onResize);
131+
if (codeRef.current) codeRef.current.removeEventListener('scroll', onScroll);
98132
};
99-
}, []);
133+
}, [codeRef.current, onScroll]);
100134

101-
useEffect(() => {
102-
const element = document.getElementById('terminal');
135+
const scrollToBottom = () => {
136+
if (!codeRef.current) return;
103137

104-
if (terminalInstance.current && element) {
105-
terminalInstance.current.open(element);
106-
}
107-
}, []);
138+
const { clientHeight, scrollHeight } = codeRef.current;
139+
codeRef.current.scrollTo(0, scrollHeight - clientHeight);
140+
};
108141

109142
return (
110143
<div className={classNames(styles.logs, className)}>
111144
<Container
112145
header={
113-
<Header variant="h2">
114-
<div className={styles.headerContainer}>
115-
{t('projects.run.log')}
116-
<Loader show={isLoading} padding={'n'} className={classNames(styles.loader)} loadingText={''} />
146+
<div className={styles.headerContainer}>
147+
<div className={styles.headerTitle}>
148+
<Header variant="h2">{t('projects.run.log')}</Header>
117149
</div>
118-
</Header>
150+
151+
<Loader
152+
show={isLoading && Boolean(logsForView.length)}
153+
padding={'n'}
154+
className={styles.loader}
155+
loadingText={''}
156+
/>
157+
158+
<div className={styles.switchers}>
159+
<Box>
160+
<Button
161+
ariaLabel="Legacy mode"
162+
formAction="none"
163+
iconName="gen-ai"
164+
variant={isEnabledDecoding ? 'primary' : 'icon'}
165+
onClick={toggleDecodeLogs}
166+
/>
167+
</Box>
168+
169+
{/*<Box>*/}
170+
{/* <Toggle onChange={({ detail }) => setIsShowTimestamp(detail.checked)} checked={isShowTimestamp}>*/}
171+
{/* Show timestamp*/}
172+
{/* </Toggle>*/}
173+
{/*</Box>*/}
174+
</div>
175+
</div>
119176
}
120177
>
121178
<TextContent>
122-
{!isLoading && !logsData.length && (
179+
{!isLoading && !logsForView.length && (
123180
<ListEmptyMessage
124181
title={t('projects.run.log_empty_message_title')}
125182
message={t('projects.run.log_empty_message_text')}
126183
/>
127184
)}
128185

129-
<div className={styles.terminal} id="terminal" />
186+
{!logsForView.length && <Loader show={isLoading} className={styles.mainLoader} />}
187+
188+
{Boolean(logsForView.length) && (
189+
<Code className={styles.terminal} ref={codeRef}>
190+
{logsForView.map((log, i) => (
191+
<p key={i}>
192+
{/*{isShowTimestamp && <span className={styles.timestamp}>{log.timestamp}</span>}*/}
193+
{log.message}
194+
</p>
195+
))}
196+
</Code>
197+
)}
130198
</TextContent>
131199
</Container>
132200
</div>

0 commit comments

Comments
 (0)