Compare commits

...

74 Commits

Author SHA1 Message Date
dependabot[bot] 52014199f1
Bump glob-parent from 5.1.0 to 5.1.2 (#237)
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.0 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.0...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-03 23:17:46 +02:00
dependabot[bot] 18de86b061
Bump lodash from 4.17.19 to 4.17.21 (#235)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 22:56:20 +02:00
Maciej Winnicki 44ff51a618
feat: produce Windows exe 2021-03-17 16:17:42 +01:00
Ethan Dye 514c16e488
chore: update socket.io (#232)
Signed-off-by: Ethan Dye <mrtops03@gmail.com>
2021-03-17 15:49:33 +01:00
EvertEt 6610661b26
chore: fix typo (#229)
Related to #163
2021-03-09 10:49:09 +01:00
Maciej Winnicki 9d14ac51a8
Release 4.9.2 2021-02-17 23:03:05 +01:00
dependabot[bot] ab66d83efc
Bump dot-prop from 4.2.0 to 4.2.1 (#224)
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-17 22:59:38 +01:00
Aaron Dewes a5582dc34f
feat: use Debian buster (#221) 2021-02-17 22:59:19 +01:00
Maciej Winnicki cd871ad361
chore: use explicit modules versions (#225) 2021-02-17 22:57:35 +01:00
dependabot[bot] 38022f7a0a
Bump socket.io from 2.2.0 to 2.4.0 (#223)
Bumps [socket.io](https://github.com/socketio/socket.io) from 2.2.0 to 2.4.0.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/2.4.0/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/2.2.0...2.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-04 08:54:43 +01:00
dependabot[bot] ea330b8ef9
Bump lodash from 4.17.15 to 4.17.19 (#213)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-20 13:37:15 +02:00
Maciej Winnicki fb1da7bd18
chore: update pkg 2020-04-14 09:37:00 +02:00
Maciej Winnicki 5dad99106e
fix: disable usage stats param not passed when in daemon mode. Closes #188 2020-04-13 21:24:54 +02:00
Maciej Winnicki 94e2c46ee6
chore: prettier standard 2020-04-13 21:21:19 +02:00
Maciej Winnicki 0ceb864693
fix: ellipsis in title 2020-04-13 21:03:18 +02:00
Maciej Winnicki 39f8410a2a
chore: fix lint 2020-03-31 20:25:47 +02:00
DomExpire 6c4561a8d7
fix of the approximate value of window.pageYOffset on chrome android (#202) 2020-03-31 20:19:08 +02:00
Maciej Winnicki d43976f3a5
Release 4.9.1 2020-03-26 15:26:52 +01:00
Maciej Winnicki 7f506ae5bf
fix: use different base image to support rpi 2020-03-26 15:24:39 +01:00
dependabot[bot] 0fcd6278fd
Bump acorn from 5.7.3 to 5.7.4 (#199)
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-03-14 21:40:11 +01:00
donnie 0aad9dd2dc
Fix websocket error during handshake (#195)
See [this SO issue](https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respo)
2020-02-19 19:55:30 +01:00
Maciej Winnicki c12a1878de
Update Release guide 2020-02-03 13:26:32 +01:00
Maciej Winnicki 25f3b23c86
Release 4.9.0 2020-02-03 13:19:55 +01:00
Maciej Winnicki 2b3a64dca2
Fix publish step 2020-02-03 13:19:23 +01:00
Maciej Winnicki 9e6e7ee5dc
Fix publish command 2020-02-03 13:02:09 +01:00
Maciej Winnicki 1a9ce68c4a
Fix publish step 2020-02-03 12:56:45 +01:00
Maciej Winnicki 06fd619a40
Fix GH action 2020-02-03 12:51:20 +01:00
Maciej Winnicki b80fc8fd0c
Fix GH action 2020-02-03 12:39:12 +01:00
Maciej Winnicki 34d5893694
Fix GH action 2020-02-03 12:37:48 +01:00
Maciej Winnicki 18c4603016
Fix GH actions config 2020-02-03 12:35:50 +01:00
Maciej Winnicki a63e6dbd87
Lint fixes 2020-02-03 12:23:47 +01:00
Alexander Wunschik 0eac303527
Add windows support (#194)
* fix: replace native tail with fs-tail-stream
fixes #111

* fix: use options.buffer as start parameter

* feat: add additional windows support

* feat: add additional windows support
2020-02-03 12:02:21 +01:00
Maciej Winnicki 1357793a28
Make CI step conditional 2019-09-13 21:34:33 +02:00
Maciej Winnicki 7ff68040c3
Typo 2019-09-13 21:21:44 +02:00
Maciej Winnicki b31de76d33
converted main.workflow to Actions V2 yml files (#183) 2019-09-13 21:19:15 +02:00
Alexander Wunschik 09acdc62af update commander to 3.0.1 (#182)
see also #176
2019-08-30 13:20:11 +02:00
Maciej Winnicki 3b390a0d54
Release 4.8.0 2019-08-29 16:42:53 +02:00
Pavlo Bashynskyi 01afdc8cdd Add pause button (#163) Closes #113. Closes #137. 2019-08-27 11:24:25 +02:00
Maciej Winnicki 4d5f29b209
Track --number param 2019-08-26 22:03:27 +02:00
Maciej Winnicki 29d660623f
Release 4.7.0 2019-08-26 20:17:38 +02:00
Maciej Winnicki 2150761fa2
Fix auto-scrolling (#180) 2019-08-26 20:14:30 +02:00
dependabot[bot] 1cacc260f4 Bump eslint-utils from 1.3.1 to 1.4.2 (#181)
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-26 20:10:31 +02:00
Maciej Winnicki f8de308820
Update pkg 2019-08-08 21:29:44 +02:00
Maciej Winnicki 7c8c9cab98
Fix CI issues (#179) 2019-08-08 21:22:14 +02:00
dependabot[bot] 62c0d67370 Bump extend from 3.0.1 to 3.0.2 (#178)
Bumps [extend](https://github.com/justmoon/node-extend) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/justmoon/node-extend/releases)
- [Changelog](https://github.com/justmoon/node-extend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/justmoon/node-extend/compare/v3.0.1...v3.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-06 19:41:48 +02:00
Alexander Wunschik 364f68b9c1 chore(docs): fix help option (#176)
fixes #53
2019-08-06 19:39:40 +02:00
Alexander Wunschik bc8c1f8d00 chore(docs): fix web interface url (#177) 2019-08-06 16:25:33 +02:00
Ryan Hunt e6cc5f4dd1 keep empty lines (#174) 2019-07-22 21:03:10 +02:00
dependabot[bot] 95d8cd5dcc Bump lodash from 4.17.4 to 4.17.15 (#173)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.4 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.4...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-21 22:14:59 +02:00
Maciej Winnicki 54101c8fad
Release 4.6.0 2019-07-03 22:58:25 +02:00
Maciej Winnicki 5bf80592cc
Support running frontial Docker on ARM by switching base image to the official one. Closes #168 2019-07-03 22:56:00 +02:00
dependabot[bot] f5c86cd3d7 Bump stringstream from 0.0.5 to 0.0.6 (#167)
Bumps [stringstream](https://github.com/mhart/StringStream) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/mhart/StringStream/releases)
- [Commits](https://github.com/mhart/StringStream/compare/v0.0.5...v0.0.6)

Signed-off-by: dependabot[bot] <support@github.com>
2019-06-30 19:02:31 +02:00
dependabot[bot] 4f4693b623 Bump js-yaml from 3.12.0 to 3.13.1 (#166)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)
2019-06-05 10:03:23 +02:00
Maciej Winnicki e9e16fa578
Release 4.5.4 2019-02-21 23:17:30 +01:00
Maciej Winnicki a869307ecc
Fix proper signal handling. Closes #158 #160 2019-02-21 23:15:17 +01:00
Maciej Winnicki 205cdbf696
Cleanup unused files (#157) 2019-01-18 22:43:51 +01:00
Maciej Winnicki 658a0914bf
Revert "Temporary remove GH workflow because of https://github.com/actions/bin/issues/13"
This reverts commit 26c3957457.
2019-01-18 22:24:18 +01:00
Maciej Winnicki b2a4f4b4af
Release 4.5.3 2019-01-09 22:04:34 +01:00
Maciej Winnicki da920b4c04
Update npm token 2019-01-09 22:03:53 +01:00
Maciej Winnicki 955bf9d436
Release 4.5.2 2019-01-09 22:00:28 +01:00
Maciej Winnicki 57367a2b0d
Fix highligthing multiple word. Closes #151 (#155) 2019-01-09 21:58:01 +01:00
Maciej Winnicki 26c3957457
Temporary remove GH workflow because of https://github.com/actions/bin/issues/13 2019-01-09 21:53:10 +01:00
Maciej Winnicki 953c6b7dbe
Add GH actions workflow 2019-01-09 21:45:21 +01:00
Maciej Winnicki 8ea0f65760
test2 2019-01-07 21:53:05 +01:00
Maciej Winnicki 6c77313a6a
test 2019-01-07 21:50:38 +01:00
Maciej Winnicki 2cfff5caf7
Update deps (#154) 2018-12-18 12:14:26 +01:00
Maciej Winnicki ef8c14bb9c
Release 4.5.1 2018-11-28 10:17:37 +01:00
Maciej Winnicki c35287ce7a
Fix runtime stats sending 2018-11-28 10:13:32 +01:00
Maciej Winnicki 28de6d18dc
Update mocha dependency 2018-11-28 09:50:19 +01:00
Maciej Winnicki 274e48731f
Release 4.5.0 2018-11-22 12:11:15 +01:00
Maciej Winnicki 4a87fa6bdf
Add usage stats gathering 2018-11-22 12:09:51 +01:00
Maciej Winnicki 90d1428449
Release 4.4.0 2018-10-30 08:52:40 +01:00
watanabe takanobu 75d31aafd6 Support OpenBSD (#146) 2018-10-30 08:39:21 +01:00
Maciej Winnicki a99286fa0d
Update Docker image link 2018-10-24 14:07:09 +02:00
28 changed files with 2059 additions and 2745 deletions

View File

@ -1,2 +0,0 @@
web/assets/tinycon.min.js
web/assets/ansi_up.js

View File

@ -1,11 +0,0 @@
{
"extends": "airbnb-base",
"rules": {
"no-console": "off",
"strict": "off",
"comma-dangle": ["error", { "functions": "never" }]
},
"env": {
"node": true
}
}

20
.github/workflows/push.yml vendored Normal file
View File

@ -0,0 +1,20 @@
on: push
name: Build, Lint, Test, and Publish
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm install
- run: npm test
- run: npm run lint
- uses: primer/publish@master
if: github.ref == 'refs/heads/master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
NPM_REGISTRY_URL: registry.npmjs.org

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules
test/fixtures/*.pem
npm-debug.log
dist
dist
.DS_Store

View File

@ -10,7 +10,7 @@ deploy:
provider: npm
email: maciej.winnicki@gmail.com
api_key:
secure: S7VCwq19j0w2vBn27f0pQBWIFCN/3iZTyy9fUXuaTrvHztB7YQHBVxkIDNPYgWAttQLKsu0PH9BFzNeA7/StrKYKsn+z6fwh07GSOQGGwVO/OxvjfjglfgzNwLsuuT+N4R2ClrkqzIn7NaYVEM3bxofL+gwlHxWSfXfErW7mUds=
secure: OuE8bOpsq/XN2AL6z0SWqHtpfKB5Qg3wug+hZVka+AdWaU6Gm0uvs4yA7B/uw9o5gH6KKgM+CP50Lq1xhC12CZbKLjkAUuFJQb9FrU/7jEAp+t+DKpoVBcru8bBjTxxx4wO1zXFcbJZ9zb4c4h76j2nfaNVuPTBB+mDF/HDd914=
on:
tags: true
repo: mthenw/frontail

View File

@ -1,4 +1,4 @@
FROM mhart/alpine-node:8.12.0
FROM node:12-buster-slim
WORKDIR /frontail
ADD . .

View File

@ -4,33 +4,33 @@
![frontial](https://user-images.githubusercontent.com/455261/29570317-660c8122-8756-11e7-9d2f-8fea19e05211.gif)
[![Build Status](https://img.shields.io/travis/mthenw/frontail.svg?style=flat)](https://travis-ci.org/mthenw/frontail)
[![Docker Pulls](https://img.shields.io/docker/pulls/mthenw/frontail.svg)](https://hub.docker.com/r/mthenw/frontail/)
## Quick start
* `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
* `frontail /var/log/syslog`
* visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
- `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
- `frontail /var/log/syslog`
- visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
## Features
* log rotation
* auto-scrolling
* marking logs
* number of unread logs in favicon
* themes (default, dark)
* [highlighting](#highlighting)
* search (`Tab` to focus, `Esc` to clear)
* set filter from url parameter `filter`
* tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
* basic authentication
- log rotation (not on Windows)
- auto-scrolling
- marking logs
- pausing logs
- number of unread logs in favicon
- themes (default, dark)
- [highlighting](#highlighting)
- search (`Tab` to focus, `Esc` to clear)
- set filter from url parameter `filter`
- tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
- basic authentication
## Installation options
* download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page (currently `frontail` doesn't work on Windows)
* using [npm package](https://www.npmjs.com/package/frontail): `npm i frontail -g`
* using [Docker image](https://registry.hub.docker.com/u/mthenw/frontail/): `docker run -d -P -v /var/log:/log mthenw/frontail /log/syslog`
- download a binary file from [Releases](https://github.com/mthenw/frontail/releases) pagegit st
- using [npm package](https://www.npmjs.com/package/frontail): `npm i frontail -g`
- using [Docker image](https://cloud.docker.com/repository/docker/mthenw/frontail): `docker run -d -P -v /var/log:/log mthenw/frontail /log/syslog`
## Usage
@ -38,7 +38,6 @@
Options:
-h, --help output usage information
-V, --version output the version number
-h, --host <host> listening host, default 0.0.0.0
-p, --port <port> listening port, default 9001
@ -58,8 +57,10 @@
--ui-highlight highlight words or lines if defined string found in logs, default preset
--ui-highlight-preset <path> custom preset for highlighting (see ./preset/default.json)
--path <path> prefix path for the running application, default /
--disable-usage-stats disable gathering usage statistics
--help output usage information
Web interface runs on **http://127.0.0.1:[port]**.
Web interface runs on **http://[host]:[port]**.
### Tailing multiple files
@ -91,6 +92,7 @@ which means that every "err" string will be in red and every line containing "er
_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._
Available presets:
- default
- npmlog
- python
@ -121,3 +123,10 @@ http {
}
}
```
### Usage statistics
`frontail` by default (from `v4.5.0`) gathers **anonymous** usage statistics in Google Analytics. It can be disabled with
`--disable-usage-stats`.
The data is used to help me understand how `frontail` is used and I can make it better.

View File

@ -18,23 +18,18 @@ After all [pull requests](https://github.com/mthenw/frontail/pulls) for a releas
$ git commit -am "Release <version>"
```
1. Tag the version.
1. Push the commit.
```sh
$ git tag v<version>
$ git push origin head
```
1. Push the commit and tag.
```sh
$ git push origin head --tags
```
1. Travis CI will publish new version to NPM.
1. GitHub action will publish new version to NPM and push new tag.
1. Publish new release on GitHub with [`release`](https://github.com/zeit/release) package.
```sh
$ git pull
$ npx release -P
```

View File

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
set -euo pipefail

View File

@ -4,7 +4,7 @@ const cookie = require('cookie');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const path = require('path');
const SocketIO = require('socket.io');
const { Server } = require('socket.io');
const fs = require('fs');
const untildify = require('untildify');
const tail = require('./lib/tail');
@ -12,6 +12,7 @@ const connectBuilder = require('./lib/connect_builder');
const program = require('./lib/options_parser');
const serverBuilder = require('./lib/server_builder');
const daemonize = require('./lib/daemonize');
const usageStats = require('./lib/stats');
/**
* Parse args
@ -22,6 +23,13 @@ if (program.args.length === 0) {
process.exit();
}
/**
* Init usage statistics
*/
const stats = usageStats(!program.disableUsageStats, program);
stats.track('runtime', 'init');
stats.time('runtime', 'runtime');
/**
* Validate params
*/
@ -29,16 +37,13 @@ const doAuthorization = !!(program.user && program.password);
const doSecure = !!(program.key && program.certificate);
const sessionSecret = String(+new Date()) + Math.random();
const files = program.args.join(' ');
const filesNamespace = crypto
.createHash('md5')
.update(files)
.digest('hex');
const filesNamespace = crypto.createHash('md5').update(files).digest('hex');
const urlPath = program.urlPath.replace(/\/$/, ''); // remove trailing slash
if (program.daemonize) {
daemonize(__filename, program, {
doAuthorization,
doSecure
doSecure,
});
} else {
/**
@ -50,8 +55,13 @@ if (program.daemonize) {
appBuilder.authorize(program.user, program.password);
}
appBuilder
.static(path.join(__dirname, 'web/assets'))
.index(path.join(__dirname, 'web/index.html'), files, filesNamespace, program.theme);
.static(path.join(__dirname, 'web', 'assets'))
.index(
path.join(__dirname, 'web', 'index.html'),
files,
filesNamespace,
program.theme
);
const builder = serverBuilder();
if (doSecure) {
@ -66,7 +76,7 @@ if (program.daemonize) {
/**
* socket.io setup
*/
const io = new SocketIO({ path: path.join(urlPath, '/socket.io') });
const io = new Server({ path: `${urlPath}/socket.io` });
io.attach(server);
if (doAuthorization) {
@ -78,7 +88,10 @@ if (program.daemonize) {
if (!sessionIdEncoded) {
return next(new Error('Session cookie not provided'), false);
}
const sessionId = cookieParser.signedCookie(sessionIdEncoded, sessionSecret);
const sessionId = cookieParser.signedCookie(
sessionIdEncoded,
sessionSecret
);
if (sessionId) {
return next(null);
}
@ -97,7 +110,7 @@ if (program.daemonize) {
let presetPath;
if (!program.uiHighlightPreset) {
presetPath = path.join(__dirname, 'preset/default.json');
presetPath = path.join(__dirname, 'preset', 'default.json');
} else {
presetPath = path.resolve(untildify(program.uiHighlightPreset));
}
@ -113,7 +126,7 @@ if (program.daemonize) {
* When connected send starting data
*/
const tailer = tail(program.args, {
buffer: program.number
buffer: program.number,
});
const filesSocket = io.of(`/${filesNamespace}`).on('connection', (socket) => {
@ -143,11 +156,15 @@ if (program.daemonize) {
filesSocket.emit('line', line);
});
stats.track('runtime', 'started');
/**
* Handle signals
*/
const cleanExit = () => {
process.exit();
stats.timeEnd('runtime', 'runtime', () => {
process.exit();
});
};
process.on('SIGINT', cleanExit);
process.on('SIGTERM', cleanExit);

View File

@ -14,7 +14,10 @@ function ConnectBuilder(urlPath) {
ConnectBuilder.prototype.authorize = function authorize(user, pass) {
this.app.use(
this.urlPath,
basicAuth((incomingUser, incomingPass) => user === incomingUser && pass === incomingPass)
basicAuth(
(incomingUser, incomingPass) =>
user === incomingUser && pass === incomingPass
)
);
return this;
@ -24,13 +27,18 @@ ConnectBuilder.prototype.build = function build() {
return this.app;
};
ConnectBuilder.prototype.index = function index(path, files, filesNamespace, themeOpt) {
ConnectBuilder.prototype.index = function index(
path,
files,
filesNamespace,
themeOpt
) {
const theme = themeOpt || 'default';
this.app.use(this.urlPath, (req, res) => {
fs.readFile(path, (err, data) => {
res.writeHead(200, {
'Content-Type': 'text/html'
'Content-Type': 'text/html',
});
res.end(
data
@ -53,7 +61,7 @@ ConnectBuilder.prototype.session = function sessionf(secret) {
expressSession({
secret,
resave: false,
saveUninitialized: true
saveUninitialized: true,
})
);
return this;
@ -64,4 +72,4 @@ ConnectBuilder.prototype.static = function staticf(path) {
return this;
};
module.exports = urlPath => new ConnectBuilder(urlPath);
module.exports = (urlPath) => new ConnectBuilder(urlPath);

View File

@ -5,7 +5,7 @@ const fs = require('fs');
const defaultOptions = {
doAuthorization: false,
doSecure: false
doSecure: false,
};
module.exports = (script, params, opts) => {
@ -23,7 +23,7 @@ module.exports = (script, params, opts) => {
'-l',
params.lines,
'-t',
params.theme
params.theme,
];
if (options.doAuthorization) {
@ -54,11 +54,15 @@ module.exports = (script, params, opts) => {
args.push('--ui-highlight-preset', params.uiHighlightPreset);
}
if (params.disableUsageStats) {
args.push('--disable-usage-stats', params.disableUsageStats);
}
args = args.concat(params.args);
const proc = daemon.daemon(script, args, {
stdout: logFile,
stderr: logFile
stderr: logFile,
});
fs.writeFileSync(params.pidPath, proc.pid);

View File

@ -3,11 +3,32 @@ const program = require('commander');
program
.version(require('../package.json').version)
.usage('[options] [file ...]')
.option('-h, --host <host>', 'listening host, default 0.0.0.0', String, '0.0.0.0')
.helpOption('--help')
.option(
'-h, --host <host>',
'listening host, default 0.0.0.0',
String,
'0.0.0.0'
)
.option('-p, --port <port>', 'listening port, default 9001', Number, 9001)
.option('-n, --number <number>', 'starting lines number, default 10', Number, 10)
.option('-l, --lines <lines>', 'number on lines stored in browser, default 2000', Number, 2000)
.option('-t, --theme <theme>', 'name of the theme (default, dark)', String, 'default')
.option(
'-n, --number <number>',
'starting lines number, default 10',
Number,
10
)
.option(
'-l, --lines <lines>',
'number on lines stored in browser, default 2000',
Number,
2000
)
.option(
'-t, --theme <theme>',
'name of the theme (default, dark)',
String,
'default'
)
.option('-d, --daemonize', 'run as daemon')
.option(
'-U, --user <username>',
@ -45,7 +66,12 @@ program
String,
'/dev/null'
)
.option('--url-path <path>', 'URL path for the browser application, default /', String, '/')
.option(
'--url-path <path>',
'URL path for the browser application, default /',
String,
'/'
)
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
.option('--ui-no-indent', "don't indent log lines")
.option(
@ -55,6 +81,7 @@ program
.option(
'--ui-highlight-preset <path>',
'custom preset for highlighting (see ./preset/default.json)'
);
)
.option('--disable-usage-stats', 'disable gathering usage statistics');
module.exports = program;

View File

@ -15,9 +15,11 @@ ServerBuilder.prototype.build = function build() {
if (this._key && this._cert) {
const options = {
key: this._key,
cert: this._cert
cert: this._cert,
};
return https.createServer(options, this._callback).listen(this._port, this._host);
return https
.createServer(options, this._callback)
.listen(this._port, this._host);
}
return http.createServer(this._callback).listen(this._port, this._host);

69
lib/stats.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const ua = require('universal-analytics');
const isDocker = require('is-docker');
const Configstore = require('configstore');
const uuidv4 = require('uuid/v4');
const pkg = require('../package.json');
const trackingID = 'UA-129582046-1';
// Usage stats
function Stats(enabled, opts) {
this.timer = {};
if (enabled === true) {
const config = new Configstore(pkg.name);
let clientID = uuidv4();
if (config.has('clientID')) {
clientID = config.get('clientID');
} else {
config.set('clientID', clientID);
}
const tracker = ua(trackingID, clientID);
tracker.set('aip', 1); // Anonymize IP
tracker.set('an', 'frontail'); // Application Name
tracker.set('av', pkg.version); // Application Version
tracker.set('ds', 'app'); // Data Source
tracker.set('cd1', process.platform); // platform
tracker.set('cd2', process.arch); // arch
tracker.set('cd3', process.version.match(/^v(\d+\.\d+)/)[1]); // Node version
tracker.set('cd4', isDocker()); // is Docker
tracker.set('cd5', opts.number !== 10); // is --number parameter set
this.tracker = tracker;
}
return this;
}
Stats.prototype.track = function track(category, action) {
if (!this.tracker) {
return;
}
this.tracker.event(category, action).send();
};
Stats.prototype.time = function time(category, action) {
if (!this.tracker) {
return;
}
if (!this.timer[category]) {
this.timer[category] = {};
}
this.timer[category][action] = Date.now();
};
Stats.prototype.timeEnd = function timeEnd(category, action, cb) {
if (!this.tracker) {
cb();
return;
}
this.tracker
.timing(category, action, Date.now() - this.timer[category][action])
.send(cb);
};
module.exports = (enabled, opts) => new Stats(enabled, opts);

View File

@ -4,45 +4,64 @@
const events = require('events');
const childProcess = require('child_process');
const tailStream = require('fs-tail-stream');
const util = require('util');
const CBuffer = require('CBuffer');
const byline = require('byline');
const commandExistsSync = require('command-exists').sync;
function Tail(path, opts) {
events.EventEmitter.call(this);
const options = opts || {
buffer: 0
buffer: 0,
};
this._buffer = new CBuffer(options.buffer);
let stream;
if (path[0] === '-') {
byline(process.stdin).on('data', (line) => {
const str = line.toString();
this._buffer.push(str);
this.emit('line', str);
});
stream = process.stdin;
} else {
const tail = childProcess.spawn('tail', ['-n', options.buffer, '-F'].concat(path));
tail.stderr.on('data', (data) => {
// If there is any important error then display it in the console. Tail will keep running.
// File can be truncated over network.
if (data.toString().indexOf('file truncated') === -1) {
console.error(data.toString());
/* Check if this os provides the `tail` command. */
const hasTailCommand = commandExistsSync('tail');
if (hasTailCommand) {
let followOpt = '-F';
if (process.platform === 'openbsd') {
followOpt = '-f';
}
});
byline(tail.stdout).on('data', (line) => {
const str = line.toString();
this._buffer.push(str);
this.emit('line', str);
});
const cp = childProcess.spawn(
'tail',
['-n', options.buffer, followOpt].concat(path)
);
cp.stderr.on('data', (data) => {
// If there is any important error then display it in the console. Tail will keep running.
// File can be truncated over network.
if (data.toString().indexOf('file truncated') === -1) {
console.error(data.toString());
}
});
stream = cp.stdout;
process.on('exit', () => {
tail.kill();
});
process.on('exit', () => {
cp.kill();
});
} else {
/* This is used if the os does not support the `tail`command. */
stream = tailStream.createReadStream(path.join(), {
encoding: 'utf8',
start: options.buffer,
tail: true,
});
}
}
byline(stream, { keepEmptyLines: true }).on('data', (line) => {
const str = line.toString();
this._buffer.push(str);
this.emit('line', str);
});
}
util.inherits(Tail, events.EventEmitter);

4002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "frontail",
"version": "4.3.3",
"version": "4.9.2",
"description": "streaming logs to the browser",
"homepage": "https://github.com/mthenw/frontail",
"author": "Maciej Winnicki <maciej.winnicki@gmail.com>",
@ -8,33 +8,40 @@
"bin": "./bin/frontail",
"dependencies": {
"CBuffer": "0.1.4",
"basic-auth-connect": "^1.0.0",
"byline": "^5.0.0",
"commander": "1.3.2",
"connect": "3.6.6",
"basic-auth-connect": "1.0.0",
"byline": "5.0.0",
"command-exists": "1.2.8",
"commander": "3.0.1",
"configstore": "4.0.0",
"connect": "3.7.0",
"cookie": "0.1.0",
"cookie-parser": "^1.4.3",
"cookie-parser": "1.4.5",
"daemon-fix41": "1.1.2",
"express-session": "^1.15.6",
"serve-static": "^1.13.2",
"socket.io": "^1.7.0",
"untildify": "^3.0.2"
"express-session": "1.15.6",
"fs-tail-stream": "1.1.0",
"is-docker": "1.1.0",
"serve-static": "1.14.1",
"socket.io": "3.1.2",
"universal-analytics": "0.4.23",
"untildify": "3.0.2",
"uuid": "3.3.2"
},
"devDependencies": {
"eslint": "^5.6.1",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"jsdom": "^11.11.0",
"mocha": "~2.3.2",
"pkg": "^4.3.3",
"eslint": "~6.8.0",
"eslint-config-airbnb-base": "~14.0.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "~2.20.0",
"jsdom": "~11.12.0",
"mocha": "~5.2.0",
"pkg": "~4.4.7",
"should": "~3.3.2",
"sinon": "~1.7.3",
"supertest": "~0.8.1",
"supertest": "^3.3.0",
"temp": "~0.8.1"
},
"scripts": {
"lint": "eslint .",
"test": "mocha -r should --reporter spec test/*.js",
"test": "mocha -r should --exit test/*.js",
"pkg": "pkg --out-path=dist ."
},
"pkg": {
@ -43,11 +50,34 @@
"web/**/*"
],
"targets": [
"node8-alpine-x64",
"node8-linux-x64",
"node8-macos-x64"
"node12-alpine-x64",
"node12-linux-x64",
"node12-macos-x64",
"node12-windows-x64"
]
},
"eslintConfig": {
"extends": [
"airbnb-base",
"prettier"
],
"rules": {
"no-console": "off",
"strict": "off",
"implicit-arrow-linebreak": "off"
},
"env": {
"node": true
},
"ignorePatterns": [
"web/assets/tinycon.min.js",
"web/assets/ansi_up.js"
]
},
"prettier": {
"singleQuote": true,
"arrowParens": "always"
},
"repository": {
"type": "git",
"url": "http://github.com/mthenw/frontail.git"

View File

@ -13,8 +13,9 @@ describe('browser application', () => {
socket: io,
container: window.document.querySelector('.log'),
filterInput: window.document.querySelector('#filter'),
pauseBtn: window.document.querySelector('#pauseBtn'),
topbar: window.document.querySelector('.topbar'),
body: window.document.querySelector('body')
body: window.document.querySelector('body'),
});
}
@ -42,8 +43,10 @@ describe('browser application', () => {
beforeEach((done) => {
io = new events.EventEmitter();
const html = '<title></title><body><div class="topbar"></div>'
+ '<div class="log"></div><input type="test" id="filter"/></body>';
const html =
'<title></title><body><div class="topbar"></div>' +
'<div class="log"></div><button type="button" id="pauseBtn"></button>' +
'<input type="test" id="filter"/></body>';
const ansiup = fs.readFileSync('./web/assets/ansi_up.js', 'utf-8');
const src = fs.readFileSync('./web/assets/app.js', 'utf-8');
@ -56,7 +59,7 @@ describe('browser application', () => {
initApp();
done();
}
},
});
});
@ -68,7 +71,9 @@ describe('browser application', () => {
log.childNodes[0].textContent.should.be.equal('test');
log.childNodes[0].className.should.be.equal('line');
log.childNodes[0].tagName.should.be.equal('DIV');
log.childNodes[0].innerHTML.should.be.equal('<p class="inner-line">test</p>');
log.childNodes[0].innerHTML.should.be.equal(
'<p class="inner-line">test</p>'
);
});
it('should select line when clicked', () => {
@ -121,20 +126,23 @@ describe('browser application', () => {
it('should highlight word', () => {
io.emit('options:highlightConfig', {
words: {
line: 'background: black'
}
foo: 'background: black',
bar: 'background: black',
},
});
io.emit('line', 'line1');
io.emit('line', 'foo bar');
const line = window.document.querySelector('.line');
line.innerHTML.should.containEql('<span style="background: black">line</span>');
line.innerHTML.should.containEql(
'<span style="background: black">foo</span> <span style="background: black">bar</span>'
);
});
it('should highlight line', () => {
io.emit('options:highlightConfig', {
lines: {
line: 'background: black'
}
line: 'background: black',
},
});
io.emit('line', 'line1');
@ -198,4 +206,37 @@ describe('browser application', () => {
log.childNodes[2].style.display.should.be.equal('none');
window.location.href.should.containEql('filter=other');
});
it('should pause', () => {
io.emit('line', 'line1');
const btn = window.document.querySelector('#pauseBtn');
const event = window.document.createEvent('Event');
event.initEvent('mouseup', true, true);
btn.dispatchEvent(event);
io.emit('line', 'line2');
io.emit('line', 'line3');
btn.className.should.containEql('play');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('==> SKIPPED: 2 <==');
});
it('should play', () => {
const btn = window.document.querySelector('#pauseBtn');
const event = window.document.createEvent('Event');
event.initEvent('mouseup', true, true);
btn.dispatchEvent(event);
io.emit('line', 'line1');
const log = window.document.querySelector('.log');
log.childNodes.length.should.be.equal(1);
log.lastChild.textContent.should.be.equal('==> SKIPPED: 1 <==');
btn.className.should.containEql('play');
btn.dispatchEvent(event);
io.emit('line', 'line2');
btn.className.should.not.containEql('play');
log.childNodes.length.should.be.equal(2);
log.lastChild.textContent.should.be.equal('line2');
});
});

View File

@ -6,18 +6,12 @@ const connectBuilder = require('../lib/connect_builder');
describe('connectBuilder', () => {
it('should build connect app', () => {
connectBuilder('/')
.build()
.should.have.property('use');
connectBuilder()
.build()
.should.have.property('listen');
connectBuilder('/').build().should.have.property('use');
connectBuilder().build().should.have.property('listen');
});
it('should build app requiring authorized user', (done) => {
const app = connectBuilder('/')
.authorize('user', 'pass')
.build();
const app = connectBuilder('/').authorize('user', 'pass').build();
request(app)
.get('/')
@ -26,9 +20,7 @@ describe('connectBuilder', () => {
});
it('should build app allowing user to login', (done) => {
const app = connectBuilder('/')
.authorize('user', 'pass')
.build();
const app = connectBuilder('/').authorize('user', 'pass').build();
app.use((req, res) => {
res.end('secret!');
});
@ -40,9 +32,7 @@ describe('connectBuilder', () => {
});
it('should build app that setup session', (done) => {
const app = connectBuilder('/')
.session('secret')
.build();
const app = connectBuilder('/').session('secret').build();
app.use((req, res) => {
res.end();
});
@ -57,9 +47,7 @@ describe('connectBuilder', () => {
.static(path.join(__dirname, 'fixtures'))
.build();
request(app)
.get('/foo')
.expect('bar', done);
request(app).get('/foo.txt').expect('bar', done);
});
it('should build app that serve index file', (done) => {
@ -67,10 +55,7 @@ describe('connectBuilder', () => {
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
.build();
request(app)
.get('/')
.expect(200)
.expect('Content-Type', 'text/html', done);
request(app).get('/').expect(200).expect('Content-Type', 'text/html', done);
});
it('should build app that serve index file on specified path', (done) => {
@ -89,24 +74,30 @@ describe('connectBuilder', () => {
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
.build();
request(app)
.get('/')
.expect('<head><title>/testfile</title></head>', done);
request(app).get('/').expect('<head><title>/testfile</title></head>', done);
});
it('should build app that sets socket.io namespace based on files', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, 'fixtures/index_with_ns'), '/testfile', 'ns', 'dark')
.index(
path.join(__dirname, 'fixtures/index_with_ns'),
'/testfile',
'ns',
'dark'
)
.build();
request(app)
.get('/')
.expect('ns', done);
request(app).get('/').expect('ns', done);
});
it('should build app that sets theme', (done) => {
const app = connectBuilder('/')
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile', 'ns', 'dark')
.index(
path.join(__dirname, '/fixtures/index_with_theme'),
'/testfile',
'ns',
'dark'
)
.build();
request(app)

View File

@ -10,7 +10,7 @@ describe('daemonize', () => {
beforeEach(() => {
sinon.stub(daemon, 'daemon');
daemon.daemon.returns({
pid: 1000
pid: 1000,
});
sinon.stub(fs, 'writeFileSync');
sinon.stub(fs, 'openSync');
@ -70,41 +70,89 @@ describe('daemonize', () => {
});
it('with authorization', () => {
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
optionsParser.parse([
'node',
'/path/to/frontail',
'-U',
'user',
'-P',
'passw0rd',
]);
daemonize('script', optionsParser, {
doAuthorization: true
doAuthorization: true,
});
daemon.daemon.lastCall.args[1].should.containDeep(['-U', 'user', '-P', 'passw0rd']);
daemon.daemon.lastCall.args[1].should.containDeep([
'-U',
'user',
'-P',
'passw0rd',
]);
});
it('without authorization if option doAuthorization not passed', () => {
optionsParser.parse(['node', '/path/to/frontail', '-U', 'user', '-P', 'passw0rd']);
optionsParser.parse([
'node',
'/path/to/frontail',
'-U',
'user',
'-P',
'passw0rd',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.not.containDeep(['-U', 'user', '-P', 'passw0rd']);
daemon.daemon.lastCall.args[1].should.not.containDeep([
'-U',
'user',
'-P',
'passw0rd',
]);
});
it('with secure connection', () => {
optionsParser.parse(['node', '/path/to/frontail', '-k', 'key.file', '-c', 'cert.file']);
optionsParser.parse([
'node',
'/path/to/frontail',
'-k',
'key.file',
'-c',
'cert.file',
]);
daemonize('script', optionsParser, {
doSecure: true
doSecure: true,
});
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
daemon.daemon.lastCall.args[1].should.containDeep([
'-k',
'key.file',
'-c',
'cert.file',
]);
});
it('without secure connection if option doSecure not passed', () => {
optionsParser.parse(['node', '/path/to/frontail', '-k', 'key.file', '-c', 'cert.file']);
optionsParser.parse([
'node',
'/path/to/frontail',
'-k',
'key.file',
'-c',
'cert.file',
]);
daemonize('script', optionsParser, {
doSecure: true
doSecure: true,
});
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
daemon.daemon.lastCall.args[1].should.containDeep([
'-k',
'key.file',
'-c',
'cert.file',
]);
});
it('with url-path option', () => {
@ -112,7 +160,10 @@ describe('daemonize', () => {
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--url-path', '/test']);
daemon.daemon.lastCall.args[1].should.containDeep([
'--url-path',
'/test',
]);
});
it('with hide-topbar option', () => {
@ -145,12 +196,31 @@ describe('daemonize', () => {
'/path/to/frontail',
'--ui-highlight',
'--ui-highlight-preset',
'test.json'
'test.json',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep(['--ui-highlight-preset', 'test.json']);
daemon.daemon.lastCall.args[1].should.containDeep([
'--ui-highlight-preset',
'test.json',
]);
});
it('with disable usage stats', () => {
optionsParser.parse([
'node',
'/path/to/frontail',
'--disable-usage-stats',
'test.json',
]);
daemonize('script', optionsParser);
daemon.daemon.lastCall.args[1].should.containDeep([
'--disable-usage-stats',
'test.json',
]);
});
it('with file to tail', () => {
@ -163,7 +233,12 @@ describe('daemonize', () => {
});
it('should write pid to pidfile', () => {
optionsParser.parse(['node', '/path/to/frontail', '--pid-path', '/path/to/pid']);
optionsParser.parse([
'node',
'/path/to/frontail',
'--pid-path',
'/path/to/pid',
]);
daemonize('script', optionsParser);
@ -172,7 +247,12 @@ describe('daemonize', () => {
});
it('should log to file', () => {
optionsParser.parse(['node', '/path/to/frontail', '--log-path', '/path/to/log']);
optionsParser.parse([
'node',
'/path/to/frontail',
'--log-path',
'/path/to/log',
]);
fs.openSync.returns('file');
daemonize('script', optionsParser);
@ -181,7 +261,7 @@ describe('daemonize', () => {
fs.openSync.lastCall.args[1].should.equal('a');
daemon.daemon.lastCall.args[2].should.eql({
stdout: 'file',
stderr: 'file'
stderr: 'file',
});
});
});

View File

@ -31,9 +31,7 @@ describe('serverBuilder', () => {
it('should build server accepting requests', () => {
const callback = () => {};
serverBuilder()
.use(callback)
.build();
serverBuilder().use(callback).build();
createServer.calledWith(callback).should.equal(true);
});
@ -45,9 +43,7 @@ describe('serverBuilder', () => {
});
it('should build server listening on specified port', () => {
serverBuilder()
.port(6666)
.build();
serverBuilder().port(6666).build();
httpServer.listen.calledWith(6666).should.equal(true);
});
@ -59,9 +55,7 @@ describe('serverBuilder', () => {
});
it('should build server listening on specified host', () => {
serverBuilder()
.host('127.0.0.1')
.build();
serverBuilder().host('127.0.0.1').build();
httpServer.listen.calledWith(9001, '127.0.0.1').should.equal(true);
});
@ -81,7 +75,9 @@ describe('serverBuilder', () => {
beforeEach(() => {
httpsServer = sinon.createStubInstance(https.Server);
httpsServer.listen.returns(httpsServer);
createHttpsServer = sinon.stub(https, 'createServer').returns(httpsServer);
createHttpsServer = sinon
.stub(https, 'createServer')
.returns(httpsServer);
readFileSyncStub = sinon.stub(fs, 'readFileSync');
readFileSyncStub.withArgs('key.pem').returns('testkey');
readFileSyncStub.withArgs('cert.pem').returns('testcert');
@ -93,15 +89,13 @@ describe('serverBuilder', () => {
});
it('should build server', () => {
const server = serverBuilder()
.secure('key.pem', 'cert.pem')
.build();
const server = serverBuilder().secure('key.pem', 'cert.pem').build();
server.should.be.an.instanceof(https.Server);
createHttpsServer
.calledWith({
key: 'testkey',
cert: 'testcert'
cert: 'testcert',
})
.should.equal(true);
});
@ -109,16 +103,13 @@ describe('serverBuilder', () => {
it('should build server accepting requests', () => {
const callback = () => {};
serverBuilder()
.use(callback)
.secure('key.pem', 'cert.pem')
.build();
serverBuilder().use(callback).secure('key.pem', 'cert.pem').build();
createHttpsServer
.calledWith(
{
key: 'testkey',
cert: 'testcert'
cert: 'testcert',
},
callback
)

View File

@ -37,7 +37,7 @@ describe('tail', () => {
writeLines(info.fd, 20);
const tailer = tail(info.path, {
buffer: 2
buffer: 2,
});
setTimeout(() => {
tailer.getBuffer().should.be.eql(['line18', 'line19']);

View File

@ -27,6 +27,24 @@ window.App = (function app(window, document) {
*/
var _filterValue = '';
/**
* @type {HTMLElement}
* @private
*/
var _pauseBtn;
/**
* @type {boolean}
* @private
*/
var _isPaused = false;
/**
* @type {number}
* @private
*/
var _skipCounter = 0;
/**
* @type {HTMLElement}
* @private
@ -133,17 +151,6 @@ window.App = (function app(window, document) {
window.history.replaceState(null, document.title, _url.toString());
};
/**
* @return {Boolean}
* @private
*/
var _isScrolledBottom = function() {
var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
var totalHeight = document.body.offsetHeight;
var clientHeight = document.documentElement.clientHeight; // eslint-disable-line
return totalHeight <= currentScroll + clientHeight;
};
/**
* @return void
* @private
@ -158,7 +165,7 @@ window.App = (function app(window, document) {
* @private
*/
var _updateFaviconCounter = function() {
if (_isWindowFocused) {
if (_isWindowFocused || _isPaused) {
return;
}
@ -177,9 +184,9 @@ window.App = (function app(window, document) {
if (_highlightConfig && _highlightConfig.words) {
Object.keys(_highlightConfig.words).forEach((wordCheck) => {
output = line.replace(
output = output.replace(
wordCheck,
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>'
'<span style="' + _highlightConfig.words[wordCheck] + '">' + wordCheck + '</span>',
);
});
}
@ -216,6 +223,7 @@ window.App = (function app(window, document) {
_logContainer = opts.container;
_filterInput = opts.filterInput;
_filterInput.focus();
_pauseBtn = opts.pauseBtn;
_topbar = opts.topbar;
_body = opts.body;
@ -234,13 +242,24 @@ window.App = (function app(window, document) {
_filterLogs();
});
// Pause button bind
_pauseBtn.addEventListener('mouseup', function() {
_isPaused = !_isPaused;
if (_isPaused) {
this.className += ' play';
} else {
_skipCounter = 0;
this.classList.remove('play');
}
});
// Favicon counter bind
window.addEventListener(
'blur',
function() {
_isWindowFocused = false;
},
true
true,
);
window.addEventListener(
'focus',
@ -248,7 +267,7 @@ window.App = (function app(window, document) {
_isWindowFocused = true;
_faviconReset();
},
true
true,
);
// socket.io init
@ -268,7 +287,12 @@ window.App = (function app(window, document) {
_highlightConfig = highlightConfig;
})
.on('line', function(line) {
self.log(line);
if (_isPaused) {
_skipCounter += 1;
self.log('==> SKIPPED: ' + _skipCounter + ' <==', (_skipCounter > 1));
} else {
self.log(line);
}
});
},
@ -277,8 +301,9 @@ window.App = (function app(window, document) {
*
* @param {string} data data to log
*/
log: function log(data) {
var wasScrolledBottom = _isScrolledBottom();
log: function log(data, replace = false) {
var wasScrolledBottom = window.innerHeight + Math.ceil(window.pageYOffset + 1)
>= document.body.offsetHeight;
var div = document.createElement('div');
var p = document.createElement('p');
p.className = 'inner-line';
@ -300,7 +325,11 @@ window.App = (function app(window, document) {
div.appendChild(p);
_filterElement(div);
_logContainer.appendChild(div);
if (replace) {
_logContainer.replaceChild(div, _logContainer.lastChild);
} else {
_logContainer.appendChild(div);
}
if (_logContainer.children.length > _linesLimit) {
_logContainer.removeChild(_logContainer.children[0]);
@ -311,6 +340,6 @@ window.App = (function app(window, document) {
}
_updateFaviconCounter();
}
},
};
}(window, document));

View File

@ -19,6 +19,17 @@ body {
color: #999;
}
.btn-pause {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%237f8289' viewBox='0 0 8 8'><path d='M1 1v6h2v-6h-2zm4 0v6h2v-6h-2z'></path></svg>") no-repeat center center;
background-color: #2f3238;
border: 1px solid transparent;
}
.btn-pause.play {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%237f8289' viewBox='0 0 8 8'><path d='M1 1v6l6-3-6-3z'></path></svg>") no-repeat center center;
background-color: #2f3238;
}
.form-control {
border: 0;
color: #7f8289;

View File

@ -10,14 +10,34 @@ body {
}
.navbar-brand {
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: inline-block;
width: 100%;
}
.btn-pause {
margin: 8px 0 8px;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 8 8'><path d='M1 1v6h2v-6h-2zm4 0v6h2v-6h-2z'></path></svg>")
no-repeat center center;
background-color: #e2e6ea;
background-size: contain;
cursor: pointer;
display: inline-block;
height: 34px;
width: 34px;
border: 1px solid #ccc;
}
.btn-pause.play {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 8 8'><path d='M1 1v6l6-3-6-3z'></path></svg>")
no-repeat center center;
background-color: #e2e6ea;
}
.navbar-form-custom {
padding: 8px 12px 8px 0;
padding: 8px 0;
}
.no-horiz-padding {

View File

@ -13,10 +13,13 @@
<nav class="topbar navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="row">
<div class="col-sm-10">
<span class="navbar-brand" title="__TITLE__">tail -f __TITLE__</span>
<div class="col-sm-8">
<span class="navbar-brand text-overflow" title="__TITLE__">tail -f __TITLE__</span>
</div>
<div class="col-sm-2">
<div class="col-sm-1 text-right">
<button type="button" class="btn btn-light btn-pause" data-toggle="button" aria-pressed="false" autocomplete="off"></button>
</div>
<div class="col-sm-3">
<form class="navbar-form-custom" role="search" onkeypress="return event.keyCode != 13;">
<input type="text" class="form-control query" placeholder="Filter" tabindex="1">
</form>
@ -36,13 +39,15 @@
<script src="__PATH__/app.js"></script>
<script type="text/javascript">
var socket = new io.connect('/__NAMESPACE__', {
path: '__PATH__/socket.io'
path: '__PATH__/socket.io',
transports: ['websocket']
});
window.load = App.init({
socket: socket,
container: document.getElementsByClassName('log')[0],
filterInput: document.getElementsByClassName('query')[0],
pauseBtn: document.getElementsByClassName('btn-pause')[0],
topbar: document.getElementsByClassName('topbar')[0],
body: document.getElementsByTagName('body')[0]
});