argo-cd

Форк
0
202 строки · 9.4 Кб
1
import {DataLoader} from 'argo-ui';
2
import * as classNames from 'classnames';
3
import * as React from 'react';
4
import {useEffect, useState, useRef} from 'react';
5
import {bufferTime, delay, retryWhen} from 'rxjs/operators';
6

7
import {LogEntry} from '../../../shared/models';
8
import {services, ViewPreferences} from '../../../shared/services';
9

10
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
11

12
import './pod-logs-viewer.scss';
13
import {CopyLogsButton} from './copy-logs-button';
14
import {DownloadLogsButton} from './download-logs-button';
15
import {ContainerSelector} from './container-selector';
16
import {FollowToggleButton} from './follow-toggle-button';
17
import {ShowPreviousLogsToggleButton} from './show-previous-logs-toggle-button';
18
import {TimestampsToggleButton} from './timestamps-toggle-button';
19
import {DarkModeToggleButton} from './dark-mode-toggle-button';
20
import {FullscreenButton} from './fullscreen-button';
21
import {Spacer} from '../../../shared/components/spacer';
22
import {LogMessageFilter} from './log-message-filter';
23
import {SinceSecondsSelector} from './since-seconds-selector';
24
import {TailSelector} from './tail-selector';
25
import {PodNamesToggleButton} from './pod-names-toggle-button';
26
import {AutoScrollButton} from './auto-scroll-button';
27
import {WrapLinesButton} from './wrap-lines-button';
28
import Ansi from 'ansi-to-react';
29

30
export interface PodLogsProps {
31
    namespace: string;
32
    applicationNamespace: string;
33
    applicationName: string;
34
    podName?: string;
35
    containerName: string;
36
    group?: string;
37
    kind?: string;
38
    name?: string;
39
    timestamp?: string;
40
    containerGroups?: any[];
41
    onClickContainer?: (group: any, i: number, tab: string) => void;
42
}
43

44
// ansi colors, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
45
const red = '\u001b[31m';
46
const green = '\u001b[32m';
47
const yellow = '\u001b[33m';
48
const blue = '\u001b[34m';
49
const magenta = '\u001b[35m';
50
const cyan = '\u001b[36m';
51
const colors = [red, green, yellow, blue, magenta, cyan];
52
const reset = '\u001b[0m';
53
const whiteOnYellow = '\u001b[1m\u001b[43;1m\u001b[37m';
54

55
// cheap string hash function
56
function stringHashCode(str: string) {
57
    let hash = 0;
58
    for (let i = 0; i < str.length; i++) {
59
        // tslint:disable-next-line:no-bitwise
60
        hash = str.charCodeAt(i) + ((hash << 5) - hash);
61
    }
62
    return hash;
63
}
64

65
// ansi color for pod name
66
function podColor(podName: string) {
67
    return colors[stringHashCode(podName) % colors.length];
68
}
69

70
// https://2ality.com/2012/09/empty-regexp.html
71
const matchNothing = /.^/;
72

