first commit
This commit is contained in:
20
uebersicht-code/server/.babelrc
Executable file
20
uebersicht-code/server/.babelrc
Executable file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": [
|
||||
"Explorer 11",
|
||||
"last 5 Safari versions",
|
||||
],
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": "2",
|
||||
"modules": "commonjs"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
]
|
||||
}
|
||||
1
uebersicht-code/server/.pydio
Normal file
1
uebersicht-code/server/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
160a04a8-07e3-427c-9d42-0ed982a3189e
|
||||
90
uebersicht-code/server/client.coffee
Executable file
90
uebersicht-code/server/client.coffee
Executable file
@@ -0,0 +1,90 @@
|
||||
redux = require 'redux'
|
||||
window.$ = require 'jquery'
|
||||
|
||||
reducer = require './src/reducer'
|
||||
listenToRemote = require './src/listen'
|
||||
sharedSocket = require './src/SharedSocket'
|
||||
render = require './src/render'
|
||||
actions = require './src/actions'
|
||||
userCssLink = null
|
||||
detectWidgetHover = require './src/detectWidgetHover'
|
||||
|
||||
|
||||
window.onload = ->
|
||||
sharedSocket.open("ws://#{window.location.host}")
|
||||
path = window.location.pathname.split('/')
|
||||
screen =
|
||||
id: Number(path[1])
|
||||
layer: path[2]
|
||||
contentEl = document.getElementById('uebersicht')
|
||||
contentEl.innerHTML = ''
|
||||
userCssLink = Array.from(document.querySelectorAll('link'))
|
||||
.find((el) => el.href.match('userMain.css'))
|
||||
|
||||
detectWidgetHover(contentEl);
|
||||
|
||||
getState (err, initialState) ->
|
||||
bail err, 10000 if err?
|
||||
store = redux.createStore(reducer, initialState)
|
||||
Object.keys(initialState.widgets).forEach (id) ->
|
||||
fetchWidget(id)
|
||||
.then (widgetImpl) -> store.dispatch(actions.showWidget(id, widgetImpl))
|
||||
|
||||
prevState = null
|
||||
store.subscribe ->
|
||||
nextState = store.getState()
|
||||
return if nextState == prevState
|
||||
render(store.getState(), screen, contentEl, store.dispatch)
|
||||
prevState = nextState
|
||||
|
||||
listenToRemote (action) ->
|
||||
if action.type == 'WIDGET_WANTS_REFRESH'
|
||||
render.rendered[action.payload]?.instance?.forceRefresh()
|
||||
else if action.type == 'WIDGET_ADDED'
|
||||
store.dispatch(action)
|
||||
return if action.payload.error
|
||||
fetchWidget(action.payload.id)
|
||||
.then (widgetImpl) ->
|
||||
store.dispatch(actions.showWidget(action.payload.id, widgetImpl))
|
||||
else if action.type == 'MASTER_STYLE_CHANGED'
|
||||
reloadUserCSS()
|
||||
else
|
||||
store.dispatch(action)
|
||||
render(initialState, screen, contentEl, store.dispatch)
|
||||
|
||||
# legacy
|
||||
window.uebersicht =
|
||||
makeBgSlice: (canvas) ->
|
||||
console.warn 'makeBgSlice has been deprecated. Please use CSS \
|
||||
backdrop-filter instead: \
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter'
|
||||
|
||||
window.addEventListener 'contextmenu', (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
getState = (callback) ->
|
||||
$.get("/state/")
|
||||
.done((response) -> callback null, JSON.parse(response))
|
||||
.fail -> callback response, null
|
||||
|
||||
fetchWidget = (id) -> new Promise (resolve, reject) ->
|
||||
scriptTag = document.createElement('SCRIPT')
|
||||
scriptTag.id = id
|
||||
scriptTag.src = '/widgets/' + id
|
||||
scriptTag.onload = ->
|
||||
document.head.removeChild(scriptTag)
|
||||
resolve(require(id))
|
||||
scriptTag.onerror = (err) ->
|
||||
document.head.removeChild(scriptTag)
|
||||
reject(err)
|
||||
document.head.appendChild(scriptTag)
|
||||
|
||||
reloadUserCSS = ->
|
||||
href = userCssLink.href.split('?')[0]
|
||||
userCssLink.href = "#{href}?#{new Date().getTime()}"
|
||||
|
||||
bail = (err, timeout = 0) ->
|
||||
console.log err if err?
|
||||
setTimeout ->
|
||||
window.location.reload(true)
|
||||
, timeout
|
||||
5648
uebersicht-code/server/package-lock.json
generated
Normal file
5648
uebersicht-code/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
uebersicht-code/server/package.json
Executable file
63
uebersicht-code/server/package.json
Executable file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "uebersicht-server",
|
||||
"version": "0.0.0",
|
||||
"description": "Node.js backend for Übersicht",
|
||||
"main": "server.coffee",
|
||||
"scripts": {
|
||||
"test": "npm run-script test-local && npm run-script test-browser",
|
||||
"test-local": "tape -r coffee-script/register spec/backend/**/* | tap-spec",
|
||||
"test-browser": "browserify -t coffeeify spec/frontend/*.* | tape-run | tap-spec",
|
||||
"start": "coffee server.coffee",
|
||||
"release": "npm run-script build-client && npm run-script build-server",
|
||||
"build-client": "browserify -i ws -t coffeeify -t babelify -r ./src/uebersicht.js:uebersicht client.coffee | uglifyjs -c > release/public/client.js",
|
||||
"build-server": "browserify -t coffeeify --node --detect-globals false --no-bundle-external server.coffee > release/server.js && cd release && npm prune --production && npm install --production --no-progress"
|
||||
},
|
||||
"author": "Felix Hageloh",
|
||||
"license": "GPL v3 <http://www.gnu.org/licenses/>",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"coffee-script": "^1.12.7",
|
||||
"sinon": "^4.0.1",
|
||||
"tap-spec": "^5.0.0",
|
||||
"tape": "^4.13.3",
|
||||
"tape-run": "^6.0.1",
|
||||
"uglify-js": "^3.10.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@emotion/core": "^10.0.35",
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"babel-plugin-emotion": "^10.0.33",
|
||||
"babelify": "^10.0.0",
|
||||
"browserify": "^16.5.2",
|
||||
"byline": "^5.0.0",
|
||||
"coffeeify": "^2.1.0",
|
||||
"connect": "^3.6.5",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"core-js": "^2.6.12",
|
||||
"cors-anywhere": "^0.4.3",
|
||||
"emotion": "^10.0.27",
|
||||
"escodegen": "^1.14.3",
|
||||
"esprima": "^2.7.3",
|
||||
"fsevents": "^2.1.3",
|
||||
"jquery": "^3.5.1",
|
||||
"minimist": "^1.2.5",
|
||||
"ms": "^2.0.0",
|
||||
"nib": "~1.1.2",
|
||||
"raf": "^3.4.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"redux": "^3.7.2",
|
||||
"serve-static": "^1.13.1",
|
||||
"source-map": "^0.7.3",
|
||||
"stylus": "^0.54.8",
|
||||
"superagent": "^3.8.3",
|
||||
"through2": "^2.0.3",
|
||||
"tmp": "0.0.33",
|
||||
"tosource": "~1.0.0",
|
||||
"ws": "^6.0.0"
|
||||
}
|
||||
}
|
||||
1
uebersicht-code/server/public/.pydio
Normal file
1
uebersicht-code/server/public/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
77b65ec0-9cd4-4c2e-b745-deb1f510bfc0
|
||||
16
uebersicht-code/server/public/index.html
Executable file
16
uebersicht-code/server/public/index.html
Executable file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Übersicht</title>
|
||||
<base href="/" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="userMain.css" />
|
||||
|
||||
<script type="text/javascript" src="client.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="uebersicht"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
uebersicht-code/server/release/.pydio
Normal file
1
uebersicht-code/server/release/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
b286686a-a486-45de-9c06-d1ceccfec10d
|
||||
BIN
uebersicht-code/server/release/localnode
Executable file
BIN
uebersicht-code/server/release/localnode
Executable file
Binary file not shown.
5648
uebersicht-code/server/release/package-lock.json
generated
Normal file
5648
uebersicht-code/server/release/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
uebersicht-code/server/release/package.json
Symbolic link
1
uebersicht-code/server/release/package.json
Symbolic link
@@ -0,0 +1 @@
|
||||
../package.json
|
||||
20
uebersicht-code/server/release/public/.babelrc
Executable file
20
uebersicht-code/server/release/public/.babelrc
Executable file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": [
|
||||
"Explorer 11",
|
||||
"last 5 Safari versions",
|
||||
],
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": "2",
|
||||
"modules": "commonjs"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
]
|
||||
}
|
||||
1
uebersicht-code/server/release/public/.pydio
Normal file
1
uebersicht-code/server/release/public/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
f43bff2f-1229-4bc5-99fd-61244f6aaaf6
|
||||
16
uebersicht-code/server/release/public/index.html
Executable file
16
uebersicht-code/server/release/public/index.html
Executable file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Übersicht</title>
|
||||
<base href="/" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="userMain.css" />
|
||||
|
||||
<script type="text/javascript" src="client.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="uebersicht"></div>
|
||||
</body>
|
||||
</html>
|
||||
22
uebersicht-code/server/release/public/main.css
Executable file
22
uebersicht-code/server/release/public/main.css
Executable file
@@ -0,0 +1,22 @@
|
||||
body,
|
||||
html {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#uebersicht {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.widget {
|
||||
position: absolute;
|
||||
}
|
||||
41
uebersicht-code/server/server.coffee
Executable file
41
uebersicht-code/server/server.coffee
Executable file
@@ -0,0 +1,41 @@
|
||||
parseArgs = require 'minimist'
|
||||
UebersichtServer = require './src/app.coffee'
|
||||
cors_proxy = require 'cors-anywhere'
|
||||
path = require 'path'
|
||||
|
||||
handleError = (err) ->
|
||||
console.log(err.message || err)
|
||||
throw err
|
||||
|
||||
try
|
||||
args = parseArgs process.argv.slice(2)
|
||||
widgetPath = path.resolve(__dirname, args.d ? args.dir ? './widgets')
|
||||
port = args.p ? args.port ? 41416
|
||||
settingsPath = path.resolve(__dirname, args.s ? args.settings ? './settings')
|
||||
publicPath = path.resolve(__dirname, './public')
|
||||
options =
|
||||
loginShell: args['login-shell']
|
||||
|
||||
server = UebersichtServer(
|
||||
Number(port),
|
||||
widgetPath,
|
||||
settingsPath,
|
||||
publicPath,
|
||||
options,
|
||||
-> console.log 'server started on port', port
|
||||
)
|
||||
server.on 'close', handleError
|
||||
server.on 'error', handleError
|
||||
|
||||
cors_host = '127.0.0.1'
|
||||
cors_port = port + 1
|
||||
cors_proxy.createServer(
|
||||
originWhitelist: ['http://127.0.0.1:' + port]
|
||||
requireHeader: ['origin']
|
||||
removeHeaders: ['cookie']
|
||||
).listen(cors_port, cors_host, ->
|
||||
console.log 'CORS Anywhere on port', cors_port
|
||||
)
|
||||
|
||||
catch e
|
||||
handleError e
|
||||
1
uebersicht-code/server/settings/.pydio
Normal file
1
uebersicht-code/server/settings/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
8fa7f9cc-9151-42bf-bd0d-b2dd84d7c529
|
||||
1
uebersicht-code/server/settings/WidgetSettings.json
Executable file
1
uebersicht-code/server/settings/WidgetSettings.json
Executable file
@@ -0,0 +1 @@
|
||||
{"top-jsx":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"index-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"top-cpu-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"circle-ci-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"top-mem-widget-index-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"Playbox-widget-index-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"simple-clock-widget-index-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"Eva-widget-index-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"GettingStarted-jsx":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]}}
|
||||
1
uebersicht-code/server/spec/.pydio
Normal file
1
uebersicht-code/server/spec/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
53020e3e-873e-430a-87dd-1ec10c273ad9
|
||||
1
uebersicht-code/server/spec/backend/.pydio
Normal file
1
uebersicht-code/server/spec/backend/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
4eca1fdb-0cf9-45cc-8990-38c4f95d5c6a
|
||||
20
uebersicht-code/server/spec/backend/SharedSocket_spec.js
Executable file
20
uebersicht-code/server/spec/backend/SharedSocket_spec.js
Executable file
@@ -0,0 +1,20 @@
|
||||
var test = require('tape');
|
||||
var WebSocket = require('ws');
|
||||
|
||||
var server = new WebSocket.Server({ port: 8890 });
|
||||
var sharedSocket = require('../../src/SharedSocket');
|
||||
var url = 'ws://localhost:8890';
|
||||
|
||||
test('subscribing listeners', (t) => {
|
||||
sharedSocket.onMessage((message) => {
|
||||
t.equal(message, 'yay');
|
||||
sharedSocket.close();
|
||||
server.close(() => t.end());
|
||||
});
|
||||
|
||||
sharedSocket.open(url);
|
||||
|
||||
server.on('connection', (ws) => {
|
||||
ws.send('yay');
|
||||
});
|
||||
});
|
||||
82
uebersicht-code/server/spec/backend/WidgetBundler_spec.js
Executable file
82
uebersicht-code/server/spec/backend/WidgetBundler_spec.js
Executable file
@@ -0,0 +1,82 @@
|
||||
const test = require('tape');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const WidgetBundler = require('../../src/WidgetBundler.js');
|
||||
const fixturePath = path.resolve(__dirname, '../test_widgets');
|
||||
const bundler = WidgetBundler();
|
||||
var callback = () => {};
|
||||
|
||||
test('bundling widgets', (t) => {
|
||||
const action = {
|
||||
type: 'added',
|
||||
filePath: path.join(fixturePath, 'widget-1.coffee'),
|
||||
id: 'widget-1',
|
||||
};
|
||||
|
||||
callback = (event) => {
|
||||
t.equal(event.type, 'added', 'it emits an "added" event');
|
||||
t.equal(typeof event.widget, 'object', 'it emits a widget object');
|
||||
t.equal(event.widget.id, 'widget-1', 'the widget object has an id');
|
||||
t.equal(
|
||||
event.widget.filePath,
|
||||
action.filePath,
|
||||
'the widget object contains the original file path',
|
||||
);
|
||||
t.equal(
|
||||
typeof event.widget.body,
|
||||
'string',
|
||||
'it also contains a string with the widget source code',
|
||||
);
|
||||
|
||||
const widget = eval(event.widget.body)('widget-1');
|
||||
t.equal(
|
||||
widget.command,
|
||||
'foo',
|
||||
'the source evals to a require function which returns the widget by id',
|
||||
);
|
||||
callback = () => {};
|
||||
t.end();
|
||||
};
|
||||
|
||||
bundler.push(action, (event) => callback(event));
|
||||
});
|
||||
|
||||
test('watching widgets', (t) => {
|
||||
callback = (event) => {
|
||||
t.equal(event.type, 'added', 'it emits another "added" event');
|
||||
t.equal(event.widget.id, 'widget-1', 'for the correct widget');
|
||||
t.equal(typeof event.widget.body, 'string', 'with the widget source code');
|
||||
t.end();
|
||||
};
|
||||
|
||||
fs.utimes(
|
||||
path.join(fixturePath, 'widget-1.coffee'),
|
||||
Date.now(),
|
||||
Date.now(),
|
||||
() => {},
|
||||
);
|
||||
});
|
||||
|
||||
test('removing widgets', (t) => {
|
||||
const action = {
|
||||
type: 'removed',
|
||||
filePath: path.join(fixturePath, 'widget-1.coffee'),
|
||||
id: 'widget-1',
|
||||
};
|
||||
|
||||
callback = (event) => {
|
||||
t.equal(event.type, 'removed', 'it emits a "removed" event');
|
||||
t.equal(event.id, 'widget-1', 'for the correct widget');
|
||||
callback = () => {};
|
||||
t.end();
|
||||
};
|
||||
|
||||
bundler.push(action, (event) => callback(event));
|
||||
});
|
||||
|
||||
test('closing', (t) => {
|
||||
bundler.close();
|
||||
t.pass('it closes');
|
||||
t.end();
|
||||
});
|
||||
78
uebersicht-code/server/spec/backend/bundleWidget_spec.js
Executable file
78
uebersicht-code/server/spec/backend/bundleWidget_spec.js
Executable file
@@ -0,0 +1,78 @@
|
||||
const test = require('tape');
|
||||
const path = require('path');
|
||||
const bundleWidget = require('../../src/bundleWidget');
|
||||
|
||||
const testDir = path.resolve(__dirname, path.join('..', 'test_widgets'));
|
||||
|
||||
test('bundling coffeescript widgets', (t) => {
|
||||
const widgetPath = path.join(testDir, 'widget-1.coffee');
|
||||
const bundle = bundleWidget('widget-id', widgetPath);
|
||||
|
||||
t.plan(2);
|
||||
t.ok(
|
||||
bundle.constructor.name === 'Browserify',
|
||||
'it returns a browserify bundle',
|
||||
);
|
||||
bundle.bundle((err, src) => {
|
||||
t.ok(
|
||||
!err && src && src.indexOf('command') > -1,
|
||||
'the source code it generates looks ok',
|
||||
);
|
||||
bundle.close();
|
||||
});
|
||||
});
|
||||
|
||||
test('bundling javascript widgets', (t) => {
|
||||
const widgetPath = path.join(testDir, 'widget-2.js');
|
||||
const bundle = bundleWidget('other-widget-id', widgetPath);
|
||||
|
||||
t.plan(2);
|
||||
t.ok(
|
||||
bundle.constructor.name === 'Browserify',
|
||||
'it returns a browserify bundle',
|
||||
);
|
||||
bundle.bundle((err, src) => {
|
||||
t.ok(
|
||||
!err && src && src.indexOf('command') > -1,
|
||||
'the source code looks ok',
|
||||
);
|
||||
bundle.close();
|
||||
});
|
||||
});
|
||||
|
||||
test('bundling jsx widgets', (t) => {
|
||||
const widgetPath = path.join(testDir, 'widget-3.jsx');
|
||||
const bundle = bundleWidget('widget-3', widgetPath);
|
||||
|
||||
t.plan(2);
|
||||
t.ok(
|
||||
bundle.constructor.name === 'Browserify',
|
||||
'it returns a browserify bundle',
|
||||
);
|
||||
bundle.bundle((err, src) => {
|
||||
t.ok(
|
||||
!err && src && src.indexOf('command') > -1,
|
||||
'the source code looks ok',
|
||||
);
|
||||
bundle.close();
|
||||
});
|
||||
});
|
||||
|
||||
test('bundling widgets with syntax errors', (t) => {
|
||||
const widgetPath = path.join(testDir, 'broken-widget.coffee');
|
||||
const bundle = bundleWidget('broken', widgetPath);
|
||||
|
||||
t.plan(2);
|
||||
t.ok(
|
||||
bundle.constructor.name === 'Browserify',
|
||||
'it returns a browserify bundle',
|
||||
);
|
||||
bundle.bundle((err, src) => {
|
||||
t.equal(
|
||||
err && err.message,
|
||||
'unexpected indentation while parsing file: ' + widgetPath,
|
||||
'it spits out an error when bundling',
|
||||
);
|
||||
bundle.close();
|
||||
});
|
||||
});
|
||||
94
uebersicht-code/server/spec/backend/command_server_spec.js
Executable file
94
uebersicht-code/server/spec/backend/command_server_spec.js
Executable file
@@ -0,0 +1,94 @@
|
||||
var test = require('tape');
|
||||
var connect = require('connect');
|
||||
var path = require('path');
|
||||
|
||||
var httpGet = require('../helpers/httpGet');
|
||||
var httpPost = require('../helpers/httpPost');
|
||||
var commandServer = require('../../src/command_server.coffee');
|
||||
|
||||
var workingDir = path.resolve(__dirname, path.join('..', 'test_widgets'));
|
||||
var server = connect().use(commandServer(workingDir)).listen(8887);
|
||||
|
||||
var url = 'http://localhost:8887/run/';
|
||||
|
||||
test('responding to POST /run/', (t) => {
|
||||
t.plan(3);
|
||||
|
||||
httpPost(url, 'echo', (res) => {
|
||||
t.equal(res.statusCode, 200, 'it reponds');
|
||||
});
|
||||
|
||||
httpPost('http://localhost:8887/foo/', 'echo', (res) => {
|
||||
t.equal(res.statusCode, 404, 'it ignores requests to other paths');
|
||||
});
|
||||
|
||||
httpGet(url, (res) => {
|
||||
t.equal(res.statusCode, 404, 'it ignores GET requests');
|
||||
});
|
||||
});
|
||||
|
||||
test('running commands', (t) => {
|
||||
t.plan(2);
|
||||
|
||||
httpPost(url, 'echo "yay"', (res, body) => {
|
||||
t.equal(body, 'yay\n', 'it runs commands');
|
||||
});
|
||||
|
||||
httpPost(url, 'pwd', (res, body) => {
|
||||
t.equal(
|
||||
body,
|
||||
workingDir + '\n',
|
||||
'it runs commands in the supplied working dir',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('shell type', (t) => {
|
||||
httpPost(url, 'echo $(shopt | grep login_shell)', (res, body) => {
|
||||
t.equal(body, 'login_shell off\n', 'it is not a login shell');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('running broken commands', (t) => {
|
||||
t.plan(2);
|
||||
|
||||
httpPost(url, 'fake-command', (res, body) => {
|
||||
t.equal(res.statusCode, 500, 'it responds with a 500 code');
|
||||
t.equal(
|
||||
body,
|
||||
'bash: line 1: fake-command: command not found\n',
|
||||
'it responds with an error message',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('forwarding stderr', (t) => {
|
||||
t.plan(2);
|
||||
|
||||
httpPost(url, 'echo "yay" >&2', (res, body) => {
|
||||
t.equal(res.statusCode, 500, 'it responds with a 500 code');
|
||||
t.equal(body, 'yay\n', 'it sends stderr along');
|
||||
});
|
||||
});
|
||||
|
||||
test('closing', (t) => {
|
||||
server.close();
|
||||
t.pass('it closes');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('using a login shell', (t) => {
|
||||
server = connect().use(commandServer(workingDir, true)).listen(8887);
|
||||
|
||||
httpPost(url, 'echo $(shopt | grep login_shell)', (res, body) => {
|
||||
const lines = body.trim().split('\n');
|
||||
t.equal(
|
||||
lines[lines.length - 1],
|
||||
'login_shell on',
|
||||
'it indeed runs in a login shell',
|
||||
);
|
||||
server.close();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
116
uebersicht-code/server/spec/backend/directory_watcher_spec.js
Executable file
116
uebersicht-code/server/spec/backend/directory_watcher_spec.js
Executable file
@@ -0,0 +1,116 @@
|
||||
var test = require('tape');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var execSync = require('child_process').execSync;
|
||||
|
||||
var DirWatcher = require('../../src/directory_watcher.coffee');
|
||||
var fixturePath = path.resolve(__dirname, '../test_widgets');
|
||||
var newWidgetPath = path.join(fixturePath, 'new-widget.coffee');
|
||||
|
||||
var stopWatching;
|
||||
var callback;
|
||||
|
||||
const throwError = (err) => {
|
||||
if (err) throw err;
|
||||
};
|
||||
|
||||
test('files that are already present in the widget dir', (t) => {
|
||||
t.timeoutAfter(300);
|
||||
var expectedWidgets = [
|
||||
path.join(fixturePath, 'widget-1.coffee'),
|
||||
path.join(fixturePath, 'widget-2.js'),
|
||||
path.join(fixturePath, 'some-dir.widget', 'index-1.coffee'),
|
||||
path.join(fixturePath, 'broken-widget.coffee'),
|
||||
path.join(fixturePath, 'invalid-widget.coffee'),
|
||||
];
|
||||
|
||||
callback = (event) => {
|
||||
if (event.type !== 'added') {
|
||||
return;
|
||||
}
|
||||
var idx = expectedWidgets.indexOf(event.filePath);
|
||||
if (idx > -1) {
|
||||
expectedWidgets.splice(idx, 1);
|
||||
}
|
||||
|
||||
if (expectedWidgets.length === 0) {
|
||||
callback = () => {};
|
||||
t.pass('it emits an event for all widgets already in the folder');
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
|
||||
stopWatching = DirWatcher(fixturePath, (event) => callback(event));
|
||||
});
|
||||
|
||||
test('adding files', (t) => {
|
||||
t.timeoutAfter(300);
|
||||
callback = (event) => {
|
||||
if (event.type === 'added' && event.filePath === newWidgetPath) {
|
||||
callback = () => {};
|
||||
t.pass('it emits an event for new files');
|
||||
t.equal(event.rootPath, fixturePath, 'the event includes the root path');
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
fs.writeFile(newWidgetPath, "command: ''", throwError);
|
||||
});
|
||||
|
||||
test('removing files', (t) => {
|
||||
t.timeoutAfter(300);
|
||||
callback = (event) => {
|
||||
if (event.type === 'removed' && event.filePath === newWidgetPath) {
|
||||
callback = () => {};
|
||||
t.pass('it emits a removed event when a widget file is removed');
|
||||
t.equal(event.rootPath, fixturePath, 'the event includes the root path');
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
fs.unlink(newWidgetPath, throwError);
|
||||
});
|
||||
|
||||
test('adding folders', (t) => {
|
||||
t.timeoutAfter(300);
|
||||
var aWidgetFolder = path.resolve(__dirname, '../tmp2');
|
||||
if (fs.existsSync(aWidgetFolder)) {
|
||||
execSync('rm -rf ' + aWidgetFolder);
|
||||
}
|
||||
|
||||
fs.mkdirSync(aWidgetFolder);
|
||||
fs.writeFileSync(path.join(aWidgetFolder, 'widget.js'), "command: 'yay'");
|
||||
|
||||
var expectedPath = path.join(fixturePath, 'another', 'widget.js');
|
||||
callback = (event) => {
|
||||
if (event.type === 'added' && event.filePath === expectedPath) {
|
||||
callback = () => {};
|
||||
t.pass('it emits an event when a subfolder containing a widget is added');
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
fs.rename(aWidgetFolder, path.join(fixturePath, 'another'), throwError);
|
||||
});
|
||||
|
||||
test('removing folders', (t) => {
|
||||
t.timeoutAfter(300);
|
||||
var expectedPath = path.join(fixturePath, 'another', 'widget.js');
|
||||
callback = (event) => {
|
||||
if (event.type === 'removed' && event.filePath === expectedPath) {
|
||||
callback = () => {};
|
||||
t.pass(
|
||||
'it emits a removed event when a subfolder containing a ' +
|
||||
'widget is removed',
|
||||
);
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
|
||||
var newPath = path.resolve(__dirname, '../tmp3');
|
||||
fs.renameSync(path.join(fixturePath, 'another'), newPath);
|
||||
execSync('rm -rf ' + newPath);
|
||||
});
|
||||
|
||||
test('stopping', (t) => {
|
||||
stopWatching();
|
||||
t.pass('it can be stopped');
|
||||
t.end();
|
||||
});
|
||||
33
uebersicht-code/server/spec/backend/dispatch_spec.js
Executable file
33
uebersicht-code/server/spec/backend/dispatch_spec.js
Executable file
@@ -0,0 +1,33 @@
|
||||
var test = require('tape');
|
||||
var WebSocket = require('ws');
|
||||
|
||||
var server = new WebSocket.Server({ port: 8888 });
|
||||
var url = 'ws://localhost:8888';
|
||||
var sharedSocket = require('../../src/SharedSocket');
|
||||
var dispatch = require('../../src/dispatch');
|
||||
|
||||
test('queuing up messages', (t) => {
|
||||
expectedMessages = ['a', 'b'];
|
||||
|
||||
server.on('connection', (ws) => {
|
||||
ws.on('message', (message) => {
|
||||
parsed = JSON.parse(message);
|
||||
|
||||
var idx = expectedMessages.indexOf(parsed);
|
||||
if (idx > -1) {
|
||||
expectedMessages.splice(idx, 1);
|
||||
}
|
||||
|
||||
if (expectedMessages.length === 0) {
|
||||
t.pass('it queues up messages and sends them once the socket opens');
|
||||
server.close(() => t.end());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dispatch('a');
|
||||
dispatch('b');
|
||||
sharedSocket.open(url);
|
||||
});
|
||||
|
||||
|
||||
26
uebersicht-code/server/spec/backend/listen_spec.js
Executable file
26
uebersicht-code/server/spec/backend/listen_spec.js
Executable file
@@ -0,0 +1,26 @@
|
||||
var test = require('tape');
|
||||
var WebSocket = require('ws');
|
||||
|
||||
var server = new WebSocket.Server({ port: 8889 });
|
||||
var sharedSocket = require('../../src/SharedSocket');
|
||||
var listen = require('../../src/listen');
|
||||
|
||||
test('listen', (t) => {
|
||||
sharedSocket.open('ws://localhost:8889');
|
||||
|
||||
listen((message) => {
|
||||
t.looseEqual(
|
||||
message,
|
||||
{ type: 'YASS', payload: 'yay' },
|
||||
'it calls listeners with deserialized messages'
|
||||
);
|
||||
server.close(() => t.end());
|
||||
});
|
||||
|
||||
server.on('connection', (ws) => {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'YASS',
|
||||
payload: 'yay',
|
||||
}));
|
||||
});
|
||||
});
|
||||
115
uebersicht-code/server/spec/backend/reducer_spec.js
Executable file
115
uebersicht-code/server/spec/backend/reducer_spec.js
Executable file
@@ -0,0 +1,115 @@
|
||||
var test = require('tape');
|
||||
var reduce = require('../../src/reducer');
|
||||
|
||||
|
||||
test('WIDGET_ADDED', (t) => {
|
||||
var action = {
|
||||
type: 'WIDGET_ADDED',
|
||||
payload: { id: 'foo', error: 'oh no', filePath: '/foo/' },
|
||||
};
|
||||
var newState = reduce({ widgets: {} }, action);
|
||||
|
||||
t.looseEqual(
|
||||
newState.widgets,
|
||||
{ foo: { id: 'foo', error: 'oh no', filePath: '/foo/' } },
|
||||
'it adds new widgets'
|
||||
);
|
||||
|
||||
t.ok(
|
||||
typeof newState.settings === 'object',
|
||||
'it creates a new settings hash if none exists'
|
||||
);
|
||||
|
||||
t.looseEqual(
|
||||
newState.settings.foo, {
|
||||
showOnAllScreens: true,
|
||||
showOnMainScreen: false,
|
||||
showOnSelectedScreens: false,
|
||||
hidden: false,
|
||||
screens: [],
|
||||
},
|
||||
'it initializes settings for a widget'
|
||||
);
|
||||
|
||||
action = {
|
||||
type: 'WIDGET_ADDED',
|
||||
payload: { id: 'foo', body: 'yay', filePath: '/foo/' },
|
||||
};
|
||||
newState = reduce(newState, action);
|
||||
|
||||
t.looseEqual(
|
||||
newState.widgets,
|
||||
{ foo: { id: 'foo', body: 'yay', filePath: '/foo/' } },
|
||||
'it updates existing widgets'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('WIDGET_REMOVED', (t) => {
|
||||
var action = { type: 'WIDGET_REMOVED', payload: 'foo' };
|
||||
var state = { widgets: {} };
|
||||
var newState = reduce(state, action);
|
||||
t.equal(state, newState, 'it ignores non existing widgets');
|
||||
|
||||
newState = reduce({
|
||||
widgets: { foo: {}, bar: {}},
|
||||
}, action);
|
||||
t.looseEqual(newState.widgets, {bar: {}}, 'it removes existing widgets');
|
||||
t.end();
|
||||
});
|
||||
|
||||
|
||||
test('WIDGET_SETTINGS_CHANGED', (t) => {
|
||||
var action = {
|
||||
type: 'WIDGET_SETTINGS_CHANGED',
|
||||
payload: { id: 'foo', settings: { a: 'b' } },
|
||||
};
|
||||
|
||||
newState = reduce({ settings: {} }, action);
|
||||
t.looseEqual(
|
||||
newState.settings,
|
||||
{ foo: { a: 'b' } },
|
||||
'it applies new settings'
|
||||
);
|
||||
|
||||
newState = reduce({ settings: { bar: {} } }, action);
|
||||
t.looseEqual(
|
||||
newState.settings,
|
||||
{ foo: { a: 'b' }, bar: {}},
|
||||
'it merges with existing settings'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('WIDGET_SET_TO_HIDE / SHOW', (t) => {
|
||||
var action = { type: 'WIDGET_SET_TO_HIDE', payload: 'bar' };
|
||||
var state = {
|
||||
settings: {
|
||||
foo: { hidden: false, some: 'other', stuff: 1 },
|
||||
bar: { hidden: false, many: 'other', things: 42 },
|
||||
},
|
||||
};
|
||||
var newState = reduce(state, action);
|
||||
t.looseEqual(
|
||||
state.settings,
|
||||
{
|
||||
foo: { hidden: false, some: 'other', stuff: 1 },
|
||||
bar: { hidden: false, many: 'other', things: 42 },
|
||||
},
|
||||
'it hides widgets'
|
||||
);
|
||||
|
||||
action = { type: 'WIDGET_SET_TO_SHOW', payload: 'bar' };
|
||||
newState = reduce(newState, action);
|
||||
t.looseEqual(
|
||||
state.settings,
|
||||
{
|
||||
foo: { hidden: false, some: 'other', stuff: 1 },
|
||||
bar: { hidden: false, many: 'other', things: 42 },
|
||||
},
|
||||
'it shows widgets'
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
102
uebersicht-code/server/spec/backend/resolveWidget_spec.js
Executable file
102
uebersicht-code/server/spec/backend/resolveWidget_spec.js
Executable file
@@ -0,0 +1,102 @@
|
||||
const test = require('tape');
|
||||
const resolveWidget = require('../../src/resolveWidget.js');
|
||||
|
||||
test('resolving widget actions from file events', (t) => {
|
||||
const action = resolveWidget({
|
||||
type: 'added',
|
||||
filePath: '/widget/dir/widget File name.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
|
||||
t.plan(3);
|
||||
t.equal(action.id, 'widget_File_name-js', 'it derives a widget id');
|
||||
t.equal(
|
||||
action.filePath, '/widget/dir/widget File name.js',
|
||||
'it contains the original file path'
|
||||
);
|
||||
|
||||
const action2 = resolveWidget({
|
||||
type: 'removed',
|
||||
filePath: '/widget/dir/widget File name.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
|
||||
t.ok(
|
||||
action.type === 'added' && action2.type === 'removed',
|
||||
'it passes on the action type'
|
||||
);
|
||||
});
|
||||
|
||||
test('separating widgets from non-wigets', (t) => {
|
||||
var action = resolveWidget({
|
||||
filePath: '/widget/dir/file.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!!action, 'it accepts js files');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/file.coffee',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!!action, 'it accepts coffee files');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/file.jsx',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!!action, 'it accepts jsx files');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/file.txt',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!action, 'it ignores other files');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/node_modules/file.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!action, 'it ignores files inside node_modules');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/src/file.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!action, 'it ignores files inside a src dir');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/lib/file.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!action, 'it ignores files inside a lib dir');
|
||||
|
||||
action = resolveWidget({
|
||||
filePath: '/widget/dir/some/other/dir/file.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.ok(!!action, 'it detects widgets in any other dir');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deriving widget ids', (t) => {
|
||||
var action = resolveWidget({
|
||||
type: 'added',
|
||||
filePath: '/widget/dir/spaces in the Name.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.equal(
|
||||
action.id, 'spaces_in____the__Name-js', 'it replaces spaces with underscores'
|
||||
);
|
||||
|
||||
action = resolveWidget({
|
||||
type: 'added',
|
||||
filePath: '/widget/dir/some-dir/widget.js',
|
||||
rootPath: '/widget/dir/',
|
||||
});
|
||||
t.equal(
|
||||
action.id, 'some-dir-widget-js', 'it replaces slashes with dashes'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
36
uebersicht-code/server/spec/backend/validateWidget_spec.js
Executable file
36
uebersicht-code/server/spec/backend/validateWidget_spec.js
Executable file
@@ -0,0 +1,36 @@
|
||||
var test = require('tape');
|
||||
var validateWidget = require('../../src/validateWidget');
|
||||
|
||||
test('checking for empty implementations', (t) => {
|
||||
var issues = validateWidget();
|
||||
t.ok(
|
||||
issues.indexOf('empty implementation') > -1,
|
||||
'it does not allow them'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('checking for commands', (t) => {
|
||||
var issues = validateWidget({});
|
||||
t.ok(
|
||||
issues.indexOf('no command given') > -1,
|
||||
'it complains if when there is no command'
|
||||
);
|
||||
|
||||
issues = validateWidget({ refreshFrequency: false });
|
||||
t.ok(
|
||||
issues.indexOf('no command given') === -1,
|
||||
'it allows no commands when refreshFrequency is false'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('valid widgets', (t) => {
|
||||
var issues = validateWidget({
|
||||
command: 'yay'
|
||||
});
|
||||
t.ok(issues.length === 0, 'it finds no issues');
|
||||
t.end();
|
||||
});
|
||||
83
uebersicht-code/server/spec/backend/widgetify_spec.js
Executable file
83
uebersicht-code/server/spec/backend/widgetify_spec.js
Executable file
@@ -0,0 +1,83 @@
|
||||
var test = require('tape');
|
||||
var widgetify = require('../../src/widgetify');
|
||||
var through = require('through2');
|
||||
|
||||
function grabOutput(then) {
|
||||
var output = '';
|
||||
return through(
|
||||
(chunk, enc, next) => { output += chunk; next(); },
|
||||
(next) => { then(output); next(); }
|
||||
);
|
||||
}
|
||||
|
||||
test('transforming valid widgets', (t) => {
|
||||
var transform = widgetify('path/', { id: 'foo' });
|
||||
var src = `
|
||||
var color = '#ff';
|
||||
var stuff = 1+2;
|
||||
color = color + 'f';
|
||||
({
|
||||
foo: 14,
|
||||
style: 'color: ' + color,
|
||||
refreshFrequency: '2s'
|
||||
})
|
||||
`;
|
||||
|
||||
transform.pipe( grabOutput((transformed) => {
|
||||
const module = {};
|
||||
new Function('module', transformed)(module);
|
||||
|
||||
t.ok(
|
||||
typeof module.exports === 'object',
|
||||
'it assigns the last object expression to module.exports'
|
||||
);
|
||||
t.equal(
|
||||
module.exports.id, 'foo',
|
||||
'it adds the widget id'
|
||||
);
|
||||
t.equal(
|
||||
module.exports.refreshFrequency, 2000,
|
||||
'it parses string refresh frequencies'
|
||||
);
|
||||
t.equal(
|
||||
module.exports.css, '#foo {\n color: #fff;\n}\n',
|
||||
'it parses and scopes styles, including interpolated variables'
|
||||
);
|
||||
t.equal(
|
||||
module.exports.style, undefined,
|
||||
'it cleans up the style property'
|
||||
);
|
||||
|
||||
t.end();
|
||||
}));
|
||||
|
||||
transform.write(src);
|
||||
transform.end();
|
||||
});
|
||||
|
||||
test('transforming a widget with a syntax error', (t) => {
|
||||
var transform = widgetify('path/', { id: 'foo' });
|
||||
var src = `
|
||||
({
|
||||
foo: 14,
|
||||
style: 'color: ' + color,
|
||||
refreshFrequency: '2s'
|
||||
})
|
||||
`;
|
||||
|
||||
transform
|
||||
.on('error', (e) => {
|
||||
t.pass('it emits an error');
|
||||
t.ok(
|
||||
e.name === 'ReferenceError' && e.message === 'color is not defined',
|
||||
'the error looks ok'
|
||||
);
|
||||
t.end();
|
||||
})
|
||||
.pipe(grabOutput((transformed) => {
|
||||
t.ok(!transformed, 'and there is no outout');
|
||||
}));
|
||||
|
||||
transform.write(src);
|
||||
transform.end();
|
||||
});
|
||||
1
uebersicht-code/server/spec/frontend/.pydio
Normal file
1
uebersicht-code/server/spec/frontend/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
0a8f99dc-3af2-4fb0-8999-8ba5fba7c2f9
|
||||
195
uebersicht-code/server/spec/frontend/render_spec.js
Executable file
195
uebersicht-code/server/spec/frontend/render_spec.js
Executable file
@@ -0,0 +1,195 @@
|
||||
var test = require('tape');
|
||||
|
||||
var render = require('../../src/render');
|
||||
var domEl = document.createElement('div');
|
||||
document.body.appendChild(domEl); // needed to use selectors
|
||||
|
||||
function buildWidget(id) {
|
||||
return {
|
||||
id: id,
|
||||
implementation: {id: id, refreshFrequency: false},
|
||||
mtime: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
var state = {
|
||||
widgets: {
|
||||
foo: buildWidget('foo'),
|
||||
bar: buildWidget('bar'),
|
||||
},
|
||||
settings: {},
|
||||
screens: ['123'],
|
||||
};
|
||||
|
||||
test('rendering a clean slate', (t) => {
|
||||
render(state, '123', domEl);
|
||||
t.equal(domEl.childNodes.length, 2, 'it renders 2 widgets');
|
||||
t.ok(!!domEl.querySelector('#foo'), 'it renders widget foo');
|
||||
t.ok(!!domEl.querySelector('#bar'), 'it renders widget bar');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rendering new widgets', (t) => {
|
||||
state.widgets.baz = buildWidget('baz');
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(domEl.childNodes.length, 3, 'it renders 3 widgets');
|
||||
t.ok(!!domEl.querySelector('#foo'), 'it renders widget foo');
|
||||
t.ok(!!domEl.querySelector('#bar'), 'it renders widget bar');
|
||||
t.ok(!!domEl.querySelector('#baz'), 'it renders widget baz');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('destroying removed widgets', (t) => {
|
||||
delete state.widgets.bar;
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(domEl.childNodes.length, 2, 'it leaves 2');
|
||||
t.ok(!!domEl.querySelector('#foo'), 'it does not remove widget foo');
|
||||
t.ok(!!domEl.querySelector('#baz'), 'it does not remove widget baz');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rendering widgets that are visible on all screens', (t) => {
|
||||
state.settings.baz = {
|
||||
showOnAllScreens: true,
|
||||
};
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(domEl.childNodes.length, 2, 'it renders them');
|
||||
|
||||
render(state, '678', domEl);
|
||||
t.equal(domEl.childNodes.length, 2, 'it renders them on any screen');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rendering widgets that are pinned to the main screen', (t) => {
|
||||
state.settings.baz = {
|
||||
showOnAllScreens: false,
|
||||
showOnMainScreen: true,
|
||||
};
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
2,
|
||||
'it renders them if current screen is main',
|
||||
);
|
||||
|
||||
render(state, '156', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
1,
|
||||
'it does not render them if current screen is mot main',
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rendering widgets that are pinned to selected screens', (t) => {
|
||||
state.settings.baz = {
|
||||
showOnAllScreens: false,
|
||||
showOnMainScreen: false,
|
||||
showOnSelectedScreens: true,
|
||||
};
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
1,
|
||||
'it does not render them if no screen is selected',
|
||||
);
|
||||
|
||||
state.settings.baz.screens = ['567'];
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
1,
|
||||
'it does not render them if current screen is not in selected screens',
|
||||
);
|
||||
|
||||
state.settings.baz.screens = ['567', '123'];
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
2,
|
||||
'it renders them if current screen is in selected screens',
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('performance when re-rendering', (t) => {
|
||||
var prevNode = domEl.querySelector('#foo');
|
||||
render(state, '123', domEl);
|
||||
var newNode = domEl.querySelector('#foo');
|
||||
|
||||
t.ok(
|
||||
prevNode === newNode,
|
||||
'it does not re-render nodes if it does not need to',
|
||||
);
|
||||
|
||||
prevNode = domEl.querySelector('#foo');
|
||||
|
||||
// new mtime
|
||||
state.widgets.foo = buildWidget('foo');
|
||||
render(state, '123', domEl);
|
||||
newNode = domEl.querySelector('#foo');
|
||||
|
||||
t.ok(prevNode !== newNode, 'it does re-render nodes when it has to');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rendering background widgets', (t) => {
|
||||
var state = {
|
||||
widgets: {
|
||||
foo: buildWidget('foo'),
|
||||
},
|
||||
settings: {foo: {inBackground: true, showOnAllScreens: true}},
|
||||
screens: ['123'],
|
||||
};
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
1,
|
||||
'it renders them if window.isBackground is not set',
|
||||
);
|
||||
|
||||
window.isBackground = false;
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
0,
|
||||
'it does not render them if window.background is false',
|
||||
);
|
||||
|
||||
window.isBackground = true;
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
1,
|
||||
'it renders them if window.background is true',
|
||||
);
|
||||
|
||||
window.isBackground = false;
|
||||
|
||||
var state = {
|
||||
widgets: {
|
||||
foo: buildWidget('foo'),
|
||||
},
|
||||
settings: {foo: {showOnAllScreens: true}},
|
||||
screens: ['123'],
|
||||
};
|
||||
|
||||
render(state, '123', domEl);
|
||||
t.equal(
|
||||
domEl.childNodes.length,
|
||||
1,
|
||||
'it renders them in foreground if setting is undefined',
|
||||
);
|
||||
|
||||
window.isBackground = undefined;
|
||||
t.end();
|
||||
});
|
||||
412
uebersicht-code/server/spec/frontend/widget_spec.js
Executable file
412
uebersicht-code/server/spec/frontend/widget_spec.js
Executable file
@@ -0,0 +1,412 @@
|
||||
var test = require('tape');
|
||||
var sinon = require('sinon');
|
||||
var tosource = require('tosource');
|
||||
var Widget = require('../../src/Widget.js');
|
||||
|
||||
function makeFakeServer() {
|
||||
var server = sinon.fakeServer.create();
|
||||
server.respondToRun = function respondToRun(body) {
|
||||
server.respondWith('POST', '/run/', [
|
||||
status,
|
||||
{ 'Content-Type': 'text/plain' },
|
||||
body,
|
||||
]);
|
||||
};
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function buildWidget(impl) {
|
||||
return Widget({implementation: impl});
|
||||
}
|
||||
|
||||
test('widget creation', (t) => {
|
||||
var widget = buildWidget({ command: '', id: 'foo', css: 'background: red' });
|
||||
var el = widget.create();
|
||||
|
||||
t.ok(
|
||||
el && el.tagName === 'DIV',
|
||||
'it creates a wrapper dom element for a widget'
|
||||
);
|
||||
|
||||
t.ok(
|
||||
!!el.querySelector('#foo'),
|
||||
'it creates an element for the widget itself'
|
||||
);
|
||||
|
||||
var style = el.querySelector('style');
|
||||
t.ok(!!style, "it includes a style tag for the widget's css");
|
||||
t.ok(
|
||||
style.innerHTML.indexOf('background: red') > -1,
|
||||
"the tag includes the widget's style"
|
||||
);
|
||||
|
||||
widget.destroy();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('defaults', (t) => {
|
||||
var implementation = buildWidget({ command: '', id: 'foo', css: '' })
|
||||
.implementation();
|
||||
|
||||
t.equal(
|
||||
implementation.refreshFrequency, 1000,
|
||||
'it sets the refresh frequency to 1s'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
typeof implementation.render, 'function',
|
||||
'it provides a default render function'
|
||||
);
|
||||
|
||||
t.ok(
|
||||
implementation.render('stuff') === 'stuff',
|
||||
'the default render method returns what is passed to it'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
typeof implementation.afterRender, 'function',
|
||||
'it provides a default afterRender function'
|
||||
);
|
||||
|
||||
implementation = buildWidget({
|
||||
id: 'foo',
|
||||
command: '',
|
||||
css: '',
|
||||
refreshFrequency: 42,
|
||||
render: () => 'render!',
|
||||
afterRender: () => 'afterRender!',
|
||||
}).implementation();
|
||||
|
||||
t.equal(
|
||||
implementation.refreshFrequency, 42,
|
||||
"it doesn't override the refreshFrequency"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
implementation.render(), 'render!',
|
||||
"it doesn't override the render method"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
implementation.afterRender(), 'afterRender!',
|
||||
"it doesn't override the afterRender method"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('internal api', (t) => {
|
||||
var api = buildWidget({ command: '', id: 'foo', css: '' })
|
||||
.internalApi();
|
||||
|
||||
t.equal(typeof api.start, 'function', 'it has a start method');
|
||||
t.equal(typeof api.stop, 'function', 'it has a stop method');
|
||||
t.equal(typeof api.refresh, 'function', 'it has a refresh method');
|
||||
t.equal(typeof api.run, 'function', 'it has a run method');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('running commands', (t) => {
|
||||
var clock = sinon.useFakeTimers();
|
||||
var instance = buildWidget({ id: 'foo', command: 'command', css: ''});
|
||||
var widget = instance.implementation();
|
||||
var server = makeFakeServer();
|
||||
|
||||
server.respondToRun('some output');
|
||||
server.autoRespond = true;
|
||||
server.respondImmediately = true;
|
||||
var requests = server.requests;
|
||||
|
||||
instance.create();
|
||||
|
||||
t.equal(
|
||||
server.requests[0].requestBody, 'command',
|
||||
'it sends the command to the server'
|
||||
);
|
||||
|
||||
clock.tick(1000);
|
||||
t.ok(
|
||||
requests.length === 2 && requests[1].requestBody === 'command',
|
||||
'for every tick'
|
||||
);
|
||||
|
||||
widget.command = 'new command';
|
||||
clock.tick(1000);
|
||||
t.equal(
|
||||
requests[2].requestBody, 'new command',
|
||||
'when updating command it sends the new command to the server'
|
||||
);
|
||||
|
||||
t.end();
|
||||
instance.destroy();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
test('manually running commands', (t) => {
|
||||
var widget = buildWidget({ id: 'foo', command: '', css: ''}).internalApi();
|
||||
var server = makeFakeServer();
|
||||
server.respondToRun('some output');
|
||||
|
||||
widget.run('some command', (err, output) => {
|
||||
t.equal(null, err, 'sends no errors');
|
||||
t.equal(output, 'some output', 'it responds with the output');
|
||||
server.restore();
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.equal(
|
||||
server.requests[0].requestBody, 'some command',
|
||||
'it sends the command to the server'
|
||||
);
|
||||
|
||||
server.respond();
|
||||
});
|
||||
|
||||
test('standard rendering', (t) => {
|
||||
var clock = sinon.useFakeTimers();
|
||||
var instance = buildWidget({
|
||||
id: 'foo',
|
||||
command: '',
|
||||
refreshFrequency: 100,
|
||||
numRenders: 0,
|
||||
render(out) {
|
||||
this.numRenders++;
|
||||
return `rendered ${this.numRenders} ${out}`;
|
||||
},
|
||||
});
|
||||
|
||||
var server = makeFakeServer();
|
||||
server.respondToRun('Hello World!');
|
||||
server.autoRespond = true;
|
||||
server.respondImmediately = true;
|
||||
|
||||
var domEl = instance.create();
|
||||
var contentEl = domEl.querySelector('.widget');
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 1 Hello World!',
|
||||
'it does an initial render'
|
||||
);
|
||||
|
||||
clock.tick(100);
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 2 Hello World!',
|
||||
'it renders after the first tick'
|
||||
);
|
||||
|
||||
clock.tick(100);
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 3 Hello World!',
|
||||
'it renders after the second tick'
|
||||
);
|
||||
|
||||
var internalApi = instance.internalApi();
|
||||
internalApi.stop();
|
||||
clock.tick(100);
|
||||
clock.tick(100);
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 3 Hello World!',
|
||||
'it pauses when stopped'
|
||||
);
|
||||
|
||||
internalApi.start();
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 4 Hello World!',
|
||||
'it resumes when started'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 4 Hello World!',
|
||||
'it continues rendering'
|
||||
);
|
||||
|
||||
instance.destroy();
|
||||
server.restore();
|
||||
clock.restore();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rendering when refreshFrequency is false', (t) => {
|
||||
var clock = sinon.useFakeTimers();
|
||||
var instance = buildWidget({
|
||||
id: 'foo',
|
||||
refreshFrequency: false,
|
||||
numRenders: 0,
|
||||
render(out) {
|
||||
this.numRenders++;
|
||||
return `rendered ${this.numRenders} times`;
|
||||
},
|
||||
});
|
||||
|
||||
var server = makeFakeServer();
|
||||
server.respondToRun('');
|
||||
server.autoRespond = true;
|
||||
server.respondImmediately = true;
|
||||
|
||||
var domEl = instance.create();
|
||||
var contentEl = domEl.querySelector('.widget');
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 1 times',
|
||||
'it does an initial render'
|
||||
);
|
||||
|
||||
clock.tick(1000);
|
||||
clock.tick(1000);
|
||||
t.equal(
|
||||
contentEl.textContent, 'rendered 1 times',
|
||||
'it does\'t render after that'
|
||||
);
|
||||
|
||||
clock.restore();
|
||||
instance.destroy();
|
||||
server.restore();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('refreshing manually', (t) => {
|
||||
var instance = buildWidget({
|
||||
id: 'bar',
|
||||
refreshFrequency: false,
|
||||
command: 'refresh me',
|
||||
css: '',
|
||||
});
|
||||
var domEl = instance.create();
|
||||
var server = makeFakeServer();
|
||||
var internalApi = instance.internalApi();
|
||||
|
||||
server.respondToRun('some output');
|
||||
internalApi.start();
|
||||
|
||||
internalApi.refresh();
|
||||
t.equal(server.requests[0].requestBody, 'refresh me');
|
||||
|
||||
server.respond();
|
||||
t.equal(
|
||||
domEl.textContent.replace(/^\s+/g, ''), 'some output',
|
||||
'it renders the output to the DOM'
|
||||
);
|
||||
|
||||
instance.destroy();
|
||||
server.restore();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('afterRender hook', (t) => {
|
||||
var server = makeFakeServer();
|
||||
var clock = sinon.useFakeTimers();
|
||||
|
||||
server.respondToRun('Hello World!');
|
||||
server.autoRespond = true;
|
||||
|
||||
var instance = buildWidget({
|
||||
id: 'fred',
|
||||
command: '',
|
||||
refreshFrequency: 100,
|
||||
numCalls: 0,
|
||||
afterRender(el) {
|
||||
this.numCalls++;
|
||||
el.innerHTML = `called ${this.numCalls} times`;
|
||||
},
|
||||
});
|
||||
|
||||
var domEl = instance.create();
|
||||
clock.tick(300);
|
||||
t.equal(
|
||||
domEl.querySelector('.widget').textContent, 'called 3 times',
|
||||
'it get\'s called after every render, with the content dom element'
|
||||
);
|
||||
|
||||
instance.destroy();
|
||||
server.restore();
|
||||
clock.restore();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('update', (t) => {
|
||||
var instance = buildWidget({
|
||||
id: 'fred',
|
||||
command: '',
|
||||
refreshFrequency: false,
|
||||
update(output, el) {
|
||||
el.innerHTML = `content: ${el.textContent}, output: ${output}`;
|
||||
},
|
||||
});
|
||||
|
||||
var server = makeFakeServer();
|
||||
server.respondToRun('stuff');
|
||||
server.autoRespond = true;
|
||||
server.respondImmediately = true;
|
||||
|
||||
var domEl = instance.create();
|
||||
var contentEl = domEl.querySelector('.widget');
|
||||
t.equal(
|
||||
contentEl.textContent, 'content: stuff, output: stuff',
|
||||
'it calls update after rendering, with the output and widget dom el'
|
||||
);
|
||||
|
||||
instance.destroy();
|
||||
server.restore();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('error handling', (t) => {
|
||||
var instance = buildWidget({
|
||||
id: 'foo',
|
||||
refreshFrequency: false,
|
||||
render() { throw new Error('something went sorry'); },
|
||||
update() { throw new Error('should not call update when render fails'); },
|
||||
});
|
||||
|
||||
var domEl = instance.create();
|
||||
t.equal(
|
||||
domEl.querySelector('.widget').textContent, 'something went sorry\n',
|
||||
'it catches and renders errors in render()'
|
||||
);
|
||||
|
||||
instance.destroy();
|
||||
instance = buildWidget({
|
||||
id: 'foo',
|
||||
refreshFrequency: false,
|
||||
update() { throw new Error('ohoh'); },
|
||||
});
|
||||
|
||||
domEl = instance.create();
|
||||
t.equal(
|
||||
domEl.querySelector('.widget').textContent, 'ohoh\n',
|
||||
'it catches and renders errors in update()'
|
||||
);
|
||||
|
||||
instance.destroy();
|
||||
instance = buildWidget({
|
||||
id: 'foo',
|
||||
command: 'yay',
|
||||
refreshFrequency: 100,
|
||||
});
|
||||
|
||||
var clock = sinon.useFakeTimers();
|
||||
var server = sinon.fakeServer.create({ respondImmediately: true });
|
||||
server.respondWith('POST', '/run/', [ 500, {}, 'oh noez!']);
|
||||
server.respondImmediately = true;
|
||||
|
||||
domEl = instance.create();
|
||||
server.respond();
|
||||
|
||||
t.equal(
|
||||
domEl.querySelector('.widget').textContent, 'oh noez!\n',
|
||||
'it renders command errors'
|
||||
);
|
||||
|
||||
server.respondWith('POST', '/run/', [ 200, {}, 'all good']);
|
||||
clock.tick(100);
|
||||
server.respond();
|
||||
|
||||
t.equal(
|
||||
domEl.querySelector('.widget').textContent, 'all good',
|
||||
'it recovers from command errors'
|
||||
);
|
||||
|
||||
clock.restore();
|
||||
server.restore();
|
||||
instance.destroy();
|
||||
t.end();
|
||||
});
|
||||
1
uebersicht-code/server/spec/helpers/.pydio
Normal file
1
uebersicht-code/server/spec/helpers/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
11e03a6d-84be-4539-a4b3-5cd819ff4112
|
||||
12
uebersicht-code/server/spec/helpers/httpGet.js
Executable file
12
uebersicht-code/server/spec/helpers/httpGet.js
Executable file
@@ -0,0 +1,12 @@
|
||||
http = require('http');
|
||||
|
||||
module.exports = function httpGet(url, callback) {
|
||||
var buffer = '';
|
||||
|
||||
http.get(url, function(res) {
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => buffer += chunk );
|
||||
res.on('end', () => callback(res, buffer) );
|
||||
});
|
||||
};
|
||||
|
||||
18
uebersicht-code/server/spec/helpers/httpPost.js
Executable file
18
uebersicht-code/server/spec/helpers/httpPost.js
Executable file
@@ -0,0 +1,18 @@
|
||||
var http = require('http');
|
||||
var URL = require('url');
|
||||
|
||||
module.exports = function httpPost(url, postData, callback) {
|
||||
var options = URL.parse(url);
|
||||
options.method = 'POST';
|
||||
options.headers = { 'Content-Length': postData.length };
|
||||
|
||||
var req = http.request(options, (res) => {
|
||||
var buffer = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => buffer += chunk);
|
||||
res.on('end', () => callback(res, buffer));
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
};
|
||||
1
uebersicht-code/server/spec/test_files/.pydio
Normal file
1
uebersicht-code/server/spec/test_files/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
f57a5179-eb69-450a-b864-836643c3a65b
|
||||
1
uebersicht-code/server/spec/test_files/WidgetSettings.json
Executable file
1
uebersicht-code/server/spec/test_files/WidgetSettings.json
Executable file
@@ -0,0 +1 @@
|
||||
{"spec-test_widgets-broken-widget-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"spec-test_widgets-widget-2-js":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"spec-test_widgets-widget-1-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"spec-test_widgets-invalid-widget-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]},"spec-test_widgets-some-dir-widget-index-1-coffee":{"showOnAllScreens":true,"showOnMainScreen":false,"showOnSelectedScreens":false,"hidden":false,"screens":[]}}
|
||||
1
uebersicht-code/server/spec/test_widgets/.pydio
Normal file
1
uebersicht-code/server/spec/test_widgets/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
126b9e6a-6167-4ab9-8404-6c546f66fba6
|
||||
5
uebersicht-code/server/spec/test_widgets/broken-widget.coffee
Executable file
5
uebersicht-code/server/spec/test_widgets/broken-widget.coffee
Executable file
@@ -0,0 +1,5 @@
|
||||
command: ""
|
||||
|
||||
render: ->
|
||||
'this is a broken widget'
|
||||
'unexpected indentation'
|
||||
0
uebersicht-code/server/spec/test_widgets/garbage.exe
Executable file
0
uebersicht-code/server/spec/test_widgets/garbage.exe
Executable file
3
uebersicht-code/server/spec/test_widgets/invalid-widget.coffee
Executable file
3
uebersicht-code/server/spec/test_widgets/invalid-widget.coffee
Executable file
@@ -0,0 +1,3 @@
|
||||
refreshFrequency: 1000
|
||||
|
||||
render: (output) -> output
|
||||
@@ -0,0 +1 @@
|
||||
3d8f802f-b31a-4e65-b7df-87a88645cd2c
|
||||
2
uebersicht-code/server/spec/test_widgets/some-dir.widget/index-1.coffee
Executable file
2
uebersicht-code/server/spec/test_widgets/some-dir.widget/index-1.coffee
Executable file
@@ -0,0 +1,2 @@
|
||||
command: "do stuff"
|
||||
refreshFrequency: 3000
|
||||
BIN
uebersicht-code/server/spec/test_widgets/test.jpg
Executable file
BIN
uebersicht-code/server/spec/test_widgets/test.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
14
uebersicht-code/server/spec/test_widgets/widget-1.coffee
Executable file
14
uebersicht-code/server/spec/test_widgets/widget-1.coffee
Executable file
@@ -0,0 +1,14 @@
|
||||
command: "foo"
|
||||
|
||||
refreshFrequency: 1000
|
||||
|
||||
style: """
|
||||
bottom: 10px
|
||||
left: 100px
|
||||
|
||||
div
|
||||
font-size: 12px
|
||||
"""
|
||||
|
||||
render: (output) ->
|
||||
output
|
||||
2
uebersicht-code/server/spec/test_widgets/widget-2.js
Executable file
2
uebersicht-code/server/spec/test_widgets/widget-2.js
Executable file
@@ -0,0 +1,2 @@
|
||||
command: "bar",
|
||||
refreshFrequency: 1020
|
||||
13
uebersicht-code/server/spec/test_widgets/widget-3.jsx
Executable file
13
uebersicht-code/server/spec/test_widgets/widget-3.jsx
Executable file
@@ -0,0 +1,13 @@
|
||||
export const command = "echo Hello World!"
|
||||
|
||||
export const refreshFrequency = 5000 // ms
|
||||
|
||||
export const render = ({ output }) => (
|
||||
<h1>{output}</h1>
|
||||
)
|
||||
|
||||
export const className = `
|
||||
left: 20px
|
||||
top: 20px
|
||||
color: #fff
|
||||
`
|
||||
1
uebersicht-code/server/src/.pydio
Normal file
1
uebersicht-code/server/src/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
4e7c972d-b67e-4e30-8bc1-415c8586ba61
|
||||
142
uebersicht-code/server/src/ClassicWidget.coffee
Executable file
142
uebersicht-code/server/src/ClassicWidget.coffee
Executable file
@@ -0,0 +1,142 @@
|
||||
$ = require('jquery')
|
||||
window.jQuery = $
|
||||
|
||||
Timer = require('./Timer')
|
||||
runCommand = require('./runCommand')
|
||||
runShellCommand = require('./runShellCommand')
|
||||
|
||||
defaults =
|
||||
id: 'widget'
|
||||
refreshFrequency: 1000
|
||||
render: (output) -> output
|
||||
afterRender: ->
|
||||
|
||||
# This is a wrapper (something like a base class), around the
|
||||
# specific implementation of a widget.
|
||||
module.exports = ClassicWidget = (widgetObject) ->
|
||||
api = {}
|
||||
internalApi = {}
|
||||
|
||||
el = null
|
||||
contentEl = null
|
||||
timer = null
|
||||
started = false
|
||||
rendered = false
|
||||
mounted = false
|
||||
commandLoop = null
|
||||
implementation = {}
|
||||
currentError = null
|
||||
|
||||
init = (widget) ->
|
||||
currentError = if widget.error then JSON.parse(widget.error) else null
|
||||
implementation = widget.implementation || {}
|
||||
implementation.id == widget.id
|
||||
|
||||
implementation[k] ?= v for k, v of defaults
|
||||
implementation[k] ||= v for k, v of internalApi
|
||||
|
||||
commandLoop = Timer().map (done) ->
|
||||
runCommand implementation, (err, output) ->
|
||||
redraw(err, output)
|
||||
done(implementation.refreshFrequency)
|
||||
|
||||
api
|
||||
|
||||
# renders and returns the widget's dom element
|
||||
api.create = ->
|
||||
el = document.createElement 'div'
|
||||
contentEl = document.createElement 'div'
|
||||
contentEl.id = implementation.id
|
||||
contentEl.className = 'widget'
|
||||
el.innerHTML = "<style>#{implementation.css}</style>\n"
|
||||
el.appendChild(contentEl)
|
||||
|
||||
start()
|
||||
el
|
||||
|
||||
api.destroy = ->
|
||||
stop()
|
||||
return unless el?
|
||||
el.parentNode?.removeChild(el)
|
||||
el = null
|
||||
contentEl = null
|
||||
rendered = false
|
||||
|
||||
api.update = (newImplementation) ->
|
||||
parentEl = el.parentNode
|
||||
api.destroy()
|
||||
init(newImplementation)
|
||||
parentEl.appendChild(api.create())
|
||||
|
||||
api.domEl = -> el
|
||||
|
||||
api.isRendered = ->
|
||||
!!el
|
||||
|
||||
api.internalApi = ->
|
||||
internalApi
|
||||
|
||||
api.implementation = ->
|
||||
implementation
|
||||
|
||||
api.forceRefresh = ->
|
||||
internalApi.refresh()
|
||||
|
||||
# starts the widget refresh cycle
|
||||
internalApi.start = start = ->
|
||||
return redraw(currentError) if currentError
|
||||
commandLoop.start()
|
||||
|
||||
# stops the widget refresh cycle
|
||||
internalApi.stop = stop = ->
|
||||
commandLoop.stop()
|
||||
|
||||
# run widget command and redraw the widget
|
||||
internalApi.refresh = refresh = ->
|
||||
return redraw() unless implementation.command?
|
||||
commandLoop.forceTick()
|
||||
|
||||
# runs command in the shell and calls callback with the result (err, stdout)
|
||||
internalApi.run = run = (command, callback) ->
|
||||
runShellCommand(command, callback)
|
||||
|
||||
redraw = (error, output) ->
|
||||
if error
|
||||
contentEl.style.fontFamily = 'monospace'
|
||||
contentEl.style.fontSize = '12px'
|
||||
contentEl.style.whiteSpace = 'pre'
|
||||
contentEl.style.background = '#fff'
|
||||
contentEl.style.padding = '20px'
|
||||
contentEl.innerHTML = error.message + '\n' + (error.lines || '')
|
||||
console.error "#{implementation.id}:", error
|
||||
return rendered = false
|
||||
else
|
||||
contentEl.style.fontFamily = ''
|
||||
contentEl.style.fontSize = ''
|
||||
contentEl.style.whiteSpace = ''
|
||||
contentEl.style.background = ''
|
||||
contentEl.style.padding = ''
|
||||
|
||||
try
|
||||
renderOutput output
|
||||
catch e
|
||||
redraw(e)
|
||||
|
||||
renderOutput = (output) ->
|
||||
if implementation.update? and rendered
|
||||
implementation.update(output, contentEl)
|
||||
else
|
||||
contentEl.innerHTML = implementation.render(output)
|
||||
loadScripts(contentEl)
|
||||
|
||||
implementation.afterRender(contentEl)
|
||||
rendered = true
|
||||
implementation.update(output, contentEl) if implementation.update?
|
||||
|
||||
loadScripts = (domEl) ->
|
||||
for script in domEl.getElementsByTagName('script')
|
||||
s = document.createElement('script')
|
||||
s.src = script.src
|
||||
domEl.replaceChild s, script
|
||||
|
||||
init(widgetObject)
|
||||
1
uebersicht-code/server/src/ErrorDetails/.pydio
Normal file
1
uebersicht-code/server/src/ErrorDetails/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
1785b276-85d2-4dbe-beda-c235f170d0ee
|
||||
33
uebersicht-code/server/src/ErrorDetails/ErrorLine.js
Executable file
33
uebersicht-code/server/src/ErrorDetails/ErrorLine.js
Executable file
@@ -0,0 +1,33 @@
|
||||
const codeLine = {
|
||||
padding: '0 10px',
|
||||
position: 'relative',
|
||||
};
|
||||
const lineNum = {
|
||||
color: '#787878',
|
||||
padding: '0 10px',
|
||||
borderRight: '0.5px solid #ddd',
|
||||
};
|
||||
|
||||
const marker = {
|
||||
background: 'rgba(255, 0, 0, 0.3)',
|
||||
fontStyle: 'normal',
|
||||
};
|
||||
|
||||
function slice(line, col) {
|
||||
return [
|
||||
line.slice(0, col),
|
||||
html('em', {style: marker, key: 'marker'}, line.slice(col, col + 1)),
|
||||
line.slice(col + 1),
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = function ErrorLine(props) {
|
||||
const style = {
|
||||
background: props.hasError ? 'rgba(255, 0, 0, 0.2)' : '',
|
||||
};
|
||||
const content = props.hasError ? slice(props.line, props.column) : props.line;
|
||||
return html('tr', {key: props.key, style: style}, [
|
||||
html('td', {style: lineNum, key: props.key + '-0'}, props.lineNum),
|
||||
html('td', {style: codeLine, key: props.key + '-1'}, content),
|
||||
]);
|
||||
};
|
||||
38
uebersicht-code/server/src/ErrorDetails/index.js
Executable file
38
uebersicht-code/server/src/ErrorDetails/index.js
Executable file
@@ -0,0 +1,38 @@
|
||||
const ErrorLine = require('./ErrorLine.js');
|
||||
|
||||
const style = {
|
||||
background: '#fff',
|
||||
padding: '20px 30px',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
const message = {
|
||||
fontSize: '12px',
|
||||
color: 'red',
|
||||
marginBottom: 20,
|
||||
whiteSpace: 'pre',
|
||||
};
|
||||
const code = {
|
||||
lineHeight: '1.5',
|
||||
whiteSpace: 'pre',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
const table = {
|
||||
borderCollapse: 'collapse',
|
||||
};
|
||||
|
||||
module.exports = function ErrorDetails(props) {
|
||||
const {lines, line, column} = props;
|
||||
return html('div', {style: style},
|
||||
html('h1', {style: message, key: 'h1'}, props.message),
|
||||
html('p', {key: 'p'}, 'in ' + props.path + ':'),
|
||||
html('table', {style: table, key: 'table'},
|
||||
html('tbody', {style: code},
|
||||
(lines || []).map((l, i) => {
|
||||
const args = {key: i, hasError: l.lineNum === line, column: column};
|
||||
return ErrorLine(Object.assign({}, l, args));
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
25
uebersicht-code/server/src/MessageBus.js
Executable file
25
uebersicht-code/server/src/MessageBus.js
Executable file
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const WebSocket = require('ws');
|
||||
|
||||
module.exports = function MessageBus(options) {
|
||||
const wss = new WebSocket.Server(options);
|
||||
|
||||
function broadcast(data) {
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
wss.on('connection', function connection(ws) {
|
||||
ws.on('message', broadcast);
|
||||
});
|
||||
|
||||
wss.on('error', function handleError(err) {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return wss;
|
||||
};
|
||||
41
uebersicht-code/server/src/Settings.js
Executable file
41
uebersicht-code/server/src/Settings.js
Executable file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = function Settings(settingsDirPath) {
|
||||
const api = {};
|
||||
let settings;
|
||||
const settingsFile = path.join(settingsDirPath, 'WidgetSettings.json');
|
||||
|
||||
initSettingsFile(settingsDirPath);
|
||||
|
||||
function initSettingsFile(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
api.load = function load() {
|
||||
let persistedSettings = {};
|
||||
try {
|
||||
persistedSettings = require(settingsFile);
|
||||
} catch (e) { /* do nothing */ }
|
||||
|
||||
return persistedSettings;
|
||||
};
|
||||
|
||||
api.persist = function persist(newSettings) {
|
||||
if (newSettings !== settings) {
|
||||
fs.writeFile(settingsFile, JSON.stringify(newSettings), (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
settings = newSettings;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
};
|
||||
66
uebersicht-code/server/src/SharedSocket.js
Executable file
66
uebersicht-code/server/src/SharedSocket.js
Executable file
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const WebSocket = typeof window !== 'undefined'
|
||||
? window.WebSocket
|
||||
: require('ws');
|
||||
|
||||
let ws = null;
|
||||
let isOpen = false;
|
||||
|
||||
const messageListeners = [];
|
||||
const openListeners = [];
|
||||
|
||||
function handleWSOpen() {
|
||||
isOpen = true;
|
||||
openListeners.forEach((f) => f());
|
||||
}
|
||||
|
||||
function handleWSCosed() {
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleMessage(data) {
|
||||
messageListeners.forEach((f) => f(data));
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
exports.open = function open(url) {
|
||||
ws = new WebSocket(url, ['ws'], {origin: 'Übersicht'});
|
||||
|
||||
if (ws.on) {
|
||||
ws.on('open', handleWSOpen);
|
||||
ws.on('close', handleWSCosed);
|
||||
ws.on('message', handleMessage);
|
||||
ws.on('error', handleError);
|
||||
} else {
|
||||
ws.onopen = handleWSOpen;
|
||||
ws.onclose = handleWSCosed;
|
||||
ws.onmessage = (e) => handleMessage(e.data);
|
||||
ws.onerror = handleError;
|
||||
}
|
||||
};
|
||||
|
||||
exports.close = function close() {
|
||||
ws.close();
|
||||
ws = null;
|
||||
};
|
||||
|
||||
exports.isOpen = function() {
|
||||
return ws && isOpen;
|
||||
};
|
||||
|
||||
exports.onMessage = function onMessage(listener) {
|
||||
messageListeners.push(listener);
|
||||
};
|
||||
|
||||
exports.onOpen = function onOpen(listener) {
|
||||
openListeners.push(listener);
|
||||
};
|
||||
|
||||
exports.send = function send(data) {
|
||||
ws.send(data);
|
||||
};
|
||||
|
||||
10
uebersicht-code/server/src/StateServer.js
Executable file
10
uebersicht-code/server/src/StateServer.js
Executable file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
// middleware to serve the current state
|
||||
module.exports = (store) => (req, res, next) => {
|
||||
if (req.url === '/state/') {
|
||||
res.end(JSON.stringify(store.getState()));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
49
uebersicht-code/server/src/Timer.js
Executable file
49
uebersicht-code/server/src/Timer.js
Executable file
@@ -0,0 +1,49 @@
|
||||
function scheduleTick(tick, duration) {
|
||||
if (duration !== false) {
|
||||
return setTimeout(tick, duration);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function Timer() {
|
||||
const api = {};
|
||||
let callback = (done) => done();
|
||||
let started = false;
|
||||
let timer;
|
||||
|
||||
function loop() {
|
||||
clearTimeout(timer);
|
||||
if (started) {
|
||||
callback((nextTickDuration) => {
|
||||
timer = scheduleTick(loop, nextTickDuration);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
api.start = function start() {
|
||||
if (!started) {
|
||||
started = true;
|
||||
loop();
|
||||
}
|
||||
return api;
|
||||
};
|
||||
|
||||
api.stop = function stop() {
|
||||
if (started) {
|
||||
started = false;
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return api;
|
||||
};
|
||||
|
||||
api.map = function map(cb) {
|
||||
callback = cb;
|
||||
return api;
|
||||
};
|
||||
|
||||
api.forceTick = function tick() {
|
||||
callback(() => {});
|
||||
return api;
|
||||
};
|
||||
|
||||
return api;
|
||||
};
|
||||
161
uebersicht-code/server/src/VirtualDomWidget.js
Executable file
161
uebersicht-code/server/src/VirtualDomWidget.js
Executable file
@@ -0,0 +1,161 @@
|
||||
const css = require('emotion').css;
|
||||
const RenderLoop = require('./RenderLoop');
|
||||
const Timer = require('./Timer');
|
||||
const runShellCommand = require('./runShellCommand');
|
||||
const ReactDom = require('react-dom');
|
||||
const html = require('react').createElement;
|
||||
const ErrorDetails = require('./ErrorDetails');
|
||||
window.html = html;
|
||||
|
||||
const defaults = {
|
||||
id: 'widget',
|
||||
refreshFrequency: 1000,
|
||||
init: function init() {},
|
||||
render: function render(props) {
|
||||
return html('div', null, props.error ? String(props.error) : props.output);
|
||||
},
|
||||
updateState: function updateState(event) {
|
||||
return {error: event.error, output: event.output};
|
||||
},
|
||||
initialState: {output: ''},
|
||||
};
|
||||
|
||||
module.exports = function VirtualDomWidget(widgetObject) {
|
||||
const api = {};
|
||||
let implementation;
|
||||
let contentEl;
|
||||
let commandLoop;
|
||||
let renderLoop;
|
||||
let currentError;
|
||||
|
||||
function init(widget) {
|
||||
currentError = widget.error ? JSON.parse(widget.error) : undefined;
|
||||
implementation = Object.create(defaults);
|
||||
Object.assign(implementation, widget.implementation || {}, {id: widget.id});
|
||||
return api;
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (currentError) {
|
||||
renderErrorDetails(currentError);
|
||||
return;
|
||||
}
|
||||
if (renderLoop) {
|
||||
renderLoop.update(renderLoop.state); // force redraw
|
||||
} else {
|
||||
renderLoop = RenderLoop(implementation.initialState, render);
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
function run() {
|
||||
implementation.init(dispatch);
|
||||
if (!implementation.command) return;
|
||||
commandLoop = Timer()
|
||||
.start()
|
||||
.map((done) => {
|
||||
execWidgetCommand()
|
||||
.then(commandCompleted)
|
||||
.catch(commandErrored)
|
||||
.then(() => done(implementation.refreshFrequency));
|
||||
});
|
||||
}
|
||||
|
||||
function commandCompleted(output) {
|
||||
dispatch({type: 'UB/COMMAND_RAN', output});
|
||||
}
|
||||
|
||||
function commandErrored(error) {
|
||||
dispatch({type: 'UB/COMMAND_RAN', error});
|
||||
}
|
||||
|
||||
const runCommandFunction = (command) => {
|
||||
try {
|
||||
return command.apply(implementation, [dispatch]);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
};
|
||||
|
||||
function execWidgetCommand() {
|
||||
const {command} = implementation;
|
||||
if (typeof command === 'function')
|
||||
return Promise.resolve(runCommandFunction(command));
|
||||
else if (typeof command === 'string') return runShellCommand(command);
|
||||
else return Promise.resolve();
|
||||
}
|
||||
|
||||
function dispatch(event) {
|
||||
try {
|
||||
const nextState = implementation.updateState(event, renderLoop.state);
|
||||
renderLoop.update(nextState);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchErrorDetails(err) {
|
||||
return fetch(
|
||||
`/widgets/${widgetObject.id}?line=${err.line}&column=${err.column}`,
|
||||
).then((res) => res.json());
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
try {
|
||||
ReactDom.render(implementation.render(state, dispatch), contentEl);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
currentError = err;
|
||||
commandLoop && commandLoop.stop();
|
||||
fetchErrorDetails(err).then((details) => {
|
||||
if (err !== currentError) return;
|
||||
renderErrorDetails(Object.assign({message: err.message}, details));
|
||||
});
|
||||
}
|
||||
|
||||
function renderErrorDetails(details) {
|
||||
ReactDom.render(html(ErrorDetails, details), contentEl);
|
||||
}
|
||||
|
||||
api.create = function create() {
|
||||
contentEl = document.createElement('div');
|
||||
contentEl.id = implementation.id;
|
||||
contentEl.classList.add('widget');
|
||||
if (implementation.className) {
|
||||
contentEl.classList.add(css(implementation.className));
|
||||
}
|
||||
document.body.appendChild(contentEl);
|
||||
start();
|
||||
return contentEl;
|
||||
};
|
||||
|
||||
api.destroy = function destroy() {
|
||||
commandLoop && commandLoop.stop();
|
||||
if (contentEl && contentEl.parentNode) {
|
||||
contentEl.parentNode.removeChild(contentEl);
|
||||
}
|
||||
renderLoop = null;
|
||||
contentEl = null;
|
||||
currentError = null;
|
||||
};
|
||||
|
||||
api.update = function update(newImplementation) {
|
||||
commandLoop && commandLoop.stop();
|
||||
contentEl.classList.remove(css(implementation.className));
|
||||
init(newImplementation);
|
||||
if (implementation.className) {
|
||||
contentEl.classList.add(css(implementation.className));
|
||||
}
|
||||
start();
|
||||
};
|
||||
|
||||
api.forceRefresh = function forceRefresh() {
|
||||
commandLoop.forceTick();
|
||||
};
|
||||
|
||||
return init(widgetObject);
|
||||
};
|
||||
14
uebersicht-code/server/src/Widget.js
Executable file
14
uebersicht-code/server/src/Widget.js
Executable file
@@ -0,0 +1,14 @@
|
||||
var ClassicWidget = require('./ClassicWidget.coffee');
|
||||
var VirtualDomWidget = require('./VirtualDomWidget');
|
||||
|
||||
module.exports = function Widget(widget) {
|
||||
var api;
|
||||
|
||||
if (/\.jsx$/.test(widget.filePath)) {
|
||||
api = VirtualDomWidget(widget);
|
||||
} else {
|
||||
api = ClassicWidget(widget);
|
||||
}
|
||||
|
||||
return api;
|
||||
};
|
||||
106
uebersicht-code/server/src/WidgetBundler.js
Executable file
106
uebersicht-code/server/src/WidgetBundler.js
Executable file
@@ -0,0 +1,106 @@
|
||||
'use strict';
|
||||
|
||||
const bundleWidget = require('./bundleWidget');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = function WidgetBundler() {
|
||||
const api = {};
|
||||
const bundles = {};
|
||||
|
||||
api.push = function push(action, callback) {
|
||||
if (action && action.type) {
|
||||
action.type === 'added'
|
||||
? addWidget(action.id, action.filePath, callback)
|
||||
: removeWidget(action.id, action.filePath, callback)
|
||||
;
|
||||
}
|
||||
};
|
||||
|
||||
api.close = function close() {
|
||||
for (var id in bundles) {
|
||||
bundles[id].close();
|
||||
delete bundles[id];
|
||||
}
|
||||
};
|
||||
|
||||
api.get = function get(id) {
|
||||
return bundles[id].widget.body;
|
||||
};
|
||||
|
||||
function addWidget(id, filePath, emit) {
|
||||
if (!bundles[id]) {
|
||||
bundles[id] = WidgetBundle(id, filePath, (widget) => {
|
||||
emit({type: 'added', widget: widget});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeWidget(id, filePath, emit) {
|
||||
if (bundles[id]) {
|
||||
bundles[id].close();
|
||||
delete bundles[id];
|
||||
emit({type: 'removed', id: id});
|
||||
}
|
||||
}
|
||||
|
||||
function WidgetBundle(id, filePath, callback) {
|
||||
const bundle = bundleWidget(id, filePath);
|
||||
const buildWidget = (paths = []) => {
|
||||
const widget = {
|
||||
id: id,
|
||||
filePath: filePath,
|
||||
};
|
||||
|
||||
fs.access(filePath, fs.R_OK, (couldNotRead) => {
|
||||
if (couldNotRead) return;
|
||||
bundle.bundle((err, srcBuffer) => {
|
||||
if (err) {
|
||||
widget.error = errorJSON(filePath, err);
|
||||
} else {
|
||||
widget.body = srcBuffer.toString();
|
||||
}
|
||||
|
||||
widget.mtime = fs.statSync(paths[0] || filePath).mtime;
|
||||
bundle.widget = widget;
|
||||
callback(widget);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
bundle.on('update', buildWidget);
|
||||
buildWidget();
|
||||
return bundle;
|
||||
}
|
||||
|
||||
function errorJSON(filePath, error) {
|
||||
if (!error._babel) {
|
||||
return JSON.stringify({
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
path: filePath,
|
||||
lines: error.annotated,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
return JSON.stringify({
|
||||
line: error.loc.line,
|
||||
column: error.loc.column,
|
||||
lines: parseCodeFrame(error.codeFrame),
|
||||
path: filePath,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
function parseCodeFrame(codeFrame) {
|
||||
return codeFrame
|
||||
.split('\n')
|
||||
.map(l => {
|
||||
const [num, line] = l.split('|', 2);
|
||||
const lineNum = parseInt(num.replace(/^>/, ''), 10);
|
||||
return isNaN(lineNum) ? undefined : {lineNum: lineNum, line: line};
|
||||
})
|
||||
.filter(i => i);
|
||||
}
|
||||
|
||||
return api;
|
||||
};
|
||||
0
uebersicht-code/server/src/WidgetSettings.json
Executable file
0
uebersicht-code/server/src/WidgetSettings.json
Executable file
38
uebersicht-code/server/src/actions.js
Executable file
38
uebersicht-code/server/src/actions.js
Executable file
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
function addWidget(widget) {
|
||||
const {id, filePath, error, mtime} = widget;
|
||||
return {
|
||||
type: 'WIDGET_ADDED',
|
||||
payload: {id, filePath, error, mtime},
|
||||
};
|
||||
}
|
||||
|
||||
exports.showWidget = function showWidget(id, impl) {
|
||||
return {
|
||||
type: 'WIDGET_LOADED',
|
||||
id: id,
|
||||
payload: impl,
|
||||
};
|
||||
};
|
||||
|
||||
function removeWidget(id) {
|
||||
return {
|
||||
type: 'WIDGET_REMOVED',
|
||||
payload: id,
|
||||
};
|
||||
}
|
||||
|
||||
exports.applyWidgetSettings = function applyWidgetSettings(id, settings) {
|
||||
return {
|
||||
type: 'WIDGET_SETTINGS_CHANGED',
|
||||
payload: { id: id, settings: settings },
|
||||
};
|
||||
};
|
||||
|
||||
exports.get = function(widgetEvent) {
|
||||
switch (widgetEvent.type) {
|
||||
case 'added': return addWidget(widgetEvent.widget);
|
||||
case 'removed': return removeWidget(widgetEvent.id);
|
||||
};
|
||||
};
|
||||
114
uebersicht-code/server/src/app.coffee
Executable file
114
uebersicht-code/server/src/app.coffee
Executable file
@@ -0,0 +1,114 @@
|
||||
connect = require 'connect'
|
||||
http = require 'http'
|
||||
serveStatic = require 'serve-static'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
redux = require 'redux'
|
||||
|
||||
MessageBus = require('./MessageBus')
|
||||
watchDir = require('./directory_watcher.coffee')
|
||||
WidgetBundler = require('./WidgetBundler.js')
|
||||
Settings = require('./Settings')
|
||||
StateServer = require('./StateServer')
|
||||
ensureSameOrigin = require('./ensureSameOrigin')
|
||||
disallowIFraming = require('./disallowIFraming')
|
||||
CommandServer = require('./command_server.coffee')
|
||||
serveWidgets = require('./serveWidgets')
|
||||
serveClient = require('./serveClient')
|
||||
serveCss = require('./serveCss')
|
||||
sharedSocket = require('./SharedSocket')
|
||||
actions = require('./actions')
|
||||
reducer = require('./reducer')
|
||||
resolveWidget = require('./resolveWidget')
|
||||
|
||||
dispatchToRemote = require('./dispatch')
|
||||
listenToRemote = require('./listen')
|
||||
|
||||
module.exports = (port, widgetPath, settingsPath, publicPath, options, callback) ->
|
||||
console.log("port ========== :", port);
|
||||
console.log("widgetPath ========== :", widgetPath);
|
||||
console.log("settingsPath ========== :", settingsPath);
|
||||
console.log("publicPath ========== :", publicPath);
|
||||
console.log("options ========== :", options);
|
||||
options ||= {}
|
||||
|
||||
# global store for app state
|
||||
store = redux.createStore(
|
||||
reducer,
|
||||
{ widgets: {}, settings: {}, screens: [] }
|
||||
)
|
||||
|
||||
# listen to remote actions
|
||||
listenToRemote (action) ->
|
||||
store.dispatch(action)
|
||||
|
||||
# follow symlink if widgetDirectory is one
|
||||
if fs.lstatSync(widgetPath).isSymbolicLink()
|
||||
widgetPath = fs.readlinkSync(widgetPath)
|
||||
widgetPath = widgetPath.normalize()
|
||||
|
||||
bundler = WidgetBundler(widgetPath)
|
||||
# TODO: use a stream/generator/promise pattern instead of nested callbacks
|
||||
stopWatchingDir = watchDir(widgetPath, (fileEvent) ->
|
||||
if (fileEvent.filePath.replace(fileEvent.rootPath, '') == '/main.css')
|
||||
dispatchToRemote({type: 'MASTER_STYLE_CHANGED'})
|
||||
return
|
||||
bundler.push(resolveWidget(fileEvent), (widgetEvent) ->
|
||||
action = actions.get(widgetEvent)
|
||||
if (action)
|
||||
store.dispatch(action)
|
||||
dispatchToRemote(action)
|
||||
)
|
||||
)
|
||||
|
||||
# load and replay settings
|
||||
settings = Settings(settingsPath)
|
||||
|
||||
for id, value of settings.load()
|
||||
action = actions.applyWidgetSettings(id, value)
|
||||
store.dispatch(action)
|
||||
dispatchToRemote(action)
|
||||
|
||||
store.subscribe ->
|
||||
settings.persist(store.getState().settings)
|
||||
|
||||
# set up the server
|
||||
host = "127.0.0.1"
|
||||
messageBus = null
|
||||
allowedOrigin = "http://#{host}:#{port}"
|
||||
middleware = connect()
|
||||
.use(disallowIFraming)
|
||||
.use(ensureSameOrigin(allowedOrigin))
|
||||
.use(CommandServer(widgetPath, options.loginShell))
|
||||
.use(StateServer(store))
|
||||
.use(serveWidgets(bundler, widgetPath))
|
||||
.use(serveStatic(publicPath))
|
||||
.use(serveStatic(widgetPath))
|
||||
.use(serveCss(widgetPath))
|
||||
.use(serveClient(publicPath))
|
||||
|
||||
server = http.createServer(middleware)
|
||||
server.keepAliveTimeout = 35000
|
||||
server.listen port, host, (err) ->
|
||||
try
|
||||
return server.emit('error', err) if err
|
||||
messageBus = MessageBus(
|
||||
server: server,
|
||||
verifyClient: (info) ->
|
||||
info.origin == allowedOrigin || info.origin == 'Übersicht'
|
||||
)
|
||||
sharedSocket.open("ws://#{host}:#{port}")
|
||||
callback?()
|
||||
catch e
|
||||
server.emit('error', e)
|
||||
|
||||
# api
|
||||
close: (cb) ->
|
||||
stopWatchingDir()
|
||||
bundler.close()
|
||||
server.close()
|
||||
sharedSocket.close()
|
||||
messageBus.close(cb)
|
||||
|
||||
on: (ev, handler) ->
|
||||
server.on(ev, handler)
|
||||
61
uebersicht-code/server/src/bundleWidget.js
Executable file
61
uebersicht-code/server/src/bundleWidget.js
Executable file
@@ -0,0 +1,61 @@
|
||||
const browserify = require('browserify');
|
||||
const watchify = require('./watchify');
|
||||
const widgetify = require('./widgetify');
|
||||
const coffeeify = require('coffeeify');
|
||||
const babelify = require('babelify');
|
||||
const jsxTransform = require('@babel/preset-react');
|
||||
const restSpreadTransform = require('@babel/plugin-proposal-object-rest-spread');
|
||||
const emotion = require('babel-plugin-emotion');
|
||||
const envPreset = require('@babel/preset-env');
|
||||
const through = require('through2');
|
||||
|
||||
function wrapJSWidget() {
|
||||
let start = true;
|
||||
function write(chunk, enc, next) {
|
||||
if (start) {
|
||||
this.push('({');
|
||||
start = false;
|
||||
}
|
||||
next(null, chunk);
|
||||
}
|
||||
function end(next) {
|
||||
this.push('})');
|
||||
next();
|
||||
}
|
||||
|
||||
return through(write, end);
|
||||
}
|
||||
|
||||
module.exports = function bundleWidget(id, filePath) {
|
||||
const isJsxWidget = filePath.match(/\.jsx$/);
|
||||
const bundle = browserify(filePath, {
|
||||
detectGlobals: false,
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
debug: isJsxWidget,
|
||||
});
|
||||
|
||||
bundle.plugin(watchify);
|
||||
bundle.require(filePath, {expose: id});
|
||||
bundle.external('uebersicht');
|
||||
|
||||
if (filePath.match(/\.coffee$/)) {
|
||||
bundle.transform(coffeeify, {
|
||||
bare: true,
|
||||
header: false,
|
||||
});
|
||||
bundle.transform(widgetify, {id: id});
|
||||
} else if (isJsxWidget) {
|
||||
bundle.transform(babelify, {
|
||||
presets: [
|
||||
[envPreset, {targets: 'last 4 Safari versions', modules: 'commonjs'}],
|
||||
[jsxTransform, {pragma: 'html'}],
|
||||
],
|
||||
plugins: [restSpreadTransform, emotion],
|
||||
});
|
||||
} else {
|
||||
bundle.transform(wrapJSWidget);
|
||||
bundle.transform(widgetify, {id: id});
|
||||
}
|
||||
return bundle;
|
||||
};
|
||||
41
uebersicht-code/server/src/command_server.coffee
Executable file
41
uebersicht-code/server/src/command_server.coffee
Executable file
@@ -0,0 +1,41 @@
|
||||
# middleware to serve the results of shell commands
|
||||
# Listens to POST /run/
|
||||
{spawn} = require('child_process')
|
||||
module.exports = (workingDir, useLoginShell) ->
|
||||
args = if useLoginShell then ['-l'] else []
|
||||
# the Connect middleware
|
||||
(req, res, next) ->
|
||||
return next() unless req.method == 'POST' and req.url == '/run/'
|
||||
shell = spawn 'bash', args, cwd: workingDir
|
||||
|
||||
command = ''
|
||||
req.on 'data', (chunk) -> shell.stdin.write chunk
|
||||
|
||||
req.on 'end', ->
|
||||
setStatusOnce = (status) ->
|
||||
res.writeHead status
|
||||
setStatusOnce = ->
|
||||
|
||||
shell.stderr.on 'data', (d) ->
|
||||
setStatusOnce 500
|
||||
res.write d
|
||||
|
||||
shell.stdout.on 'data', (d) ->
|
||||
setStatusOnce 200
|
||||
res.write d
|
||||
|
||||
shell.on 'error', (err) ->
|
||||
setStatusOnce 500
|
||||
res.write err.message
|
||||
|
||||
shell.on 'close', ->
|
||||
setStatusOnce 200
|
||||
res.end()
|
||||
|
||||
shell.stdin.write '\n'
|
||||
shell.stdin.end()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
26
uebersicht-code/server/src/detectWidgetHover.js
Executable file
26
uebersicht-code/server/src/detectWidgetHover.js
Executable file
@@ -0,0 +1,26 @@
|
||||
module.exports = (containerEl) => {
|
||||
let insideWidget = false;
|
||||
|
||||
const checkHover = (e) => {
|
||||
if (insideWidget && containerEl === e.target) {
|
||||
insideWidget = false;
|
||||
window.webkit.messageHandlers.uebersicht.postMessage('widgetLeave');
|
||||
} else if (!insideWidget && containerEl !== e.target) {
|
||||
insideWidget = true;
|
||||
window.webkit.messageHandlers.uebersicht.postMessage('widgetEnter');
|
||||
}
|
||||
};
|
||||
|
||||
const checkHoverRecursive = () => {
|
||||
window.addEventListener(
|
||||
'mousemove',
|
||||
(e) => {
|
||||
checkHover(e);
|
||||
setTimeout(checkHoverRecursive, 32);
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
};
|
||||
|
||||
checkHoverRecursive();
|
||||
};
|
||||
73
uebersicht-code/server/src/directory_watcher.coffee
Executable file
73
uebersicht-code/server/src/directory_watcher.coffee
Executable file
@@ -0,0 +1,73 @@
|
||||
paths = require 'path'
|
||||
fs = require 'fs'
|
||||
fsevents = require('fsevents')
|
||||
|
||||
module.exports = (directoryPath, callback) ->
|
||||
api = {}
|
||||
foundPaths = {}
|
||||
closed = true
|
||||
stopWatching = null;
|
||||
|
||||
init = ->
|
||||
if !fs.existsSync(directoryPath)
|
||||
throw new Error "could not find #{directoryPath}"
|
||||
|
||||
closed = false
|
||||
stopWatching = fsevents.watch(directoryPath, (filePath, flags, id) ->
|
||||
return if closed
|
||||
info = fsevents.getInfo(filePath, flags, id);
|
||||
switch info.event
|
||||
when 'modified', 'created'
|
||||
findFiles filePath, info.type, registerFile
|
||||
when 'deleted'
|
||||
unregisterFiles filePath
|
||||
when 'moved'
|
||||
unregisterFiles filePath
|
||||
findFiles filePath, info.type, registerFile
|
||||
)
|
||||
|
||||
console.log 'watching', directoryPath
|
||||
|
||||
findFiles directoryPath, 'directory', registerFile
|
||||
close
|
||||
|
||||
close = ->
|
||||
closed = true
|
||||
stopWatching?()
|
||||
|
||||
registerFile = (filePath) ->
|
||||
filePath = filePath.normalize()
|
||||
foundPaths[filePath] = true
|
||||
callback({
|
||||
type: 'added',
|
||||
filePath: filePath.normalize(),
|
||||
rootPath: directoryPath,
|
||||
})
|
||||
|
||||
unregisterFiles = (path) ->
|
||||
path = path.normalize()
|
||||
for filePath in Object.keys(foundPaths) when filePath.indexOf(path) == 0
|
||||
callback({type: 'removed', filePath: filePath, rootPath: directoryPath})
|
||||
|
||||
# recursively walks the directory tree and calls onFound for every file it
|
||||
# finds
|
||||
findFiles = (path, type, onFound) ->
|
||||
if type == 'file'
|
||||
onFound path
|
||||
else
|
||||
fs.readdir path, (err, subPaths) ->
|
||||
return console.log err if err
|
||||
for subPath in subPaths
|
||||
fullPath = paths.join(path, subPath)
|
||||
getPathType fullPath, (p, t) -> findFiles(p, t, onFound)
|
||||
|
||||
# get type of path as either 'file' or 'directory'
|
||||
# callback gets called with (path, type) where path is the path passed in,
|
||||
# for convenience
|
||||
getPathType = (path, callback) ->
|
||||
fs.stat path, (err, stat) ->
|
||||
return console.log err if err
|
||||
type = if stat.isDirectory() then 'directory' else 'file'
|
||||
callback path, type
|
||||
|
||||
init()
|
||||
4
uebersicht-code/server/src/disallowIFraming.js
Executable file
4
uebersicht-code/server/src/disallowIFraming.js
Executable file
@@ -0,0 +1,4 @@
|
||||
module.exports = function disallowIFraming(req, res, next) {
|
||||
res.setHeader('X-Frame-Options', 'sameorigin');
|
||||
next();
|
||||
};
|
||||
21
uebersicht-code/server/src/dispatch.js
Executable file
21
uebersicht-code/server/src/dispatch.js
Executable file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const ws = require('./SharedSocket');
|
||||
const queuedMessages = [];
|
||||
|
||||
function drainQueuedMessages() {
|
||||
queuedMessages.forEach((m) => ws.send(m));
|
||||
queuedMessages.length = 0;
|
||||
}
|
||||
|
||||
ws.onOpen(drainQueuedMessages);
|
||||
|
||||
module.exports = function dispatch(message) {
|
||||
const serializedMessage = JSON.stringify(message);
|
||||
|
||||
if (ws.isOpen()) {
|
||||
ws.send(serializedMessage);
|
||||
} else {
|
||||
queuedMessages.push(serializedMessage);
|
||||
}
|
||||
};
|
||||
12
uebersicht-code/server/src/ensureSameOrigin.js
Executable file
12
uebersicht-code/server/src/ensureSameOrigin.js
Executable file
@@ -0,0 +1,12 @@
|
||||
module.exports = function ensureSameOrgin(origin) {
|
||||
const fromSameOrigin = (req) => {
|
||||
return req.method == 'GET' ||
|
||||
(req.headers.origin && req.headers.origin === origin);
|
||||
}
|
||||
|
||||
return ((req, res, next) => {
|
||||
if (fromSameOrigin(req)) return next();
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
})
|
||||
}
|
||||
17
uebersicht-code/server/src/listen.js
Executable file
17
uebersicht-code/server/src/listen.js
Executable file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const ws = require('./SharedSocket');
|
||||
const listeners = [];
|
||||
|
||||
ws.onMessage(function handleMessage(data) {
|
||||
let message;
|
||||
try { message = JSON.parse(data); } catch (e) { null; }
|
||||
|
||||
if (message) {
|
||||
listeners.forEach((f) => f(message));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = function listen(callback) {
|
||||
listeners.push(callback);
|
||||
};
|
||||
162
uebersicht-code/server/src/reducer.js
Executable file
162
uebersicht-code/server/src/reducer.js
Executable file
@@ -0,0 +1,162 @@
|
||||
'use strict';
|
||||
|
||||
const defaultSettings = {
|
||||
showOnAllScreens: true,
|
||||
showOnMainScreen: false,
|
||||
showOnSelectedScreens: false,
|
||||
hidden: false,
|
||||
screens: [],
|
||||
};
|
||||
|
||||
const handlers = {
|
||||
WIDGET_ADDED: (state, action) => {
|
||||
const widget = action.payload;
|
||||
const newWidgets = Object.assign({}, state.widgets, {
|
||||
[widget.id]: widget,
|
||||
});
|
||||
|
||||
const settings = state.settings || {};
|
||||
const newSettings = settings[widget.id]
|
||||
? state.settings
|
||||
: Object.assign({}, settings, {[widget.id]: defaultSettings});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
widgets: newWidgets,
|
||||
settings: newSettings,
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_LOADED: (state, action) => {
|
||||
if (!state.widgets[action.id]) {
|
||||
return state;
|
||||
}
|
||||
const widget = Object.assign({}, state.widgets[action.id], {
|
||||
implementation: action.payload,
|
||||
});
|
||||
const newWidgets = Object.assign({}, state.widgets, {[widget.id]: widget});
|
||||
return Object.assign({}, state, {widgets: newWidgets});
|
||||
},
|
||||
|
||||
WIDGET_REMOVED: (state, action) => {
|
||||
const id = action.payload;
|
||||
|
||||
if (!state.widgets[id]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newWidgets = Object.assign({}, state.widgets);
|
||||
delete newWidgets[id];
|
||||
|
||||
return Object.assign({}, state, {widgets: newWidgets});
|
||||
},
|
||||
|
||||
WIDGET_SETTINGS_CHANGED: (state, action) => {
|
||||
const newSettings = Object.assign({}, state.settings, {
|
||||
[action.payload.id]: action.payload.settings,
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {settings: newSettings});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_ALL_SCREENS: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
showOnAllScreens: true,
|
||||
showOnSelectedScreens: false,
|
||||
showOnMainScreen: false,
|
||||
hidden: false,
|
||||
screens: [],
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_SELECTED_SCREENS: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
showOnSelectedScreens: true,
|
||||
showOnAllScreens: false,
|
||||
showOnMainScreen: false,
|
||||
hidden: false,
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_MAIN_SCREEN: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
showOnSelectedScreens: false,
|
||||
showOnAllScreens: false,
|
||||
showOnMainScreen: true,
|
||||
hidden: false,
|
||||
screens: [],
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_HIDE: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
hidden: true,
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_SHOW: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
hidden: false,
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_BACKGROUND: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
inBackground: true,
|
||||
});
|
||||
},
|
||||
|
||||
WIDGET_SET_TO_FOREGROUND: (state, action) => {
|
||||
return updateSettings(state, action.payload, {
|
||||
inBackground: false,
|
||||
});
|
||||
},
|
||||
|
||||
SCREEN_SELECTED_FOR_WIDGET: (state, action) => {
|
||||
const settings = state.settings[action.payload.id];
|
||||
const newScreens = (settings.screens || []).slice();
|
||||
|
||||
if (newScreens.indexOf(action.payload.screenId) === -1) {
|
||||
newScreens.push(action.payload.screenId);
|
||||
}
|
||||
|
||||
return updateSettings(state, action.payload.id, {
|
||||
screens: newScreens,
|
||||
});
|
||||
},
|
||||
|
||||
SCREEN_DESELECTED_FOR_WIDGET: (state, action) => {
|
||||
const newScreens = (state.settings[action.payload.id].screens || []).filter(
|
||||
(s) => s !== action.payload.screenId,
|
||||
);
|
||||
|
||||
return updateSettings(state, action.payload.id, {
|
||||
screens: newScreens,
|
||||
});
|
||||
},
|
||||
|
||||
SCREENS_DID_CHANGE: (state, action) => {
|
||||
return Object.assign({}, state, {
|
||||
screens: action.payload,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function updateSettings(state, widgetId, patch) {
|
||||
const widgetSettings = state.settings[widgetId];
|
||||
const newSettings = Object.assign({}, state.settings, {
|
||||
[widgetId]: Object.assign({}, widgetSettings, patch),
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {settings: newSettings});
|
||||
}
|
||||
|
||||
module.exports = function reduce(state, action) {
|
||||
let newState;
|
||||
const handler = handlers[action.type];
|
||||
if (handler) {
|
||||
newState = handler(state, action);
|
||||
} else {
|
||||
newState = state;
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
77
uebersicht-code/server/src/render.js
Executable file
77
uebersicht-code/server/src/render.js
Executable file
@@ -0,0 +1,77 @@
|
||||
var Widget = require('./Widget');
|
||||
var rendered = {};
|
||||
|
||||
function isVisibleOnScreen(widgetId, screenId, state) {
|
||||
var settings = state.settings[widgetId] || {};
|
||||
var isVisible = false;
|
||||
|
||||
if (settings.hidden) {
|
||||
isVisible = false;
|
||||
} else if (
|
||||
settings.showOnAllScreens ||
|
||||
settings.showOnAllScreens === undefined
|
||||
) {
|
||||
isVisible = true;
|
||||
} else if (settings.showOnMainScreen) {
|
||||
isVisible = state.screens.indexOf(screenId) === 0;
|
||||
} else if (settings.showOnSelectedScreens) {
|
||||
isVisible = (settings.screens || []).indexOf(screenId) !== -1;
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
function isInBackground(widgetId, state) {
|
||||
const settings = state.settings[widgetId] || {};
|
||||
return settings.inBackground === true;
|
||||
}
|
||||
|
||||
function renderWidget(widget, domEl) {
|
||||
var prevRendered = rendered[widget.id];
|
||||
|
||||
if (prevRendered && prevRendered.widget.mtime === widget.mtime) {
|
||||
return;
|
||||
} else if (prevRendered) {
|
||||
prevRendered.instance.update(widget);
|
||||
prevRendered.widget = widget;
|
||||
} else {
|
||||
var instance = Widget(widget);
|
||||
domEl.appendChild(instance.create());
|
||||
rendered[widget.id] = {
|
||||
instance: instance,
|
||||
widget: widget,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function destroyWidget(id) {
|
||||
rendered[id].instance.destroy();
|
||||
delete rendered[id];
|
||||
}
|
||||
|
||||
function render(state, screen, domEl, dispatch) {
|
||||
const remaining = Object.keys(rendered);
|
||||
|
||||
for (var id in state.widgets) {
|
||||
const widget = state.widgets[id];
|
||||
|
||||
if (!isVisibleOnScreen(id, screen.id, state)) continue;
|
||||
|
||||
if (
|
||||
screen.layer &&
|
||||
(screen.layer === 'background') != isInBackground(id, state)
|
||||
)
|
||||
continue;
|
||||
|
||||
if (widget.error || widget.implementation)
|
||||
renderWidget(widget, domEl, dispatch);
|
||||
|
||||
const idx = remaining.indexOf(widget.id);
|
||||
if (idx > -1) remaining.splice(idx, 1);
|
||||
}
|
||||
|
||||
remaining.forEach((obsolete) => destroyWidget(obsolete));
|
||||
}
|
||||
|
||||
render.rendered = rendered;
|
||||
module.exports = render;
|
||||
45
uebersicht-code/server/src/renderLoop.js
Executable file
45
uebersicht-code/server/src/renderLoop.js
Executable file
@@ -0,0 +1,45 @@
|
||||
var raf = require('raf');
|
||||
|
||||
module.exports = function RenderLoop(initialState, render) {
|
||||
var currentState = null;
|
||||
var redrawScheduled = false;
|
||||
var inRenderingTransaction = false;
|
||||
|
||||
var loop = {
|
||||
state: initialState,
|
||||
update: update,
|
||||
};
|
||||
|
||||
function update(state) {
|
||||
if (inRenderingTransaction) {
|
||||
throw Error("can't update while rendering");
|
||||
}
|
||||
|
||||
if (currentState === null && !redrawScheduled) {
|
||||
redrawScheduled = true;
|
||||
raf(redraw);
|
||||
}
|
||||
|
||||
currentState = state;
|
||||
loop.state = currentState;
|
||||
return loop;
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
redrawScheduled = false;
|
||||
if (currentState === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
inRenderingTransaction = true;
|
||||
try {
|
||||
render(currentState);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
inRenderingTransaction = false;
|
||||
currentState = null;
|
||||
}
|
||||
|
||||
return update(initialState);
|
||||
};
|
||||
33
uebersicht-code/server/src/resolveWidget.js
Executable file
33
uebersicht-code/server/src/resolveWidget.js
Executable file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
function isWidgetPath(filePath) {
|
||||
return (
|
||||
filePath.indexOf('/node_modules/') === -1 &&
|
||||
filePath.indexOf('/src/') === -1 &&
|
||||
filePath.indexOf('/lib/') === -1 &&
|
||||
/\.coffee$|\.js$|\.jsx$/.test(filePath)
|
||||
);
|
||||
}
|
||||
|
||||
function widgetId(filePath, rootPath) {
|
||||
const fileParts = filePath
|
||||
.replace(rootPath, '')
|
||||
.split(/\/+/)
|
||||
.filter((part) => !!part);
|
||||
|
||||
return fileParts.join('-')
|
||||
.replace(/\./g, '-')
|
||||
.replace(/\s/g, '_');
|
||||
}
|
||||
|
||||
module.exports = function resolveWidget(fileEvent) {
|
||||
if (!isWidgetPath(fileEvent.filePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: widgetId(fileEvent.filePath, fileEvent.rootPath),
|
||||
filePath: fileEvent.filePath,
|
||||
type: fileEvent.type,
|
||||
};
|
||||
};
|
||||
15
uebersicht-code/server/src/runCommand.js
Executable file
15
uebersicht-code/server/src/runCommand.js
Executable file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const runShellCommand = require('./runShellCommand');
|
||||
|
||||
module.exports = function runCommand(widget, callback, dispatch) {
|
||||
const {command, refreshFrequency} = widget;
|
||||
|
||||
if (typeof command === 'function') {
|
||||
command.apply(widget, [callback]);
|
||||
} else if (typeof command === 'string') {
|
||||
runShellCommand(command, callback).timeout(refreshFrequency);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
22
uebersicht-code/server/src/runShellCommand.js
Executable file
22
uebersicht-code/server/src/runShellCommand.js
Executable file
@@ -0,0 +1,22 @@
|
||||
const post = require('superagent').post;
|
||||
|
||||
function wrapError(err, res) {
|
||||
return err ? new Error((res || {}).text || 'error running command') : null;
|
||||
}
|
||||
|
||||
function isKeepAliveError(err) {
|
||||
return err && err.message.indexOf('Request has been terminated') === 0;
|
||||
}
|
||||
|
||||
module.exports = function runShellCommand(command, callback) {
|
||||
const request = post('/run/')
|
||||
.retry(2, isKeepAliveError)
|
||||
.send(command);
|
||||
return callback
|
||||
? request.end((err, res) => callback(wrapError(err, res), (res || {}).text))
|
||||
: request
|
||||
.catch((err) => {
|
||||
throw wrapError(err, err.response);
|
||||
})
|
||||
.then((res) => res.text);
|
||||
};
|
||||
14
uebersicht-code/server/src/serveClient.js
Executable file
14
uebersicht-code/server/src/serveClient.js
Executable file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const stream = require('stream');
|
||||
|
||||
module.exports = (publicDir) => {
|
||||
const indexHTML = fs.readFileSync(path.join(publicDir, 'index.html'));
|
||||
return function serveClient(req, res, next) {
|
||||
const bufferStream = new stream.PassThrough();
|
||||
bufferStream.pipe(res);
|
||||
bufferStream.end(indexHTML);
|
||||
};
|
||||
};
|
||||
15
uebersicht-code/server/src/serveCss.js
Executable file
15
uebersicht-code/server/src/serveCss.js
Executable file
@@ -0,0 +1,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const urls = require('url');
|
||||
|
||||
module.exports = (widgetsDir) => (req, res, next) => {
|
||||
const url = urls.parse(req.url);
|
||||
if (url.pathname !== '/userMain.css') return next();
|
||||
|
||||
fs.ReadStream(path.join(widgetsDir, 'main.css'))
|
||||
.on('error', (err) => {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
res.end('');
|
||||
})
|
||||
.pipe(res);
|
||||
};
|
||||
79
uebersicht-code/server/src/serveWidgets.js
Executable file
79
uebersicht-code/server/src/serveWidgets.js
Executable file
@@ -0,0 +1,79 @@
|
||||
const URL = require('url');
|
||||
const fs = require('fs');
|
||||
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||
const convert = require('convert-source-map');
|
||||
const {Transform} = require('stream');
|
||||
const byline = require('byline');
|
||||
const path = require('path');
|
||||
|
||||
// middleware to serve widget bundles
|
||||
module.exports = (bundler, widgetPath) => (req, res, next) => {
|
||||
const url = URL.parse(req.url, true);
|
||||
const match = url.pathname.match(/\/widgets\/(.+)$/);
|
||||
if (match) {
|
||||
const code = bundler.get(match[1]);
|
||||
if (!code) {
|
||||
res.writeHead(404);
|
||||
return res.end();
|
||||
}
|
||||
return url.search
|
||||
? codeLines(code, widgetPath, url.query, res)
|
||||
: res.end(code);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
function asErrorJSON(codeLocation, padding) {
|
||||
const lineNum = codeLocation.line;
|
||||
let i = 0;
|
||||
let lines = [];
|
||||
return new Transform({
|
||||
transform(line, _, next) {
|
||||
if (i >= lineNum - padding && i < lineNum + padding) {
|
||||
lines.push({lineNum: i + 1, line: line.toString()});
|
||||
}
|
||||
i++;
|
||||
next();
|
||||
},
|
||||
flush(done) {
|
||||
done(null, JSON.stringify({
|
||||
line: codeLocation.line,
|
||||
column: codeLocation.column,
|
||||
lines: lines,
|
||||
path: codeLocation.path,
|
||||
}));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function codeLines(source, widgetDir, options, res) {
|
||||
const padding = 5;
|
||||
const lineNum = Number(options.line) || 0;
|
||||
const column = Number(options.column) || 0;
|
||||
const converter = convert.fromSource(source);
|
||||
|
||||
if (!converter) {
|
||||
res.writeHead(404);
|
||||
res.end('could not find sourcemap comment');
|
||||
return;
|
||||
}
|
||||
|
||||
SourceMapConsumer.with(converter.toObject(), null, (smc) => {
|
||||
var origpos = smc.originalPositionFor({ line: lineNum, column: column });
|
||||
if (!origpos.source) {
|
||||
res.writeHead(404);
|
||||
res.end('no match found for line ' + lineNum + ':' + column + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
origpos.path = path.relative(widgetDir, origpos.source);
|
||||
byline(fs.createReadStream(origpos.source), {keepEmptyLines: true})
|
||||
.pipe(asErrorJSON(origpos, padding))
|
||||
.pipe(res)
|
||||
.on('error', err => {
|
||||
res.writeHead(500);
|
||||
res.end(err.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
7
uebersicht-code/server/src/uebersicht.js
Executable file
7
uebersicht-code/server/src/uebersicht.js
Executable file
@@ -0,0 +1,7 @@
|
||||
import run from './runShellCommand';
|
||||
import request from 'superagent';
|
||||
import {css} from 'emotion';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
|
||||
export {run, request, css, styled, React};
|
||||
23
uebersicht-code/server/src/validateWidget.js
Executable file
23
uebersicht-code/server/src/validateWidget.js
Executable file
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
function validateHasCommand(impl, issues, message) {
|
||||
if (impl.refreshFrequency === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof impl.command !== 'string' && impl.command !== 'function') {
|
||||
issues.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function validateWidget(impl) {
|
||||
const issues = [];
|
||||
|
||||
if (impl) {
|
||||
validateHasCommand(impl, issues, 'no command given');
|
||||
} else {
|
||||
issues.push('empty implementation');
|
||||
}
|
||||
|
||||
return issues;
|
||||
};
|
||||
180
uebersicht-code/server/src/watchify.js
Executable file
180
uebersicht-code/server/src/watchify.js
Executable file
@@ -0,0 +1,180 @@
|
||||
var through = require('through2');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = watchify;
|
||||
module.exports.args = {
|
||||
cache: {}, packageCache: {}
|
||||
};
|
||||
|
||||
function watchify (b, opts) {
|
||||
if (!opts) opts = {};
|
||||
var cache = b._options.cache;
|
||||
var pkgcache = b._options.packageCache;
|
||||
var delay = typeof opts.delay === 'number' ? opts.delay : 0;
|
||||
var changingDeps = {};
|
||||
var pending = false;
|
||||
var updating = false;
|
||||
var mtimes = {};
|
||||
|
||||
var wopts = {persistent: true};
|
||||
if (opts.ignoreWatch) {
|
||||
wopts.ignored = opts.ignoreWatch !== true
|
||||
? opts.ignoreWatch
|
||||
: '**/node_modules/**';
|
||||
}
|
||||
if (opts.poll || typeof opts.poll === 'number') {
|
||||
wopts.usePolling = true;
|
||||
wopts.interval = opts.poll !== true
|
||||
? opts.poll
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
b.on('reset', collect);
|
||||
collect();
|
||||
}
|
||||
|
||||
function collect () {
|
||||
b.pipeline.get('deps').push(through.obj(function(row, enc, next) {
|
||||
var file = row.expose ? b._expose[row.id] : row.file;
|
||||
cache[file] = {
|
||||
source: row.source,
|
||||
deps: Object.assign({}, row.deps)
|
||||
};
|
||||
this.push(row);
|
||||
next();
|
||||
}));
|
||||
}
|
||||
|
||||
b.on('file', function (file) {
|
||||
watchFile(file);
|
||||
});
|
||||
|
||||
b.on('package', function (pkg) {
|
||||
var file = path.join(pkg.__dirname, 'package.json');
|
||||
if (fs.existsSync(file)) {
|
||||
watchFile(file);
|
||||
}
|
||||
if (pkgcache) pkgcache[file] = pkg;
|
||||
});
|
||||
|
||||
b.on('reset', reset);
|
||||
reset();
|
||||
|
||||
function reset () {
|
||||
var time = null;
|
||||
var bytes = 0;
|
||||
b.pipeline.get('record').on('end', function () {
|
||||
time = Date.now();
|
||||
});
|
||||
|
||||
b.pipeline.get('wrap').push(through(write, end));
|
||||
function write (buf, enc, next) {
|
||||
bytes += buf.length;
|
||||
this.push(buf);
|
||||
next();
|
||||
}
|
||||
function end () {
|
||||
var delta = Date.now() - time;
|
||||
b.emit('time', delta);
|
||||
b.emit('bytes', bytes);
|
||||
b.emit('log', bytes + ' bytes written ('
|
||||
+ (delta / 1000).toFixed(2) + ' seconds)'
|
||||
);
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
var fwatchers = {};
|
||||
var fwatcherFiles = {};
|
||||
var ignoredFiles = {};
|
||||
|
||||
b.on('transform', function (tr, mfile) {
|
||||
tr.on('file', function (dep) {
|
||||
watchFile(mfile, dep);
|
||||
});
|
||||
});
|
||||
b.on('bundle', function (bundle) {
|
||||
updating = true;
|
||||
bundle.on('error', onend);
|
||||
bundle.on('end', onend);
|
||||
function onend () { updating = false }
|
||||
});
|
||||
|
||||
function watchFile (file, dep) {
|
||||
dep = dep || file;
|
||||
if (!fwatchers[file]) fwatchers[file] = [];
|
||||
if (!fwatcherFiles[file]) fwatcherFiles[file] = [];
|
||||
if (fwatcherFiles[file].indexOf(dep) >= 0) return;
|
||||
|
||||
var w = b._watcher(dep, wopts);
|
||||
w.setMaxListeners(0);
|
||||
w.on('error', b.emit.bind(b, 'error'));
|
||||
w.on('change', function () {
|
||||
invalidate(file);
|
||||
});
|
||||
fwatchers[file].push(w);
|
||||
fwatcherFiles[file].push(dep);
|
||||
}
|
||||
|
||||
function getMTime(filePath) {
|
||||
var mtime;
|
||||
|
||||
try {
|
||||
fs.statSync(filePath).mtime.getTime();
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
mtime = new Date().getTime();
|
||||
} else {
|
||||
throw(e);
|
||||
}
|
||||
}
|
||||
|
||||
return mtime;
|
||||
}
|
||||
|
||||
function invalidate (id) {
|
||||
var mtime = getMTime(id);
|
||||
if ((mtimes[id] || 0) >= mtime) return;
|
||||
mtimes[id] = mtime;
|
||||
|
||||
if (cache) delete cache[id];
|
||||
if (pkgcache) delete pkgcache[id];
|
||||
changingDeps[id] = true;
|
||||
|
||||
if (!updating && fwatchers[id]) {
|
||||
fwatchers[id].forEach(function (w) {
|
||||
w.close();
|
||||
});
|
||||
delete fwatchers[id];
|
||||
delete fwatcherFiles[id];
|
||||
}
|
||||
|
||||
// wait for the disk/editor to quiet down first:
|
||||
if (pending) clearTimeout(pending);
|
||||
pending = setTimeout(notify, delay);
|
||||
}
|
||||
|
||||
function notify () {
|
||||
if (updating) {
|
||||
pending = setTimeout(notify, delay);
|
||||
} else {
|
||||
pending = false;
|
||||
b.emit('update', Object.keys(changingDeps));
|
||||
changingDeps = {};
|
||||
}
|
||||
}
|
||||
|
||||
b.close = function () {
|
||||
Object.keys(fwatchers).forEach(function (id) {
|
||||
fwatchers[id].forEach(function (w) { w.close() });
|
||||
});
|
||||
};
|
||||
|
||||
b._watcher = function (file, opts) {
|
||||
return fs.watch(file, opts);
|
||||
};
|
||||
|
||||
return b;
|
||||
}
|
||||
132
uebersicht-code/server/src/widgetify.js
Executable file
132
uebersicht-code/server/src/widgetify.js
Executable file
@@ -0,0 +1,132 @@
|
||||
var through = require('through2');
|
||||
var esprima = require('esprima');
|
||||
var escodegen = require('escodegen');
|
||||
var stylus = require('stylus');
|
||||
var nib = require('nib');
|
||||
var ms = require('ms');
|
||||
|
||||
function addExports(node) {
|
||||
var widgetObjectExp = node.expression;
|
||||
|
||||
node.expression = {
|
||||
type: 'AssignmentExpression',
|
||||
operator: '=',
|
||||
left: { type: 'Identifier', name: 'module.exports' },
|
||||
right: widgetObjectExp,
|
||||
};
|
||||
}
|
||||
|
||||
function addId(widgetObjectExp, widetId) {
|
||||
var idProperty = {
|
||||
type: 'Property',
|
||||
key: { type: 'Identifier', name: 'id' },
|
||||
value: { type: 'Literal', value: widetId },
|
||||
computed: false,
|
||||
};
|
||||
|
||||
widgetObjectExp.properties.push(idProperty);
|
||||
}
|
||||
|
||||
function flattenStyle(styleProp, tree) {
|
||||
var preface = {
|
||||
type: 'Program',
|
||||
body: tree.body.slice(0, -1),
|
||||
};
|
||||
|
||||
preface.body.push({
|
||||
type: 'ExpressionStatement',
|
||||
expression: styleProp.value,
|
||||
});
|
||||
|
||||
return eval(escodegen.generate(preface));
|
||||
}
|
||||
|
||||
function parseStyle(styleProp, widetId, tree) {
|
||||
var styleString;
|
||||
|
||||
if (styleProp.value.type === 'Literal') {
|
||||
styleString = styleProp.value.value;
|
||||
} else {
|
||||
styleString = flattenStyle(styleProp, tree);
|
||||
}
|
||||
|
||||
if (typeof styleString !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
var scopedStyle = '#' + widetId
|
||||
+ '\n '
|
||||
+ styleString.replace(/\n/g, '\n ');
|
||||
|
||||
var css = stylus(scopedStyle)
|
||||
.import('nib')
|
||||
.use(nib())
|
||||
.render();
|
||||
|
||||
styleProp.key.name = 'css';
|
||||
styleProp.value.type = 'Literal';
|
||||
styleProp.value.value = css;
|
||||
}
|
||||
|
||||
function parseRefreshFrequency(prop) {
|
||||
if (typeof prop.value.value === 'string') {
|
||||
prop.value.value = ms(prop.value.value);
|
||||
}
|
||||
}
|
||||
|
||||
function parseWidgetProperty(prop, widgetId, tree) {
|
||||
switch (prop.key.name) {
|
||||
case 'style': parseStyle(prop, widgetId, tree); break;
|
||||
case 'refreshFrequency': parseRefreshFrequency(prop); break;
|
||||
}
|
||||
}
|
||||
|
||||
function modifyAST(tree, widgetId) {
|
||||
var widgetObjectExp = getWidgetObjectExpression(tree);
|
||||
|
||||
if (widgetObjectExp) {
|
||||
widgetObjectExp.properties.map(function(prop) {
|
||||
parseWidgetProperty(prop, widgetId, tree);
|
||||
});
|
||||
addId(widgetObjectExp, widgetId);
|
||||
addExports(tree.body[tree.body.length - 1]);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
function getWidgetObjectExpression(tree) {
|
||||
var lastStatement = tree.body[tree.body.length - 1];
|
||||
|
||||
if (lastStatement && lastStatement.type === 'ExpressionStatement' ) {
|
||||
var widgetObjectExp = lastStatement.expression;
|
||||
if (widgetObjectExp.type === 'ObjectExpression') {
|
||||
return widgetObjectExp;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
module.exports = function(file, options) {
|
||||
var widgetId = options.id;
|
||||
var src = '';
|
||||
|
||||
function write(buf, enc, next) { src += buf; next(); }
|
||||
function end(next) {
|
||||
var tree;
|
||||
try {
|
||||
tree = esprima.parse(src);
|
||||
if (tree) {
|
||||
this.push(escodegen.generate(modifyAST(tree, widgetId)));
|
||||
}
|
||||
} catch (e) {
|
||||
this.emit('error', e);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
return through(write, end);
|
||||
};
|
||||
|
||||
1
uebersicht-code/server/widgets/.pydio
Normal file
1
uebersicht-code/server/widgets/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
4fa758e9-532d-45cc-b203-d99026f2950b
|
||||
60
uebersicht-code/server/widgets/GettingStarted.jsx
Normal file
60
uebersicht-code/server/widgets/GettingStarted.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { css, run } from "uebersicht"
|
||||
|
||||
export const command = "ls -a"
|
||||
|
||||
export const refreshFrequency = 100000 // ms
|
||||
|
||||
export const className = css`
|
||||
left: 40px;
|
||||
top: 50px;
|
||||
`
|
||||
const hello = css`
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
`
|
||||
|
||||
const liclass = css`
|
||||
color: #fff;
|
||||
`
|
||||
|
||||
// 定义变量
|
||||
export const initialState = {
|
||||
count: 2,
|
||||
list: [
|
||||
{ id: 1, title: '你好1' },
|
||||
{ id: 2, title: '你好2' },
|
||||
{ id: 3, title: '你好3' },
|
||||
]
|
||||
};
|
||||
|
||||
// 初始化函数
|
||||
export const init = (dispatch) => {
|
||||
console.log("init run....")
|
||||
}
|
||||
|
||||
// 更新数据状态,刷新页面
|
||||
export const updateState = (event, previousState) => {
|
||||
console.log("event: ", event);
|
||||
console.log("previousState: ", previousState);
|
||||
return Object.assign(previousState,event);
|
||||
}
|
||||
|
||||
export const render = ({ output, count, list }, dispatch) => (
|
||||
<div>
|
||||
<h1 className={hello}>测试:{count}</h1>
|
||||
<h1 className={hello}>你好世界{output}</h1>
|
||||
<ul>
|
||||
{
|
||||
list.map((e,i) => {
|
||||
return <li className={liclass} key={i}>{e.title}</li>
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<button onClick={() => {
|
||||
count+=1;
|
||||
dispatch({ count: count })
|
||||
console.log("点击事件");
|
||||
|
||||
}}>button按钮 - 点我</button>
|
||||
</div>
|
||||
)
|
||||
BIN
uebersicht-code/server/widgets/logo.png
Normal file
BIN
uebersicht-code/server/widgets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Reference in New Issue
Block a user