Skip to content

Commit 5a69fc9

Browse files
dgolovinmohitsuman
andauthored
Log viewer based on React LazyLog (#1491)
* Shows progress for log loading and allows to stop it * Supports searching through the log and filtering matched lines * Kills odo command streaming the log, when log view is closed or streaming is stopped with the button Signed-off-by: Denis Golovin <dgolovin@redhat.com> Co-authored-by: Mohit Suman <mohit.skn@gmail.com>
1 parent c37a741 commit 5a69fc9

File tree

13 files changed

+6041
-952
lines changed

13 files changed

+6041
-952
lines changed

package-lock.json

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

package.json

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@
3838
"scripts": {
3939
"verify": "node ./out/build/verify-tools.js",
4040
"vscode:prepublish": "npm run build && node ./out/build/bundle-tools.js",
41-
"compile": "tsc -p ./",
41+
"compile": "npm-run-all compile:*",
42+
"compile:ext": "tsc -p ./",
43+
"compile:views": "webpack --mode development",
4244
"watch": "tsc -watch -p ./",
43-
"clean": "shx rm -rf out/build out/coverage out/src out/test out/tools out/test-resources",
45+
"watch:views": "webpack --watch --mode development",
46+
"clean": "shx rm -rf out/build out/coverage out/src out/test out/tools out/test-resources out/logViewer",
4447
"lint": "eslint . --ext .ts --quiet",
4548
"lint-fix": "eslint . --ext .ts --fix",
4649
"lint-nic": "eslint . --ext .ts --no-inline-config",
@@ -54,6 +57,8 @@
5457
},
5558
"dependencies": {
5659
"@kubernetes/client-node": "^0.11.1",
60+
"@material-ui/core": "^4.9.10",
61+
"@material-ui/icons": "^4.9.1",
5762
"binary-search": "^1.3.6",
5863
"byline": "^5.0.0",
5964
"fs-extra": "^8.1.0",
@@ -63,10 +68,15 @@
6368
"globby": "^10.0.1",
6469
"got": "^10.6.0",
6570
"hasha": "5.0.0",
71+
"immutable": "^4.0.0-rc.12",
6672
"js-yaml": "^3.13.1",
6773
"mkdirp": "^0.5.1",
6874
"openshift-rest-client": "^4.0.0",
6975
"pify": "^4.0.1",
76+
"react": "^16.13.1",
77+
"react-dom": "^16.13.1",
78+
"react-lazylog": "^4.5.2",
79+
"react-loader-spinner": "^3.1.14",
7080
"rxjs": "^6.5.3",
7181
"semver": "^6.3.0",
7282
"shelljs": "^0.8.3",
@@ -86,8 +96,11 @@
8696
"@types/js-yaml": "^3.12.2",
8797
"@types/mkdirp": "^0.5.2",
8898
"@types/mocha": "^5.2.7",
89-
"@types/node": "^12.12.29",
99+
"@types/node": "^12.12.35",
90100
"@types/pify": "^3.0.2",
101+
"@types/react": "^16.9.32",
102+
"@types/react-dom": "^16.9.6",
103+
"@types/react-lazylog": "^4.4.0",
91104
"@types/shelljs": "^0.8.6",
92105
"@types/sinon": "^5.0.7",
93106
"@types/sinon-chai": "^3.2.3",
@@ -101,6 +114,7 @@
101114
"bufferutil": "^4.0.1",
102115
"chai": "^4.2.0",
103116
"codecov": "^3.6.5",
117+
"css-loader": "^3.5.1",
104118
"decache": "^4.5.1",
105119
"eslint": "5.16.0",
106120
"eslint-config-airbnb-base": "^14.0.0",
@@ -116,20 +130,25 @@
116130
"leasot": "^10.1.0",
117131
"mocha": "^6.2.2",
118132
"mocha-jenkins-reporter": "^0.4.2",
133+
"npm-run-all": "^4.1.5",
119134
"prettier": "^1.19.1",
120135
"proxyquire": "^2.1.3",
121136
"remap-istanbul": "^0.13.0",
122137
"shx": "^0.3.2",
123138
"sinon": "^7.5.0",
124139
"sinon-chai": "^3.5.0",
125140
"source-map-support": "^0.5.16",
141+
"style-loader": "^1.1.3",
126142
"tmp": "0.1.0",
143+
"ts-loader": "^6.2.2",
127144
"tslint": "^5.20.1",
128145
"typescript": "^3.8.3",
129146
"utf-8-validate": "^5.0.2",
130147
"vscode-extension-tester": "^2.4.0",
131148
"vscode-test": "^1.2.3",
132-
"walker": "^1.0.7"
149+
"walker": "^1.0.7",
150+
"webpack": "^4.35.2",
151+
"webpack-cli": "^3.3.5"
133152
},
134153
"activationEvents": [
135154
"onView:openshiftProjectExplorer",

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface CliExitData {
1515

1616
export interface Cli {
1717
execute(cmd: string, opts?: cp.ExecOptions): Promise<CliExitData>;
18+
spawn(cmd: string, params: string[], opts: cp.SpawnOptions): cp.ChildProcess;
1819
}
1920

2021
export interface OdoChannel {
@@ -94,4 +95,8 @@ export class CliChannel implements Cli {
9495
});
9596
});
9697
}
98+
99+
spawn(cmd: string, params: string[], opts: cp.SpawnOptions = {cwd: undefined, env: process.env}): cp.ChildProcess {
100+
return cp.spawn(cmd, params, opts);
101+
}
97102
}

