Skip to content
This repository has been archived by the owner on Sep 4, 2024. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
eliperelman committed Aug 8, 2016
0 parents commit 5d9aa29
Show file tree
Hide file tree
Showing 10 changed files with 1,390 additions and 0 deletions.
31 changes: 31 additions & 0 deletions .gitignore
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
373 changes: 373 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions package.json
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"
}
}
11 changes: 11 additions & 0 deletions src/Cog.js
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;
297 changes: 297 additions & 0 deletions src/LogViewer.js
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>
)
}
});
11 changes: 11 additions & 0 deletions src/Spinner.js
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;
Loading

0 comments on commit 5d9aa29

Please sign in to comment.