Skip to content

Commit de1073f

Browse files
committed
[UI] Query logs using descending and show timestamp per log entry #2892
1 parent 8cf079a commit de1073f

8 files changed

Lines changed: 100 additions & 78 deletions

File tree

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: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,55 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useLayoutEffect, 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 { Code, Container, Header, ListEmptyMessage, Loader, TextContent } from 'components';
96

10-
import { useAppSelector } from 'hooks';
117
import { useLazyGetProjectLogsQuery } from 'services/project';
128

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

1511
import { IProps } from './types';
1612

1713
import styles from './styles.module.scss';
1814

1915
const LIMIT_LOG_ROWS = 1000;
16+
const LOADING_SCROLL_GAP = 300;
2017

2118
export 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>

frontend/src/pages/Runs/Details/Logs/styles.module.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
.terminal {
5757
flex-grow: 1;
5858
min-height: 0;
59+
overflow-y: auto;
60+
61+
p {
62+
padding: 0 !important;
63+
}
5964
}
6065

6166
.scroll {

0 commit comments

Comments
 (0)