src/odo.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ export class OpenShiftObjectImpl implements OpenShiftObject {
397397
segments.splice(0, 0, parent ? parent.getName() : this.getName());
398398
parent = parent ? parent.getParent() : this.getParent();
399399
} while (parent);
400-
this.explorerPath = path.join(...segments);
400+
this.explorerPath = segments.join('/');
401401
}
402402
return this.explorerPath;
403403
}
@@ -490,6 +490,7 @@ export interface Odo {
490490
getServiceTemplatePlans(svc: string): Promise<string[]>;
491491
getServices(application: OpenShiftObject): Promise<OpenShiftObject[]>;
492492
execute(command: string, cwd?: string, fail?: boolean): Promise<cliInstance.CliExitData>;
493+
spawn(command: string, params: string[], cwd?: string): Promise<ChildProcess>;
493494
executeInTerminal(command: string, cwd?: string): Promise<void>;
494495
requireLogin(): Promise<boolean>;
495496
clearCache?(): void;
@@ -934,6 +935,15 @@ export class OdoImpl implements Odo {
934935
).then(async (result) => result.error && fail ? Promise.reject(result.error) : result).catch((err) => fail ? Promise.reject(err) : Promise.resolve({error: null, stdout: '', stderr: ''}));
935936
}
936937

938+
public async spawn(command: string, params: string[], cwd?: string): Promise<ChildProcess> {
939+
const toolLocation = await ToolsConfig.detect(command);
940+
const defaultOptions = {
941+
cwd,
942+
env: process.env
943+
};
944+
return OdoImpl.cli.spawn(toolLocation, params, defaultOptions);
945+
}
946+
937947
public async requireLogin(): Promise<boolean> {
938948
const result: cliInstance.CliExitData = await this.execute(Command.printOdoVersionAndProjects(), process.cwd(), false);
939949
return this.odoLoginMessages.some((msg) => result.stderr.includes(msg));

src/openshift/component.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import { Refs, Ref, Type } from '../util/refs';
1616
import { Delayer } from '../util/async';
1717
import { Platform } from '../util/platform';
1818
import { selectWorkspaceFolder } from '../util/workspace';
19+
import * as consts from '../util/constants';
1920
import { ToolsConfig } from '../tools';
2021
import { Catalog } from './catalog';
22+
import LogViewLoader from '../view/log/LogViewLoader';
2123

2224
import path = require('path');
2325
import globby = require('globby');
26+
import treeKill = require('tree-kill');
2427

2528
const waitPort = require('wait-port');
2629
const getPort = require('get-port');
@@ -157,7 +160,25 @@ export class Component extends OpenShiftItem {
157160
(value: OpenShiftObject) => value.contextValue === ContextType.COMPONENT_PUSHED
158161
);
159162
if (!component) return null;
160-
Component.odo.executeInTerminal(Command.showLog(component.getParent().getParent().getName(), component.getParent().getName(), component.getName()), component.contextPath.fsPath);
163+
const cmd = Command.showLog(component.getParent().getParent().getName(), component.getParent().getName(), component.getName());
164+
const panel = LogViewLoader.loadView(extensions.getExtension(consts.ExtenisonID).extensionPath, `${component.path} Log`, cmd);
165+
const [tool, ...params] = cmd.split(' ');
166+
const process = await Component.odo.spawn(tool, params, component.contextPath.fsPath);
167+
process.stdout.on('data', (data) => {
168+
panel.webview.postMessage({action: 'add', data: `${data}`.trim().split('\n')});
169+
}).on('end', (data) => {
170+
panel.webview.postMessage({action: 'finished'});
171+
});
172+
const recieveDisposable = panel.webview.onDidReceiveMessage((event) => {
173+
if (event.action === 'stop') {
174+
treeKill(process.pid);
175+
recieveDisposable.dispose();
176+
}
177+
})
178+
const disposable = panel.onDidDispose(()=> {
179+
treeKill(process.pid);
180+
disposable.dispose();
181+
});
161182
}
162183

