This repository has been archived by the owner on Sep 4, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5d9aa29
Showing
10 changed files
with
1,390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
|
||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
|
||
# Coverage | ||
lib-cov | ||
.coverage | ||
.nyc_output | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# build directory | ||
build | ||
|
||
# Dependency directories | ||
node_modules | ||
jspm_packages | ||
bower_components | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional REPL history | ||
.node_repl_history |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"name": "unified-logviewer", | ||
"private": true, | ||
"version": "1.0.0", | ||
"description": "Unified LogViewer", | ||
"license": "MPL-2.0", | ||
"config": { | ||
"entry": "src/index.js", | ||
"html": { | ||
"title": "Unified LogViewer", | ||
"description": "Unified LogViewer", | ||
"author": "Eli Perelman" | ||
} | ||
}, | ||
"scripts": { | ||
"build": "neo build", | ||
"start": "neo start --port 4000" | ||
}, | ||
"keywords": [], | ||
"dependencies": { | ||
"immutable": "3.8.1", | ||
"lodash.throttle": "4.1.0", | ||
"react": "15.3.0", | ||
"react-addons-shallow-compare": "15.3.0", | ||
"react-dom": "15.3.0", | ||
"react-waypoint": "3.1.1" | ||
}, | ||
"devDependencies": { | ||
"mozilla-neo": "2.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
|
||
const Cog = ({ width = 35, height = 35, ...props }) => ( | ||
<svg width={width} height={height} {...props} viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"> | ||
<path d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z" /> | ||
</svg> | ||
); | ||
|
||
Cog.displayName = 'Cog'; | ||
|
||
export default Cog; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
import React from 'react'; | ||
import { List } from 'immutable'; | ||
import Waypoint from 'react-waypoint'; | ||
import shallowCompare from 'react-addons-shallow-compare'; | ||
import querystring from 'querystring'; | ||
import parse from './vendor/ansi-parse'; | ||
import Cog from './Cog'; | ||
import Spinner from './Spinner'; | ||
|
||
const BLOCK = 1024 * 64; | ||
const CLEAR_ANSI = /(?:\033)(?:\[0?c|\[[0356]n|\[7[lh]|\[\?25[lh]|\(B|H|\[(?:\d+(;\d+){,2})?G|\[(?:[12])?[JK]|[DM]|\[0K)/gm; | ||
const DECODER = new TextDecoder('utf-8'); | ||
|
||
export default React.createClass({ | ||
displayName: 'LogViewer', | ||
|
||
// Very hackish, but improves performance by not performing DOM lookup to find rendered indices, | ||
// so track this as chunks are rendered | ||
lineOffset: 0, | ||
|
||
getInitialState() { | ||
return { | ||
url: '', | ||
isLoading: true, | ||
chunks: List(), | ||
nextChunk: 0, | ||
chunkPartial: null, | ||
container: null, | ||
totalChunks: 1, | ||
error: false, | ||
toolbarOpen: false, | ||
showLineNumbers: true, | ||
wrapLines: true, | ||
highlightStart: null, | ||
highlightEnd: null | ||
}; | ||
}, | ||
|
||
componentWillMount() { | ||
const { | ||
url, | ||
highlightStart = null, | ||
highlightEnd = null, | ||
wrapLines = true, | ||
showLineNumbers = true | ||
} = querystring.parse(location.search.substr(1)); | ||
|
||
this.setState({ | ||
url, | ||
wrapLines, | ||
highlightStart, | ||
highlightEnd, | ||
showLineNumbers | ||
}); | ||
|
||
window.addEventListener('message', (e) => e.data && typeof e.data === 'object' && this.setState(e.data)); | ||
}, | ||
|
||
componentWillUpdate(nextProps, nextState) { | ||
const qs = querystring.stringify({ | ||
url: nextState.url, | ||
highlightStart: nextState.highlightStart, | ||
highlightEnd: nextState.highlightEnd, | ||
wrapLines: nextState.wrapLines, | ||
showLineNumbers: nextState.showLineNumbers | ||
}); | ||
|
||
history.pushState(null, '', `${location.origin}${location.pathname}?${qs}`); | ||
}, | ||
|
||
shouldComponentUpdate(nextProps, nextState) { | ||
return shallowCompare(this, nextProps, nextState); | ||
}, | ||
|
||
componentDidMount() { | ||
this.request(); | ||
}, | ||
|
||
request() { | ||
const { url } = this.state; | ||
|
||
if (!url) { | ||
return this.setState({ error: true }); | ||
} | ||
|
||
const xhr = new XMLHttpRequest(); | ||
|
||
xhr.open('GET', url); | ||
xhr.overrideMimeType('text/plain; charset=utf-8'); | ||
xhr.responseType = 'arraybuffer'; | ||
|
||
xhr.addEventListener('error', () => { | ||
this.setState({ error: true }); | ||
}); | ||
|
||
xhr.addEventListener('load', () => { | ||
const container = new Uint8Array(xhr.response); | ||
|
||
this.setState({ | ||
container, | ||
isLoading: false, | ||
totalChunks: Math.ceil(container.length / BLOCK) | ||
}); | ||
}); | ||
|
||
xhr.send(); | ||
}, | ||
|
||
decodeParts(slice) { | ||
return DECODER | ||
.decode(new DataView(slice.buffer)) | ||
.replace(/\033\[1000D/gm, '\r') | ||
.replace(/\r+\n/gm, '\n') | ||
.split(/^/gm); | ||
}, | ||
|
||
cleanChunk(chunk) { | ||
return chunk.map(string => parse(string.replace(CLEAR_ANSI, ''))) | ||
}, | ||
|
||
addChunk() { | ||
const index = this.state.nextChunk; | ||
const start = index * BLOCK; | ||
const end = Math.min((index + 1) * BLOCK, this.state.container.length); | ||
const slice = this.state.container.slice(start, end); | ||
const chunk = this.decodeParts(slice); | ||
|
||
if (this.state.chunkPartial) { | ||
chunk[0] = this.state.chunkPartial + chunk[0]; | ||
} | ||
|
||
if (this.state.totalChunks === index + 1) { | ||
const parts = this.cleanChunk(chunk); | ||
|
||
return this.setState({ | ||
fullyLoaded: true, | ||
chunks: this.state.chunks.push(parts) | ||
}); | ||
} | ||
|
||
const chunkPartial = chunk.pop(); | ||
const parts = this.cleanChunk(chunk); | ||
|
||
this.setState({ | ||
chunkPartial, | ||
nextChunk: Math.min(this.state.nextChunk + 1, this.state.totalChunks - 1), | ||
chunks: this.state.chunks.push(parts) | ||
}); | ||
}, | ||
|
||
getClassName(part) { | ||
const colors = []; | ||
|
||
if (part.foreground) { | ||
colors.push(part.foreground); | ||
} | ||
|
||
if (part.background) { | ||
colors.push(`bg-${part.background}`); | ||
} | ||
|
||
if (part.bold) { | ||
colors.push('bold'); | ||
} | ||
|
||
if (part.italic) { | ||
colors.push('italic'); | ||
} | ||
|
||
if (part.underline) { | ||
colors.push('underline'); | ||
} | ||
|
||
return colors.join(' '); | ||
}, | ||
|
||
renderChunk(chunk, index, chunks) { | ||
const { highlightStart, highlightEnd } = this.state; | ||
const offset = index !== 0 ? | ||
chunks.get(index - 1).length + this.lineOffset : | ||
1; | ||
|
||
this.lineOffset = offset; | ||
|
||
return chunk | ||
.map((parts, key) => { | ||
const line = key + offset; | ||
const className = line >= highlightStart && line <= highlightEnd ? 'highlight' : ''; | ||
|
||
return ( | ||
<p key={key} data-chunk={index} data-line={line} className={className}> | ||
<a id={line} /> | ||
{parts.map((part, key) => { | ||
const className = this.getClassName(part); | ||
|
||
return className ? | ||
<span key={key} className={className}>{part.text}</span> : | ||
<span key={key}>{part.text}</span>; | ||
})} | ||
</p> | ||
) | ||
}); | ||
}, | ||
|
||
toggleToolbar(e, open) { | ||
e.stopPropagation(); | ||
|
||
if (open != null && open !== this.state.toolbarOpen) { | ||
return this.setState({ toolbarOpen: open }); | ||
} | ||
|
||
this.setState({ toolbarOpen: !this.state.toolbarOpen }); | ||
}, | ||
|
||
toggleLineNumbers() { | ||
this.setState({ showLineNumbers: !this.state.showLineNumbers }); | ||
}, | ||
|
||
toggleWrapLines() { | ||
this.setState({ wrapLines: !this.state.wrapLines }); | ||
}, | ||
|
||
highlight(e) { | ||
const a = e.target; | ||
const lineNumber = parseInt(a.getAttribute('id')); | ||
const { highlightStart } = this.state; | ||
|
||
if (lineNumber === highlightStart) { | ||
this.setState({ highlightStart: null, highlightEnd: null }) | ||
} else if (!e.shiftKey || !highlightStart) { | ||
this.setState({ | ||
highlightStart: lineNumber, | ||
highlightEnd: lineNumber | ||
}); | ||
} else if (lineNumber > highlightStart) { | ||
this.setState({ highlightEnd: lineNumber }); | ||
} else { | ||
this.setState({ highlightStart: lineNumber }); | ||
} | ||
}, | ||
|
||
handleDelegation(e) { | ||
if (e.target.tagName === 'A') { | ||
this.highlight(e); | ||
} else if (this.state.toolbarOpen) { | ||
this.toggleToolbar(e, false); | ||
} | ||
}, | ||
|
||
render() { | ||
const { chunks, isLoading, toolbarOpen, showLineNumbers, wrapLines, fullyLoaded } = this.state; | ||
|
||
if (isLoading) { | ||
return ( | ||
<div> | ||
<h1 className="loading"> | ||
<i><Spinner className="spinner" /></i> Loading... | ||
</h1> | ||
</div> | ||
); | ||
} | ||
|
||
let className = 'ansi'; | ||
|
||
if (showLineNumbers) { | ||
className += ' show-line-numbers'; | ||
} | ||
|
||
if (wrapLines) { | ||
className += ' wrap-lines'; | ||
} | ||
|
||
return ( | ||
<div> | ||
<div id="log-container"> | ||
<pre id="log" className={className} onClick={this.handleDelegation}> | ||
{!isLoading && chunks.map(this.renderChunk)} | ||
{!isLoading && !fullyLoaded && <Waypoint onEnter={this.addChunk} threshold={0.2} />} | ||
</pre> | ||
</div> | ||
|
||
<div id="toolbar" className={toolbarOpen ? 'open' : ''}> | ||
<div className="cog-wrapper" onClick={this.toggleToolbar}> | ||
<Cog className="cog" width={20} height={20} /> | ||
<header>Settings</header> | ||
</div> | ||
<menu onClick={(e) => e.stopPropagation()}> | ||
<ul> | ||
<li><input type="checkbox" checked={showLineNumbers} onChange={this.toggleLineNumbers} /> Show Line Numbers</li> | ||
<li><input type="checkbox" checked={wrapLines} onChange={this.toggleWrapLines} /> Wrap Lines</li> | ||
</ul> | ||
</menu> | ||
</div> | ||
</div> | ||
) | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
|
||
const Spinner = ({ width = 35, height = 35, ...props }) => ( | ||
<svg width={width} height={height} {...props} viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"> | ||
<path d="M1760 896q0 176-68.5 336t-184 275.5-275.5 184-336 68.5-336-68.5-275.5-184-184-275.5-68.5-336q0-213 97-398.5t265-305.5 374-151v228q-221 45-366.5 221t-145.5 406q0 130 51 248.5t136.5 204 204 136.5 248.5 51 248.5-51 204-136.5 136.5-204 51-248.5q0-230-145.5-406t-366.5-221v-228q206 31 374 151t265 305.5 97 398.5z"/> | ||
</svg> | ||
); | ||
|
||
Spinner.displayName = 'Cog'; | ||
|
||
export default Spinner; |
Oops, something went wrong.