73
export const PodsLogsViewer = (props: PodLogsProps) => {
74
    const {containerName, onClickContainer, timestamp, containerGroups, applicationName, applicationNamespace, namespace, podName, group, kind, name} = props;
75
    const queryParams = new URLSearchParams(location.search);
76
    const [viewPodNames, setViewPodNames] = useState(queryParams.get('viewPodNames') === 'true');
77
    const [follow, setFollow] = useState(queryParams.get('follow') !== 'false');
78
    const [viewTimestamps, setViewTimestamps] = useState(queryParams.get('viewTimestamps') === 'true');
79
    const [previous, setPreviousLogs] = useState(queryParams.get('showPreviousLogs') === 'true');
80
    const [tail, setTail] = useState<number>(parseInt(queryParams.get('tail'), 10) || 1000);
81
    const [sinceSeconds, setSinceSeconds] = useState(0);
82
    const [filter, setFilter] = useState(queryParams.get('filterText') || '');
83
    const [highlight, setHighlight] = useState<RegExp>(matchNothing);
84
    const [scrollToBottom, setScrollToBottom] = useState(true);
85
    const [logs, setLogs] = useState<LogEntry[]>([]);
86
    const logsContainerRef = useRef(null);
87

88
    useEffect(() => {
89
        if (viewPodNames) {
90
            setViewTimestamps(false);
91
        }
92
    }, [viewPodNames]);
93

94
    useEffect(() => {
95
        // https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
96
        // matchNothing this is chosen instead of empty regexp, because that would match everything and break colored logs
97
        setHighlight(filter === '' ? matchNothing : new RegExp(filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'));
98
    }, [filter]);
99

100
    if (!containerName || containerName === '') {
101
        return <div>Pod does not have container with name {containerName}</div>;
102
    }
103

104
    useEffect(() => setScrollToBottom(true), [follow]);
105

106
    useEffect(() => {
107
        if (scrollToBottom) {
108
            const element = logsContainerRef.current;
109
            if (element) {
110
                element.scrollTop = element.scrollHeight;
111
            }
112
        }
113
    }, [logs, scrollToBottom]);
114

115
    useEffect(() => {
116
        setLogs([]);
117
        const logsSource = services.applications
118
            .getContainerLogs({
119
                applicationName,
120
                appNamespace: applicationNamespace,
121
                namespace,
122
                podName,
123
                resource: {group, kind, name},
124
                containerName,
125
                tail,
126
                follow,
127
                sinceSeconds,
128
                filter,
129
                previous
130
            }) // accumulate log changes and render only once every 100ms to reduce CPU usage
131
            .pipe(bufferTime(100))
132
            .pipe(retryWhen(errors => errors.pipe(delay(500))))
133
            .subscribe(log => setLogs(previousLogs => previousLogs.concat(log)));
134

135
        return () => logsSource.unsubscribe();
136
    }, [applicationName, applicationNamespace, namespace, podName, group, kind, name, containerName, tail, follow, sinceSeconds, filter, previous]);
137

138
    const handleScroll = (event: React.WheelEvent<HTMLDivElement>) => {
139
        if (event.deltaY < 0) setScrollToBottom(false);
140
    };
141

142
    const renderLog = (log: LogEntry, lineNum: number) =>
143
        // show the pod name if there are multiple pods, pad with spaces to align
144
        (viewPodNames ? (lineNum === 0 || logs[lineNum - 1].podName !== log.podName ? podColor(podName) + log.podName + reset : ' '.repeat(log.podName.length)) + ' ' : '') +
145
        // show the timestamp if requested, pad with spaces to align
146
        (viewTimestamps ? (lineNum === 0 || (logs[lineNum - 1].timeStamp !== log.timeStamp ? log.timeStampStr : '').padEnd(30)) + ' ' : '') +
147
        // show the log content, highlight the filter text
148
        log.content?.replace(highlight, (substring: string) => whiteOnYellow + substring + reset);
149
    const logsContent = (width: number, height: number, isWrapped: boolean) => (
150
        <div ref={logsContainerRef} onScroll={handleScroll} style={{width, height, overflow: 'scroll'}}>
151
            {logs.map((log, lineNum) => (
152
                <div key={lineNum} style={{whiteSpace: isWrapped ? 'normal' : 'pre', lineHeight: '16px'}} className='noscroll'>
153
                    <Ansi>{renderLog(log, lineNum)}</Ansi>
154
                </div>
155
            ))}
156
        </div>
157
    );
158

159
    return (
160
        <DataLoader load={() => services.viewPreferences.getPreferences()}>
161
            {(prefs: ViewPreferences) => {
162
                return (
163
                    <React.Fragment>
164
                        <div className='pod-logs-viewer__settings'>
165
                            <span>
166
                                <FollowToggleButton follow={follow} setFollow={setFollow} />
167
                                {follow && <AutoScrollButton scrollToBottom={scrollToBottom} setScrollToBottom={setScrollToBottom} />}
168
                                <ShowPreviousLogsToggleButton setPreviousLogs={setPreviousLogs} showPreviousLogs={previous} />
169
                                <Spacer />
170
                                <ContainerSelector containerGroups={containerGroups} containerName={containerName} onClickContainer={onClickContainer} />
171
                                <Spacer />
172
                                {!follow && (
173
                                    <>
174
                                        <SinceSecondsSelector sinceSeconds={sinceSeconds} setSinceSeconds={n => setSinceSeconds(n)} />
175
                                        <TailSelector tail={tail} setTail={setTail} />
176
                                    </>
177
                                )}
178
                                <LogMessageFilter filterText={filter} setFilterText={setFilter} />
179
                            </span>
180
                            <Spacer />
181
                            <span>
182
                                <WrapLinesButton prefs={prefs} />
183
                                <PodNamesToggleButton viewPodNames={viewPodNames} setViewPodNames={setViewPodNames} />
184
                                <TimestampsToggleButton setViewTimestamps={setViewTimestamps} viewTimestamps={viewTimestamps} timestamp={timestamp} />
185
                                <DarkModeToggleButton prefs={prefs} />
186
                            </span>
187
                            <Spacer />
188
                            <span>
189
                                <CopyLogsButton logs={logs} />
190
                                <DownloadLogsButton {...props} />
191
                                <FullscreenButton {...props} />
192
                            </span>
193
                        </div>
194
                        <div className={classNames('pod-logs-viewer', {'pod-logs-viewer--inverted': prefs.appDetails.darkMode})} onWheel={handleScroll}>
195
                            <AutoSizer>{({width, height}: {width: number; height: number}) => logsContent(width, height, prefs.appDetails.wrapLines)}</AutoSizer>
196
                        </div>
197
                    </React.Fragment>
198
                );
199
            }}
200
        </DataLoader>
201
    );
202
};
203

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.