163184
static async followLog(context: OpenShiftObject): Promise<string> {
@@ -168,7 +189,25 @@ export class Component extends OpenShiftItem {
168189
(value: OpenShiftObject) => value.contextValue === ContextType.COMPONENT_PUSHED
169190
);
170191
if (!component) return null;
171-
Component.odo.executeInTerminal(Command.showLogAndFollow(component.getParent().getParent().getName(), component.getParent().getName(), component.getName()), component.contextPath.fsPath);
192+
const cmd = Command.showLogAndFollow(component.getParent().getParent().getName(), component.getParent().getName(), component.getName());
193+
const panel = LogViewLoader.loadView(extensions.getExtension(consts.ExtenisonID).extensionPath, `${component.path} Follow Log`, cmd);
194+
const [tool, ...params] = cmd.split(' ');
195+
const process = await Component.odo.spawn(tool, params, component.contextPath.fsPath);
196+
process.stdout.on('data', (data) => {
197+
panel.webview.postMessage({action: 'add', data: `${data}`.trim().split('\n')});
198+
}).on('close', ()=>{
199+
panel.webview.postMessage({action: 'finished'});
200+
});
201+
const recieveDisposable = panel.webview.onDidReceiveMessage((event) => {
202+
if (event.action === 'stop') {
203+
treeKill(process.pid);
204+
recieveDisposable.dispose();
205+
}
206+
})
207+
const disposable = panel.onDidDispose(()=> {
208+
treeKill(process.pid);
209+
disposable.dispose();
210+
});
172211
}
173212

