blob: a4409d2c46429c6b31b59663e27538f66ce4e2b5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AnchorButton, Button, ButtonGroup, Intent, Switch } from '@blueprintjs/core';
import axios from 'axios';
import copy from 'copy-to-clipboard';
import React from 'react';
import { Loader } from '../../components';
import { AppToaster } from '../../singletons/toaster';
import { UrlBaser } from '../../singletons/url-baser';
import { QueryManager, QueryState } from '../../utils';
import './show-log.scss';
function removeFirstPartialLine(log: string): string {
const lines = log.split('\n');
if (lines.length > 1) {
lines.shift();
}
return lines.join('\n');
}
export interface ShowLogProps {
endpoint: string;
downloadFilename?: string;
tailOffset?: number;
status?: string;
}
export interface ShowLogState {
logState: QueryState<string>;
tail: boolean;
}
export class ShowLog extends React.PureComponent<ShowLogProps, ShowLogState> {
static CHECK_INTERVAL = 2500;
private showLogQueryManager: QueryManager<null, string>;
private log = React.createRef<HTMLTextAreaElement>();
private interval: number | undefined;
constructor(props: ShowLogProps, context: any) {
super(props, context);
this.state = {
logState: QueryState.INIT,
tail: true,
};
this.showLogQueryManager = new QueryManager({
processQuery: async () => {
const { endpoint, tailOffset } = this.props;
const resp = await axios.get(endpoint + (tailOffset ? `?offset=-${tailOffset}` : ''));
const data = resp.data;
let logValue = typeof data === 'string' ? data : JSON.stringify(data, undefined, 2);
if (tailOffset) logValue = removeFirstPartialLine(logValue);
return logValue;
},
onStateChange: logState => {
if (logState.data) {
this.scrollToBottomIfNeeded();
}
this.setState({
logState,
});
},
});
}
componentDidMount(): void {
const { status } = this.props;
if (status === 'RUNNING') {
this.addTailer();
}
this.showLogQueryManager.runQuery(null);
}
componentWillUnmount(): void {
this.showLogQueryManager.terminate();
this.removeTailer();
}
private scrollToBottomIfNeeded(): void {
const { tail } = this.state;
if (!tail) return;
const { current } = this.log;
if (current) {
current.scrollTop = current.scrollHeight;
}
}
addTailer() {
if (this.interval) return;
this.interval = setInterval(
() => this.showLogQueryManager.rerunLastQuery(true),
ShowLog.CHECK_INTERVAL,
) as any;
}
removeTailer() {
if (!this.interval) return;
clearInterval(this.interval);
delete this.interval;
}
private handleCheckboxChange = () => {
const { tail } = this.state;
const nextTail = !tail;
this.setState({
tail: nextTail,
});
if (nextTail) {
this.addTailer();
} else {
this.removeTailer();
}
};
render(): JSX.Element {
const { endpoint, downloadFilename, status } = this.props;
const { logState } = this.state;
return (
<div className="show-log">
<div className="top-actions">
{status === 'RUNNING' && (
<Switch
className="tail-log"
label="Tail log"
checked={this.state.tail}
onChange={this.handleCheckboxChange}
/>
)}
<ButtonGroup className="right-buttons">
{downloadFilename && (
<AnchorButton
text="Save"
minimal
download={downloadFilename}
href={UrlBaser.base(endpoint)}
/>
)}
<Button
text="Copy"
minimal
onClick={() => {
copy(logState.data || '', { format: 'text/plain' });
AppToaster.show({
message: 'Log copied to clipboard',
intent: Intent.SUCCESS,
});
}}
/>
<Button
text="View full log"
minimal
onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
/>
</ButtonGroup>
</div>
<div className="main-area">
{logState.loading ? (
<Loader />
) : (
<textarea
className="bp3-input"
readOnly
value={logState.data || logState.getErrorMessage()}
ref={this.log}
/>
)}
</div>
</div>
);
}
}