174213
private static async getLinkData(component: OpenShiftObject): Promise<any> {

src/util/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export enum GlyphChars {
1212
NoContext = '\u25CB',
1313
LocallyDeleted = '\u2297',
1414
}
15+
16+
export const ExtenisonID = 'redhat.vscode-openshift-connector';

src/view/log/LogViewLoader.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
import * as vscode from 'vscode';
6+
import * as path from 'path';
7+
8+
export default class LogViewLoader {
9+
static loadView(extensionPath: string, title: string, cmdText: string): vscode.WebviewPanel {
10+
const localResourceRoot = vscode.Uri.file(path.join(extensionPath, 'out', 'logViewer'));
11+
12+
const panel = vscode.window.createWebviewPanel('logView', title, vscode.ViewColumn.One, {
13+
enableScripts: true,
14+
localResourceRoots: [localResourceRoot],
15+
retainContextWhenHidden: true
16+
});
17+
panel.iconPath = vscode.Uri.file(path.join(extensionPath, "images/context/cluster-node.png"));
18+
19+
// TODO: When webview is going to be ready?
20+
panel.webview.html = LogViewLoader.getWebviewContent(extensionPath, cmdText);
21+
return panel;
22+
}
23+
24+
private static getWebviewContent(extensionPath: string, cmdText: string): string {
25+
// Local path to main script run in the webview
26+
const reactAppPathOnDisk = vscode.Uri.file(
27+
path.join(extensionPath, 'out', 'logViewer', 'logViewer.js'),
28+
);
29+
const reactAppUri = reactAppPathOnDisk.with({ scheme: 'vscode-resource' });
30+
31+
return `<!DOCTYPE html>
32+
<html lang="en">
33+
<head>
34+
<meta charset="UTF-8">
35+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
36+
<title>Config View</title>
37+
<meta http-equiv="Content-Security-Policy"
38+
content="connect-src *;
39+
default-src 'none';
40+
img-src https:;
41+
script-src 'unsafe-eval' 'unsafe-inline' vscode-resource:;
42+
style-src vscode-resource: 'unsafe-inline';">
43+
<script>
44+
window.acquireVsCodeApi = acquireVsCodeApi;
45+
window.cmdText = "${cmdText}";
46+
</script>
47+
<style>
48+
html,
49+
body {
50+
padding: 0;
51+
overflow: hidden;
52+
}
53+
54+
.box {
55+
display: flex;
56+
flex-flow: column;
57+
position: absolute;
58+
top: 0px;
59+
bottom: 1px;
60+
left: 0px;
61+
right: 0px;
62+
}
63+
64+
.box .row.header {
65+
flex: 0 1 auto;
66+
/* The above is shorthand for:
67+
flex-grow: 0,
68+
flex-shrink: 1,
69+
flex-basis: auto
70+
*/
71+
}
72+
73+
.box .row.content {
74+
flex: 1 1 auto;
75+
}
76+
</style>
77+
</head>
78+
<div class="box">
79+
<div class="row header" id="spinner">
80+
</div>
81+
<div class="row content" id="root">
82+
</div>
83+
</div>
84+
<script src="${reactAppUri}"></script>
85+
</body>
86+
</html>`;
87+
}
88+
}

src/view/log/app/index.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
6+
import * as React from "react";
7+
import * as ReactDOM from "react-dom";
8+
import Spinner from './spinner'
9+
import Log from './log';
10+
11+
declare global {
12+
interface Window {
13+
cmdText: string;
14+
}
15+
}
16+
17+
ReactDOM.render(
18+
<Spinner/>,
19+
document.getElementById("spinner")
20+
)
21+
22+
ReactDOM.render(
23+
React.createElement(Log, {text: window.cmdText, enableSearch: true}),
24+
document.getElementById("root"),
25+
);

src/view/log/app/log.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
import { LazyLog } from 'react-lazylog';
6+
import { List } from 'immutable'
7+
8+
declare global {
9+
interface Window {
10+
cmdText: string;
11+
}
12+
}
13+
14+
export default class Log extends LazyLog {
15+
constructor(props: any) {
16+
super(props);
17+
const enc = new TextEncoder();
18+
let wholeLog = `${window.cmdText}\n`;
19+
window.addEventListener('message', event => {
20+
const message: {action: string, data: string[]} = event.data; // The JSON data our extension sent
21+
switch (message.action) {
22+
case 'add': {
23+
const lastIndex = message.data.length - 1;
24+
message.data.forEach((element:string, index: number)=> {
25+
if (index === lastIndex) {
26+
wholeLog = wholeLog.concat(`${element}`);
27+
} else {
28+
wholeLog = wholeLog.concat(`${element}\n`);
29+
}
30+
});
31+
const encodedLines = message.data.map((line) => enc.encode(line));
32+
(this as any).handleUpdate({lines: List(encodedLines), encodedLog: enc.encode(wholeLog)});
33+
break;
34+
}
35+
default:
36+
break;
37+
}
38+
});
39+
}
40+
}

0 commit comments

Comments
 (0)