Showing preview only (311K chars total). Download the full file or copy to clipboard to get everything.
Repository: vudash/vudash
Branch: master
Commit: b560ef804a10
Files: 244
Total size: 252.0 KB
Directory structure:
gitextract_gganvaua/
├── .codeclimate.json
├── .gitignore
├── .travis.yml
├── README.MD
├── docs/
│ ├── .nojekyll
│ ├── README.md
│ ├── _coverpage.md
│ ├── api/
│ │ └── README.md
│ ├── datasources/
│ │ └── README.md
│ ├── developers/
│ │ └── README.md
│ ├── index.html
│ ├── transformers/
│ │ └── README.md
│ └── widgets/
│ └── README.md
├── lerna.json
├── package.json
├── packages/
│ ├── core/
│ │ ├── README.md
│ │ ├── app.js
│ │ ├── bin/
│ │ │ └── vudash.js
│ │ ├── dashboards/
│ │ │ ├── simple.json
│ │ │ └── template.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli/
│ │ │ │ ├── create.js
│ │ │ │ ├── help.js
│ │ │ │ └── logo.js
│ │ │ ├── config-validator/
│ │ │ │ ├── config-validator.spec.js
│ │ │ │ └── index.js
│ │ │ ├── dashboard/
│ │ │ │ ├── bundler/
│ │ │ │ │ └── index.js
│ │ │ │ ├── compiler/
│ │ │ │ │ ├── compiler.spec.js
│ │ │ │ │ ├── configuration-builder/
│ │ │ │ │ │ ├── configuration-builder.js
│ │ │ │ │ │ ├── configuration-builder.spec.js
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── dashboard.js
│ │ │ │ ├── dashboard.spec.js
│ │ │ │ ├── emitter/
│ │ │ │ │ ├── emitter.js
│ │ │ │ │ ├── emitter.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ ├── loader/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── loader.js
│ │ │ │ │ └── loader.spec.js
│ │ │ │ ├── parser/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── parser.js
│ │ │ │ │ └── parser.spec.js
│ │ │ │ ├── renderer/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── renderer.js
│ │ │ │ │ └── renderer.spec.js
│ │ │ │ └── schema/
│ │ │ │ ├── index.js
│ │ │ │ ├── schema.js
│ │ │ │ └── schema.spec.js
│ │ │ ├── dashboard-event/
│ │ │ │ ├── dashboard-event.js
│ │ │ │ └── index.js
│ │ │ ├── datasource/
│ │ │ │ ├── datasource.spec.js
│ │ │ │ ├── dummy-datasource/
│ │ │ │ │ ├── dummy-datasource.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ ├── locator/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── locator.spec.js
│ │ │ │ └── validator/
│ │ │ │ ├── index.js
│ │ │ │ └── validator.spec.js
│ │ │ ├── datasource-binder/
│ │ │ │ ├── datasource-binder.js
│ │ │ │ ├── datasource-binder.spec.js
│ │ │ │ ├── datasource-emitter.js
│ │ │ │ └── index.js
│ │ │ ├── datasource-loader/
│ │ │ │ ├── datasource-loader.js
│ │ │ │ ├── datasource-loader.spec.js
│ │ │ │ └── index.js
│ │ │ ├── errors/
│ │ │ │ ├── configuration.error.js
│ │ │ │ ├── index.js
│ │ │ │ ├── not-found.error.js
│ │ │ │ ├── plugin-registration.error.js
│ │ │ │ └── widget-registration.error.js
│ │ │ ├── id-gen/
│ │ │ │ ├── id-gen.js
│ │ │ │ ├── id-gen.spec.js
│ │ │ │ └── index.js
│ │ │ ├── plugins/
│ │ │ │ ├── api/
│ │ │ │ │ ├── api.js
│ │ │ │ │ ├── handlers/
│ │ │ │ │ │ ├── dashboards/
│ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ ├── put.js
│ │ │ │ │ │ │ └── put.spec.js
│ │ │ │ │ │ └── view/
│ │ │ │ │ │ └── current/
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── put.js
│ │ │ │ │ │ └── put.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── socket/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── socket.js
│ │ │ │ ├── static/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── static.js
│ │ │ │ └── ui/
│ │ │ │ ├── handlers/
│ │ │ │ │ ├── dashboard/
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── index.spec.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ui.js
│ │ │ │ └── ui.spec.js
│ │ │ ├── public/
│ │ │ │ ├── css/
│ │ │ │ │ ├── listing.css
│ │ │ │ │ └── style.css
│ │ │ │ └── js/
│ │ │ │ ├── audio.js
│ │ │ │ └── object-assign.polyfill.js
│ │ │ ├── resolver/
│ │ │ │ ├── index.js
│ │ │ │ ├── resolver.js
│ │ │ │ └── resolver.spec.js
│ │ │ ├── server.js
│ │ │ ├── transform-loader/
│ │ │ │ ├── index.js
│ │ │ │ ├── transform-loader.js
│ │ │ │ └── transform-loader.spec.js
│ │ │ ├── upper-camel/
│ │ │ │ ├── index.js
│ │ │ │ └── upper-camel.spec.js
│ │ │ ├── views/
│ │ │ │ ├── dashboard.html
│ │ │ │ └── listing.html
│ │ │ ├── widget/
│ │ │ │ ├── history/
│ │ │ │ │ ├── history.js
│ │ │ │ │ ├── history.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ ├── loader/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── loader.spec.js
│ │ │ │ ├── renderer/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── renderer.spec.js
│ │ │ │ ├── validator/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── validator.js
│ │ │ │ │ └── validator.spec.js
│ │ │ │ ├── widget-position/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── widget-position.spec.js
│ │ │ │ └── widget.spec.js
│ │ │ ├── widget-binder/
│ │ │ │ ├── index.js
│ │ │ │ ├── widget-binder.js
│ │ │ │ └── widget-binder.spec.js
│ │ │ └── widget-datasource-binding/
│ │ │ ├── index.js
│ │ │ ├── widget-datasource-binding.js
│ │ │ └── widget-datasource-binding.spec.js
│ │ └── test/
│ │ ├── resources/
│ │ │ └── widgets/
│ │ │ ├── broken/
│ │ │ │ ├── package.json
│ │ │ │ └── widget.js
│ │ │ ├── configurable/
│ │ │ │ ├── component.html
│ │ │ │ ├── package.json
│ │ │ │ └── widget.js
│ │ │ ├── example/
│ │ │ │ ├── markup.html
│ │ │ │ ├── package.json
│ │ │ │ └── widget.js
│ │ │ └── missing/
│ │ │ ├── package.json
│ │ │ └── widget.js
│ │ └── util/
│ │ ├── dashboard.builder.js
│ │ ├── datasource.builder.js
│ │ └── widget.builder.js
│ ├── datasource-google-sheets/
│ │ ├── datasource.js
│ │ ├── datasource.spec.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── config-validator/
│ │ │ │ ├── config-validator.spec.js
│ │ │ │ └── index.js
│ │ │ └── google-sheets-transport/
│ │ │ ├── google-sheets-transport.spec.js
│ │ │ └── index.js
│ │ └── test/
│ │ ├── config.util.js
│ │ ├── example.credentials.test.json
│ │ └── example.invalid-credentials.test.json
│ ├── datasource-random/
│ │ ├── datasource.js
│ │ ├── datasource.spec.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── datasource-validation/
│ │ │ └── index.js
│ │ └── random-transport/
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── datasource-rest/
│ │ ├── datasource.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── datasource-validation/
│ │ │ ├── datasource-validation.js
│ │ │ ├── datasource-validation.spec.js
│ │ │ └── index.js
│ │ └── rest-transport/
│ │ ├── index.js
│ │ └── rest-transport.spec.js
│ ├── datasource-value/
│ │ ├── datasource.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── datasource-validation/
│ │ │ ├── datasource-validation.spec.js
│ │ │ └── index.js
│ │ └── value-transport/
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── transformer-jq/
│ │ ├── index.js
│ │ ├── lib/
│ │ │ ├── transformer.js
│ │ │ └── transformer.spec.js
│ │ └── package.json
│ ├── transformer-map/
│ │ ├── package.json
│ │ └── transformer.js
│ ├── widget-chart/
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ ├── chart-options.js
│ │ │ └── markup.html
│ │ └── server/
│ │ ├── index.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-ci/
│ │ ├── .gitignore
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── build-status.enum.js
│ │ ├── client/
│ │ │ └── markup.html
│ │ ├── engines/
│ │ │ ├── circleci/
│ │ │ │ ├── circleci.spec.js
│ │ │ │ └── index.js
│ │ │ ├── factory.js
│ │ │ └── travis/
│ │ │ ├── index.js
│ │ │ └── travis.spec.js
│ │ └── server/
│ │ ├── index.js
│ │ ├── validation.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-gauge/
│ │ ├── .gitignore
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── markup.html
│ │ └── server/
│ │ ├── index.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-health/
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── component.html
│ │ └── server/
│ │ ├── index.js
│ │ └── widget.js
│ ├── widget-progress/
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── component.html
│ │ └── server/
│ │ ├── index.js
│ │ └── widget.js
│ ├── widget-statistic/
│ │ ├── .gitignore
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── markup.html
│ │ └── server/
│ │ ├── index.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-status/
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── markup.html
│ │ ├── health-status.js
│ │ ├── providers/
│ │ │ ├── github/
│ │ │ │ ├── github.spec.js
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── statuspageio/
│ │ │ ├── index.js
│ │ │ └── statuspageio.spec.js
│ │ └── server/
│ │ ├── index.js
│ │ ├── validator.js
│ │ └── widget.js
│ └── widget-time/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ └── src/
│ ├── client/
│ │ └── markup.html
│ ├── server/
│ │ ├── alarms.js
│ │ ├── index.js
│ │ ├── validation.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ └── time/
│ ├── index.js
│ └── time.spec.js
└── test/
└── unit.lab.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .codeclimate.json
================================================
{
"version": "2",
"exclude_patterns": [
"**/**/**.spec.js"
]
}
================================================
FILE: .gitignore
================================================
node_modules
*.log
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "9"
matrix:
fast_finish: true
cache:
directories:
- ~/.npm
- node_modules
- packages/**/node_modules
env:
matrix:
- PACKAGE=vudash
- PACKAGE=@vudash/datasource-rest
- PACKAGE=@vudash/datasource-random
- PACKAGE=@vudash/datasource-value
- PACKAGE=@vudash/datasource-google-sheets
- PACKAGE=vudash-widget-ci
- PACKAGE=vudash-widget-gauge
- PACKAGE=vudash-widget-progress
- PACKAGE=vudash-widget-statistic
- PACKAGE=vudash-widget-time
- PACKAGE=vudash-widget-status
- PACKAGE=@vudash/widget-chart
script:
- lerna run lint --scope $TEST_DIR
- lerna run test --scope $TEST_DIR
================================================
FILE: README.MD
================================================
# Vudash
An open-source, configurable, extensible dashboard for monitoring, marketing, and more.
Note that this project is a lerna `monorepo`, individual packages in the vudash family are under `/packages`
[](https://gitter.im/vudash/vudash-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/vudash/vudash) [](http://standardjs.com/)
[](https://www.codacy.com/app/ant/vudash?utm_source=github.com&utm_medium=referral&utm_content=vudash/vudash&utm_campaign=Badge_Grade)
[](https://codeclimate.com/github/vudash/vudash/maintainability)
[](https://www.codefactor.io/repository/github/vudash/vudash)
See this project on NPM: [Vudash](https://npmjs.org/vudash)
# Screenshots


# Product Demo
* Removed due to domain squatters.
Got a dashboard you want to showcase? Let us know!
# Quick Start
```
npm -g install vudash
vudash create
vudash
```
================================================
FILE: docs/.nojekyll
================================================
================================================
FILE: docs/README.md
================================================
# Vudash
[](https://gitter.im/vudash/vudash-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/vudash/vudash) [](http://standardjs.com/)
[](https://www.codacy.com/app/ant/vudash?utm_source=github.com&utm_medium=referral&utm_content=vudash/vudash&utm_campaign=Badge_Grade)
[](https://codeclimate.com/github/vudash/vudash/maintainability)
[](https://www.codefactor.io/repository/github/vudash/vudash)
A dashboard, like dashing, but written in NodeJS.
Vudash open source component
Writen using hapijs, lab, material ui, socket.io, lerna, and svelte
# Quick start
In so few lines:
```bash
npm install -g vudash
vudash create
vudash
```
# Usage
Install as a global module `npm install -g vudash` and use `vudash create` to create an example dashboard.
Add new widgets under `/widgets` and add them to your dashboard under `/dashboards`.
You can visit your created dashboard by visiting http://localhost:3300/`dashboard`.dashboard - where `dashboard` is the name of a JSON file within the `/dashboards` directory.
Visiting the root of the application will yield a list of all available dashboards, unless the environment variable `DEFAULT_DASHBOARD` is set, in which case that dashboard will be loaded instead. Other dashboards will still be available via the normal methods.
# Screenshots


# Demo
- [Demo Dashboard](http://vudash.herokuapp.com/demo.dashboard)
- [Crypto Dashboard](http://vudash.herokuapp.com/crypto.dashboard)
If, like me, you learn by example rather than reams of documentation, check out the [Demo Dashboard's Configuration](https://github.com/vudash/vudash-demo/blob/master/dashboards/demo.json) on github. You can then clarify any questions using the documentation below.
# Dashboards
A dashboard is a collection of widgets separated into rows and columns.
## Creating Dashboards
Dashboards are in JSON format and take the form:
```javascript
{
"name": "Happy",
"layout": {
"columns": 5,
"rows": 4
},
"datasources": {
"datasource-exchange-rates": {
"module": "@vudash/datasource-rest",
"schedule": 30000,
"options": {
"url": "http://exchangerat.es/api/v1/rates",
"method": "get",
"graph": "rates.GBP"
}
}
},
"widgets": [
{ "position": {"x": 0, "y": 0, "w": 1, "h": 1}, "widget": "./widgets/random" },
{ "position": {"x": 3, "y": 0, "w": 2, "h": 1}, "widget": "vudash-widget-time" },
{ "position": {"x": 4, "y": 1, "w": 1, "h": 1}, "widget": "./widgets/github" },
{ "position": {"x": 0, "y": 1, "w": 2, "h": 1},
"widget": "vudash-widget-statistic",
"datasource": "datasource-exchange-rates",
"history": 100,
"options": {
"description": "EUR -> GBP",
}
},
{
"position": {"x": 4, "y": 2, "w": 1, "h": 1},
"widget": "@vudash/widget-ci",
"options": {
"schedule": 60000,
"user": "vudash",
"repo": "vudash-widget-ci"
}
}
]
}
```
Where 'widgets' is an array of widgets. The position in the grid (specified by `layout`) is indicated by the widget's `x` and `y` `position` values.
The values for `position.w` and `position.h` are the number of grid units the widget occupies in width and height, respectively.
The `history` attribute of a widget defines how many historical items a widget should store (i.e. where history is `X`, the widget will store `X` previous values) - the value history can be read by widgets, and used in things like graphs.
Widgets can be either a path to a directory containing a widget (see below), or an npm module of the same. If the widget is a npm module, you would need to `npm install --save <widget-name>` first.
### Environment variables
You can use environment variables in your dashboard or widget configuration:
```javascript
{
"position": { ... },
"widget": "@vudash/widget-ci",
"options": {
"user": "vudash",
"repo": "vudash-widget-ci",
"auth": {
"$env": "ENVIRONMENT_VARIABLE_NAME"
}
}
}
```
Where the value of `auth` in the configuration will be replaced with the contents of the environment variable `ENVIRONMENT_VARIABLE_NAME`.
## Custom CSS
You can add to (or override) the CSS for a dashboard, using the `css` attribute in your dashboard's json configuration.
Because the dashboard configuration is in JSON format, your CSS must be too, and uses the [json-to-css package](https://www.npmjs.com/package/json-to-css) to transform json into minified CSS.
As a (rather ugly) example, lets change the dashboard's background colour to red.
```javascript
{
"name": "dashboard-with-custom-css",
"layout": { ... },
"css": {
"body": {
"background-color": "red"
}
},
"datasources": { ... },
"widgets": [ ... ]
}
```
As you can see, the hash under `css` follows the basic format of css, and is rendered into the dashboard after all the default vudash, and widget generated CSS.
# Widgets
Widgets are configured as an array in the `dashboard.json` file, in the format:
```javascript
"widgets": [
{
"widget": "./widgets/pluck", // widget file path, node module name, or class definition
"datasource": "datasource-xyz", // name of a datasource listed in `datasources`
"position": {
"x": 1, // x position (row number) of widget
"y": 1, // y position (column number) of widget
"w": 1, // widget width in columns
"h": 1 // widget height in columns
},
"options": { // widget specific config
"your" : "config"
}
}
]
```
Widgets have some optional properties:
| property name | description | example |
| ------------- | ------------------------------------ | ------- |
| background | css for "background" style attribute | #ffffff |
For a list of built in widgets, see [Widgets](widgets/).
For developing widgets see [Developing Widgets](developers/?id=developing-widgets).
# Datasources
Unless a widget specifies its own data fetching method, data is fetched by a datasource.
Datasources are specified as a hash in the `dashboard.json` as follows:
```javascript
{
"datasources": {
"datasource-id": { // can be anything as long as it is unique
"module": "../datasource-random", // as with widgets, a node module name or directory
"schedule": 1000, // how often (in ms) the datasource should be refreshed
"options": { // options for the datasource
"method": "string"
}
}
}
}
```
Each refresh, the datasource will fetch new data, and tell all widgets that listen to it about the new data.
For a list of built in datasources, see [Datasources](datasources/).
For developing datasources see [Developing Datasources](developers/?id=developing-datasources).
# Configuration
When running the server, a number of environment variables are available:
| environment variable | description | default value |
| ------------------------- | ---------------------------------------------------------------------- | ------------- |
| DEFAULT_DASHBOARD | specify default dashboard to mount at / | none |
| DISCONNECT_RELOAD_TIMEOUT | default number of milliseconds to wait to reload if server disconnects | 30000 |
| API_KEY | api key used to access the vudash api | (random) |
| SERVER_URL | external server url (for when node can't resolve it by itself) | (inferred) |
# Tips and tricks
## Securing your dashboard with basic auth
Want to protect your dashboard from the public eye? You can secure it with basic auth in a few steps:
1. Install basic-auth and http-proxy modules:
```javascript
npm i -S basic-auth http-proxy
```
2. Change the start script in package.json
```json
{
"scripts": {
"start": "node ./proxy"
}
}
```
3. Create a simple proxy server called `proxy.js` in your project's root directory
```javascript
'use strict'
const http = require('http')
const httpProxy = require('http-proxy')
const auth = require('basic-auth')
const proxy = httpProxy.createProxyServer()
function verify (credentials) {
const user = process.env.BASIC_AUTH_NAME
const pass = process.env.BASIC_AUTH_PASS
return credentials && credentials.name === user && credentials.pass === pass
}
http.createServer((req, res) => {
const credentials = auth(req)
if (verify (credentials)) {
return proxy.web(req, res, {
target: 'http://localhost:3300'
})
}
res.statusCode = 401
res.setHeader('WWW-Authenticate', 'Basic realm="example"')
res.end('Access denied')
}).listen(process.env.PORT)
process.env.PORT = 3300
require('vudash')
```
4. When you run the project, don't forget your credentials:
```bash
BASIC_AUTH_NAME=username BASIC_AUTH_PASS=password npm run start
```
# Troubleshooting
* Q. The console shows that the websocket is failing to connect, and my widgets aren't updating.
* A. Your hosting provider might not be correctly reporting the external vhost of the server. Add an environment variable `SERVER_URL` with the full url to your server, i.e: `http://www.example.com/`
# Contributing
## Running Tests
Vudash > 5 is a monorepo! This makes it easier to contribute, and keep track of all the native plugins.
Clone the project and run:
```
lerna bootstrap
lerna run test
```
# Why create Vudash?
* I'll get to the point. I like dashing, but I don't like ruby.
* Both Dashing and Dashing-js are stellar efforts, but abandoned.
* Jade is an abomination.
* Coffeescript is an uneccessary abstraction.
* dashing-js has a lot of bugs
# Features
* will happily run on heroku, now.sh, or any other hosting you fancy.
* es6
* all cross-origin requests done server side
* websockets rather than polling
* websocket fallback to long-poll when sockets aren't available
* Custom widgets
* Custom dashboards
* Simple row-by-row layout
* Super simple widget structure
# Roadmap
- now.sh 5-second howto
- You, sending Pull Requests.
- Plugins
# Credits
- Concept and foundation by Antony Jones / Desirable Objects Ltd
- Contributions from github committers
- Contains svg imagery from flaticons, by [Gregor Cresnar](http://www.flaticon.com/authors/gregor-cresnar), [Vectors Market](http://www.flaticon.com/authors/vectors-market)
- Various fixes and improvements by [Alex Voigt](https://github.com/alex-voigt)
================================================
FILE: docs/_coverpage.md
================================================
<svg class="logo" viewBox="0 0 176.08649 106.1699">
<g transform="translate(-24.377661,-51.731245)">
<g>
<path d="M 52.739091,115.62048 41.50574,141.57211 H 35.573938 L 24.377661,115.62048 h 6.487909 l 7.896712,18.53688 8.007933,-18.53688 z" />
<path d="m 66.978891,142.017 q -5.561064,0 -8.675261,-3.07712 -3.077122,-3.07713 -3.077122,-8.78649 v -14.53291 h 6.00595 v 14.31047 q 0,6.96987 5.783507,6.96987 2.817606,0 4.300557,-1.66832 1.48295,-1.70539 1.48295,-5.30155 v -14.31047 h 5.931803 v 14.53291 q 0,5.70936 -3.114196,8.78649 -3.077123,3.07712 -8.638188,3.07712 z" />
<path d="m 86.409602,128.55922 q -1.520025,0 -2.55809,-1.03806 -1.038066,-1.03807 -1.038066,-2.59517 0,-1.59417 1.038066,-2.55809 1.038065,-1.00099 2.55809,-1.00099 1.520024,0 2.558089,1.00099 1.038066,0.96392 1.038066,2.55809 0,1.5571 -1.038066,2.59517 -1.038065,1.03806 -2.558089,1.03806 z m 0,13.30948 q -1.520025,0 -2.55809,-1.03806 -1.038066,-1.03807 -1.038066,-2.59517 0,-1.59417 1.038066,-2.55809 1.038065,-1.00099 2.55809,-1.00099 1.520024,0 2.558089,1.00099 1.038066,0.96392 1.038066,2.55809 0,1.5571 -1.038066,2.59517 -1.038065,1.03806 -2.558089,1.03806 z" />
<path d="m 94.332957,115.62048 h 11.789453 q 4.22641,0 7.45183,1.63124 3.26249,1.59418 5.04203,4.523 1.81662,2.92883 1.81662,6.82158 0,3.89274 -1.81662,6.82157 -1.77954,2.92883 -5.04203,4.56007 -3.22542,1.59417 -7.45183,1.59417 H 94.332957 Z m 11.492863,21.02082 q 3.89275,0 6.19132,-2.15028 2.33565,-2.18735 2.33565,-5.89472 0,-3.70738 -2.33565,-5.85766 -2.29857,-2.18735 -6.19132,-2.18735 h -5.48691 v 16.09001 z" />
<path d="m 141.69122,136.01105 h -12.04897 l -2.29858,5.56106 h -6.15424 l 11.56701,-25.95163 h 5.93181 l 11.60408,25.95163 h -6.30254 z m -1.89076,-4.56007 -4.11519,-9.93577 -4.11519,9.93577 z" />
<path d="m 161.51178,142.017 q -3.07712,0 -5.96888,-0.81562 -2.85468,-0.8527 -4.59714,-2.18736 l 2.03905,-4.523 q 1.66832,1.22344 3.9669,1.96491 2.29857,0.74148 4.59714,0.74148 2.55809,0 3.78153,-0.74148 1.22343,-0.77855 1.22343,-2.03905 0,-0.92685 -0.74147,-1.52003 -0.70441,-0.63025 -1.85369,-1.00099 -1.11222,-0.37074 -3.04005,-0.81562 -2.9659,-0.7044 -4.85666,-1.40881 -1.89077,-0.7044 -3.2625,-2.2615 -1.33465,-1.55709 -1.33465,-4.15226 0,-2.2615 1.22343,-4.07811 1.22344,-1.85369 3.67031,-2.92883 2.48394,-1.07514 6.04302,-1.07514 2.48394,0 4.85666,0.59318 2.37272,0.59318 4.15226,1.7054 l -1.85368,4.56007 q -3.59616,-2.03906 -7.19231,-2.03906 -2.52102,0 -3.74446,0.81562 -1.18636,0.81563 -1.18636,2.15028 0,1.33466 1.37173,2.00199 1.40881,0.63025 4.26349,1.2605 2.9659,0.70441 4.85666,1.40881 1.89076,0.7044 3.22542,2.22442 1.37173,1.52003 1.37173,4.11519 0,2.22443 -1.26051,4.07812 -1.22344,1.81661 -3.70738,2.89175 -2.48394,1.07514 -6.04302,1.07514 z" />
<path d="m 200.46415,115.62048 v 25.95163 h -6.00595 v -10.64017 h -11.78946 v 10.64017 h -6.00595 v -25.95163 h 6.00595 v 10.23236 h 11.78946 v -10.23236 z" />
</g>
<g transform="matrix(0.69851314,0,0,0.7944649,208.52468,40.27109)">
<g>
<rect y="14.424998" x="-177.00626" height="18.785416" width="37.570831" />
<rect y="36.782299" x="-177.27081" height="43.65625" width="37.570835" />
</g>
<g transform="matrix(1,0,0,-1,41.539557,94.863551)">
<rect y="14.424998" x="-177.00626" height="18.785416" width="37.570831" />
<rect y="36.782299" x="-177.27081" height="43.65625" width="37.570835" />
</g>
</g>
<g>
<path d="m 60.611902,151.0302 h 2.492399 q 0.981562,0 1.741792,0.40418 0.76023,0.39455 1.183649,1.1644 0.423419,0.76023 0.423419,1.79953 0,1.0393 -0.423419,1.80916 -0.423419,0.76023 -1.183649,1.1644 -0.76023,0.39455 -1.741792,0.39455 h -2.492399 z m 2.492399,5.90863 q 1.12591,0 1.732169,-0.68325 0.615882,-0.69287 0.615882,-1.85727 0,-1.1644 -0.615882,-1.84765 -0.606259,-0.69286 -1.732169,-0.69286 h -1.578199 v 5.08103 z" />
<path d="m 70.974544,155.94764 h -2.579008 l -0.644752,1.81878 h -0.962316 l 2.415414,-6.73622 h 0.962316 l 2.415414,6.73622 h -0.962316 z m -0.288695,-0.82759 -1.000809,-2.80996 -1.000809,2.80996 z" />
<path d="m 75.336092,157.90114 q -0.596636,0 -1.212519,-0.13472 -0.615882,-0.1251 -0.991185,-0.29832 l 0.125101,-0.97194 q 0.481158,0.23096 1.087417,0.40417 0.606259,0.17322 1.202895,0.17322 0.673622,0 1.039302,-0.24058 0.375303,-0.2502 0.375303,-0.74098 0,-0.36568 -0.192463,-0.60626 -0.18284,-0.2502 -0.54852,-0.4138 -0.356057,-0.17321 -1.000809,-0.36568 -0.654375,-0.19246 -1.087418,-0.41379 -0.433042,-0.22134 -0.70249,-0.60626 -0.259826,-0.38493 -0.259826,-0.97194 0,-0.81797 0.625506,-1.31837 0.635129,-0.50041 1.780285,-0.50041 0.615882,0 1.164402,0.13472 0.558144,0.13473 0.97194,0.32719 l -0.09623,0.97194 q -0.538897,-0.31756 -1.039301,-0.46191 -0.500405,-0.14435 -1.039302,-0.14435 -0.625505,0 -1.000809,0.23096 -0.36568,0.23095 -0.36568,0.71211 0,0.32719 0.163594,0.5389 0.173217,0.21171 0.490781,0.36568 0.317565,0.14435 0.885331,0.31756 1.135533,0.34644 1.693677,0.8276 0.558143,0.47153 0.558143,1.30875 0,0.88533 -0.663998,1.38573 -0.663998,0.49078 -1.963125,0.49078 z" />
<path d="m 79.031236,151.0302 h 0.9142 v 2.7811 h 3.425846 v -2.7811 h 0.914201 v 6.73622 h -0.914201 v -3.08904 h -3.425846 v 3.08904 h -0.9142 z" />
<path d="m 89.194046,154.09999 q 0.635129,0.15397 0.971939,0.59664 0.336811,0.43304 0.336811,1.14516 0,0.89495 -0.57739,1.4146 -0.567766,0.51003 -1.568575,0.51003 h -2.540515 v -6.73622 h 2.126719 q 0.923823,0 1.424228,0.4138 0.510027,0.4138 0.510027,1.23177 0,0.48115 -0.173216,0.85646 -0.173217,0.3753 -0.510028,0.56776 z m -2.46353,-0.26944 h 1.097041 q 0.538897,0 0.817969,-0.21171 0.288695,-0.22134 0.288695,-0.77948 0,-0.55814 -0.288695,-0.76985 -0.279072,-0.22134 -0.817969,-0.22134 h -1.097041 z m 1.54933,3.10828 q 0.567766,0 0.894954,-0.27908 0.327187,-0.28869 0.327187,-0.84683 0,-1.15478 -1.222141,-1.15478 h -1.54933 v 2.28069 z" />
<path d="m 94.502725,157.90114 q -0.962316,0 -1.722546,-0.43304 -0.750607,-0.43304 -1.174026,-1.22214 -0.423419,-0.79872 -0.423419,-1.84765 0,-1.04892 0.423419,-1.83802 0.423419,-0.79872 1.174026,-1.23177 0.76023,-0.43304 1.722546,-0.43304 0.962316,0 1.712923,0.43304 0.76023,0.43305 1.183649,1.23177 0.423419,0.7891 0.423419,1.83802 0,1.04893 -0.423419,1.84765 -0.423419,0.7891 -1.183649,1.22214 -0.750607,0.43304 -1.712923,0.43304 z m 0,-0.88533 q 0.750607,0 1.279881,-0.33681 0.529274,-0.34643 0.789099,-0.93345 0.269449,-0.59663 0.269449,-1.34724 0,-0.7506 -0.269449,-1.33762 -0.259825,-0.59663 -0.789099,-0.93344 -0.529274,-0.34644 -1.279881,-0.34644 -0.750606,0 -1.27988,0.34644 -0.529274,0.33681 -0.798723,0.93344 -0.259825,0.58702 -0.259825,1.33762 0,0.75061 0.259825,1.34724 0.269449,0.58702 0.798723,0.93345 0.529274,0.33681 1.27988,0.33681 z" />
<path d="m 102.3438,155.94764 h -2.579011 l -0.644752,1.81878 h -0.962316 l 2.415409,-6.73622 h 0.96232 l 2.41541,6.73622 h -0.96231 z m -0.2887,-0.82759 -1.00081,-2.80996 -1.00081,2.80996 z" />
<path d="m 104.78071,151.0302 h 2.18446 q 1.09704,0 1.71292,0.51003 0.61589,0.50041 0.61589,1.53971 0,1.34724 -1.17403,1.89576 l 1.51084,2.79072 h -1.11629 l -1.36649,-2.62712 h -1.40498 v 2.62712 h -0.96232 z m 2.17484,3.29113 q 0.67362,0 1.02968,-0.32719 0.35605,-0.32719 0.35605,-0.9142 0,-0.58701 -0.35605,-0.90458 -0.34644,-0.32719 -1.02968,-0.32719 h -1.21252 v 2.47316 z" />
<path d="m 110.58844,151.0302 h 2.4924 q 0.98156,0 1.74179,0.40418 0.76023,0.39455 1.18365,1.1644 0.42342,0.76023 0.42342,1.79953 0,1.0393 -0.42342,1.80916 -0.42342,0.76023 -1.18365,1.1644 -0.76023,0.39455 -1.74179,0.39455 h -2.4924 z m 2.4924,5.90863 q 1.12591,0 1.73217,-0.68325 0.61588,-0.69287 0.61588,-1.85727 0,-1.1644 -0.61588,-1.84765 -0.60626,-0.69286 -1.73217,-0.69286 h -1.5782 v 5.08103 z" />
<path d="m 123.22531,155.94764 h -2.57901 l -0.64475,1.81878 h -0.96232 l 2.41542,-6.73622 h 0.96231 l 2.41542,6.73622 h -0.96232 z m -0.2887,-0.82759 -1.00081,-2.80996 -1.0008,2.80996 z" />
<path d="m 125.66223,151.0302 h 0.99118 l 3.31999,5.04254 v -5.04254 h 0.92383 v 6.73622 h -0.77948 l -3.54132,-5.32161 v 5.32161 h -0.9142 z" />
<path d="m 137.21122,151.0302 -2.3673,3.8204 v 2.91582 h -0.9142 v -2.91582 l -2.37692,-3.8204 h 1.0393 l 1.78991,2.92545 1.78991,-2.92545 z" />
<path d="m 139.21675,151.8578 h -1.96313 v -0.8276 h 4.84045 v 0.8276 h -1.96312 v 5.90862 h -0.9142 z" />
<path d="m 143.01023,151.0302 h 0.9142 v 2.7811 h 3.42585 v -2.7811 h 0.9142 v 6.73622 h -0.9142 v -3.08904 h -3.42585 v 3.08904 h -0.9142 z" />
<path d="m 149.79531,151.0302 h 0.9142 v 6.73622 h -0.9142 z" />
<path d="m 152.25749,151.0302 h 0.99118 l 3.31999,5.04254 v -5.04254 h 0.92383 v 6.73622 h -0.77948 l -3.54132,-5.32161 v 5.32161 h -0.9142 z" />
<path d="m 161.92997,157.90114 q -0.93345,0 -1.68405,-0.43304 -0.75061,-0.43304 -1.18365,-1.23176 -0.43305,-0.79873 -0.43305,-1.84765 0,-1.04893 0.44267,-1.83803 0.45229,-0.78909 1.24139,-1.22214 0.7891,-0.43304 1.79953,-0.43304 0.51003,0 1.00081,0.10586 0.5004,0.10585 0.89495,0.27907 l -0.0866,0.79872 q -0.93344,-0.35606 -1.78028,-0.35606 -0.83722,0 -1.40498,0.35606 -0.55815,0.34643 -0.83722,0.95269 -0.26945,0.59664 -0.26945,1.36649 0,1.21252 0.64476,1.94388 0.65437,0.73136 1.915,0.73136 0.26945,0 0.57739,-0.0385 0.30795,-0.0481 0.54852,-0.14435 v -1.67443 h -1.05854 v -0.82759 h 1.97275 v 3.00243 q -0.42342,0.23095 -1.02006,0.3753 -0.58701,0.13472 -1.27988,0.13472 z" />
</g>
</g>
</svg>
<style>
.logo {
width: 33vw;
}
path, rect {
fill: #000;
}
</style>
- Uses websockets for realtime updates
- Integrates with a huge number of services
- Familiar JSON configuration
- Extensible datasource system
[GitHub](https://github.com/vudash/vudash)
[Get Started](#quick-start)
================================================
FILE: docs/api/README.md
================================================
# API
Vudash exposes a very simple RESTful HTTP versioned api which can be used to perform a number of operations on a running dashboard.
## Versioning
API endpoints are versioned in their url. Current versions are:
`/api/v1`
## Authentication
### Authenticating Requests
Authentication to the api is performed by passing an `api-key` parameter as either a `header` or a `request parameter`, i.e:
```bash
curl -X GET 'http://your.dashboard.url:3300/api/v1/something/to-do?api-key=abcde12345'
```
or
```bash
curl -X GET --header 'api-key: abcde12345' 'http://your.dashboard.url:3300/api/v1/something/to-do'
```
### API Keys
By default, an api key is generated and output to the console when the dashboard loads. However, you can override this api key with the `API_KEY` environment variable, i.e:
```bash
$ API_KEY=abcde12345 vudash
```
## Endpoints
Below is a list of operations which can be peformed via the API. This list will grow over time
### PUT /api/v1/view/current
Change which dashboard viewers are seeing. This affects *all viewers* of any dashboard, so use it wisely.
Possible uses for this endpoint are if you only have a single screen but need multiple dashboards - you can set up a simple cron-job to change the dashboard every N minutes.
Another use for this is to set up some sort of IoT button (or a [hacked Amazon Dash button](https://www.npmjs.com/package/homebridge-dash)), to allow your team to view different dashboards on a single screen by pressing a button.
### PUT /api/v1/dashboards/{name}
Dynamically add a new dashboard to vudash (or replace an existing one of the same `{name}`).
This way, you can add dashboards to a running instance of vudash without redeploying.
You pass a payload containing a single key `descriptor`, which contains a dashboard descriptor (the same as you would add a dashboard normally as {name}.json)
An example payload might be:
```json
{
"descriptor": {
"name": "my-dynamic-dashboard",
"layout": {
"columns": 2,
"rows": 2
},
"datasources": {
"ds-rnd": {
"module": "../datasource-random",
"schedule": 1000,
"options": {
"method": "natural",
"options": {
"max": 100000
}
}
}
},
"widgets": [{
"position": {
"x": 0,
"y": 0,
"w": 2,
"h": 2
},
"widget": "../widget-statistic",
"datasource": "ds-rnd"
}]
}
}
```
You can then visit the new dashboard as you normally would at `http://<vudash-url>/<name>.dashboard`.
Note that dynamically added dashboards are *in memory* and do not survive server restarts.
If you are adding a new dashboard (i.e. `{name}` is unique), the api will return http status code `201`. If you are replacing an existing one (even one which exists on disk), you will recieve a `200` http status code
================================================
FILE: docs/datasources/README.md
================================================
# Datasources
## What is a Datasource
A datasource provides the mechanism for widgets to recieve the information they show.
## How to add a datasource to the dashboard
In this guide, we'll install the `value` datasource and use it in a widget.
1. Firstly, install the module required:
```
npm install --save @vudash/datasource-value
```
2. Next, configure the datasource. We'll give this datasource an id of `value-datasource`, and pass it some default options. Add the datasource to your `<dashboard-name>.json` file under datasources. Don't forget to set the update schedule.
```
{
"datasources": {
"my-datasource-id": {
"module": "@vudash/datasource-value",
"schedule": 30000,
"options": {
"value": "12345"
}
}
}
}
```
3. Add the datasource to one of your widgets, by adding a `datasource` attribute to the widget's config, with the id of the datasource (`value-datasource`), in the `<dashboard-name>.json` file, this time under widgets:
```
"widgets": [
...,
{
"position": ...,
"widget": "vudash-widget-statistic",
"datasource": "my-datasource-id",
"options": {
"description": "Some Description"
}
}
]
```
4. You're good to go! Your widget will now use the `value-datasource` to fetch its data.
## Shared datasource configuration
When you install a datasource to the dashboard, you can pass it some configuration using the `options` attribute. This can be useful because all consumers of the datasource will receive those options:
```javascript
{
"datasources": {
"datasource-id": {
"module": "some-datasource-npm-package-name",
"options": {
"number": 1,
"foo": "bar"
}
}
}
}
```
## Provided Datasources
### Benefits
Vudash Datasources are referenced using the `datasource` attribute of a widget.
This saves time for a widget developer, and means that any widget can easily fetch data from a number of different sources.
### Supported sources
| Datasource name | Source of data | Documentation |
|------------------|----------------------------------------------------------------------|------------------------------------------------------|
| value | config ```{ value: <value> }``` | [Value Datasource](#value-datasource)
| random | [chance.natural({ min: 0, max: 999})](http://chancejs.org) | [Random Datasource](#random-datasource)
| rest | http(s) using [request](http://requestjs.org) | [REST Datasource](#REST-datasource)
| google-sheets | [Google Sheets](http://drive.google.com) | [Google Sheets Datasource](#google-sheets-datasource)
### Usage in widgets
When using a widget which allows a datasource, just pass in the id of the datasource (registered in `datasource`), and any configuration you want it to have.
In `dashboard.json`
1. Add the datasource under the `datasource` section. The `options` will contain the configuration for the datasource:
```javascript
"datasource": {
"datasource-id": {
"module": "datasource-package-name",
"options": {
"url": "http://example.com/some/api",
"method": "get"
}
}
}
```
2. Tell the widget to use the datasource, by modifying your widget entry under `widgets`:
```javascript
{
"position": { ... },
"widget": "some-widget",
"datasource": "datasource-id",
"options": {
...
}
}
```
Configuration is validated when the datasources are registered by the dashboard, if a datasource supports it.
### Value datasource
The Vudash Value Datasource returns hardcoded values.
#### Basic Config
Simply specify the value you want returned.
```javascript
{
"module": "@vudash/datasource-value",
"options": {
"value": 2
}
}
```
Returns the number 2.
#### Arrays and Objects
Value Datasource can return anything you can provide in JSON
Simply specify the value you want returned.
```javascript
{
"module": "@vudash/datasource-value",
"options": {
"value": { "x": [{ "y": [1,2,3,4,5] }, { "z": false }] }
}
}
```
Will return the object specified by 'value'.
### REST datasource
The Vudash REST Datasource allows fetching data from external APIs.
#### Basic Config
The default method is GET. Simply specify an URL.
```javascript
{
"module": "@vudash/datasource-rest",
"options": {
"url": "http://example.com/some/api"
}
}
```
#### POSTing data
Say you wanted to POST to the endpoint `/v1/api` at `https://example.org` on port 3333.
Furthermore, you want to send JSON request data as specified in "body" below.
```javascript
{
"module": "@vudash/datasource-rest",
"options": {
"method": "post",
"url": "https://example.org:3333/v1/api",
"body": {
"foo": "bar",
"one": 2,
"three": false
}
}
}
```
#### Query Parameters
Say you wanted to POST to the endpoint `/v1/api` at `https://example.org` on port 3333.
Furthermore, you want to send JSON request data as specified in "query" below.
```javascript
{
"module": "@vudash/datasource-rest",
"options": {
"method": "get", // optional
"url": "https://example.net/v1/api",
"query": {
"param1":"foo",
"param2":"bar"
}
}
}
```
#### Parsing data
Vudash automatically parses returned JSON which means it can be easily formatted. To determine what is returned, you can use a [transformer](/#/transformers) to modify the json response before the widget receives it. Consult the documentation for information on the transformers available to you and how to use them.
## Troubleshooting SSL
You might encounter an error when trying to fetch data from SSL protected servers, such as:
```bash
Error in widget datasource-rest (461305c2) { RequestError
at ClientRequest.req.once.err (/home/aj/Projects/vudash-core/packages/core/node_modules/got/index.js:73:21)
at Object.onceWrapper (events.js:291:19)
at emitOne (events.js:96:13)
at ClientRequest.emit (events.js:189:7)
at TLSSocket.socketErrorListener (_http_client.js:358:9)
at emitOne (events.js:96:13)
at TLSSocket.emit (events.js:189:7)
at emitErrorNT (net.js:1280:8)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
message: 'unable to verify the first certificate',
host: 'ssl.example.com',
hostname: 'ssl.example.com',
method: 'GET',
path: '/health' }
```
This means that you don't have the correct root CA certificates for node to connect to the endpoint.
This is easily fixed, if you are using node > 7.3 however:
1. Find out who the Root CA for the domain you are trying to connect to, using [an ssl analysis tool](https://sslanalyzer.comodoca.com/)
1. Download the root CA's pem file and drop it in your project folder.
1. When you run vudash, pass an environment variable with the path to your root ca's certificate, i.e: `NODE_EXTRA_CA_CERTS='./path/to/root-cas.pem' vudash`
More details on this [can be found here](https://git.daplie.com/Daplie/node-ssl-root-cas)
### Random datasource
The Vudash Random Datasource returns hardcoded values.
#### Basic Config
Vudash Random Datasource uses ChanceJS underneath. That means you can it bare, to generate a random integer:
```javascript
{
"source": "random"
}
```
#### Custom Chance Methods
or you can define the method used to generate your random data:
```javascript
{
"module": "@vudash/datasource-random",
"options": {
"method": "string"
}
}
```
#### Chance Methods with Custom Parameters
or you can define the method AND the parameters passed to the method. In this example, we use `chance.n()` to generate an array of values.
You need to pass parameters to `n()`, the method used to generate the values, the number of values to generarate, and the third parameter is the parameters passed to `chance.integer()`. Confused?
The code used below is the equivalent of calling [chance.n(chance.integer, 12, {min: 15, max: 32})](http://chancejs.com/#n).
```javascript
{
"module": "@vudash/datasource-random",
"options": {
"method": "n",
"options": [
"integer",
12,
[{
min: 15,
max: 32
}]
]
}
}
```
Generates an array of 12 integers between 15 and 32.
================================================
FILE: docs/developers/README.md
================================================
# Developing
Creating widgets and datasources is designed to be quick and painless. They are delivered as simple npm modules, and follow basic node patterns in order to get you up to speed quickly.
## Developing Widgets
A widget is a visible indicator of some sort of statistic or other monitor, which emits new data using websockets, and updates its display in the dashboard based on the information given in this data.
A widget is packaged as a node module, but a node module can simply be a folder with a `package.json` file.
A widget is simply a node module, and really only needs a couple of files.
### package.json
```javascript
{
"name": "vudash-widget-example",
"main": "widget.js",
"vudash": {
"component": "somefile.html"
}
}
```
The `main` js file above should reference your main module class, in this example we call it `widget.js`
The `vudash.component` is a single file [SvelteJS](http://svelte.technology) component that is an all-in-one (html, css, js),
view-component with an immutable-data-tree based state model.
### Writing the server-side component
```javascript
'use strict'
const moment = require('moment')
class TimeWidget {
constructor (options, emitter) {
this.options = options // options is the configuration passed under "options" in your dashboard.json
this.emitter = emitter // emitter is only useful if you want to emit events yourself (see below)
}
/**
* This method is called by the datasource when it gets new data.
**/
update (data) {
const now = moment()
return {
time: now.format('HH:mm:ss'),
date: now.format('MMMM Do YYYY')
} // just return the data you want to display on the dashboard
}
}
exports.register = function (options, emitter) {
return new TimeWidget(options, emitter)
}
```
* The first parameter to register is the widget configuration given in the `dashboard.json` file
* The second parameter to register is the optional parameter `emitter` which can be used to emit events (at any time) to the dashboard. See `Events` below for more information about this.
### Writing the client side component
Client side components are defined using [svelte](https://svelte.technology/) which allows you to build framework-independent client side components with ease.
Create your svelte component as a single html file, and reference it as a module-absolute path named `vudash.component` in your widget's package.json.
In order to ease development, [a 'harness' exists](https://svelte.technology/repl?version=1.40.1&gist=0ef39c92a284251d65d1e29c63cd1ca8) for rapidly building Vudash widgets using the Svelte REPL. Edit the `widget.html` file there and then simply copy-paste it into the file you reference under `vudash.component` in `package.json`.
#### Example of a component
package.json
```javascript
{
"name": "vudash-widget-health",
"main": "widget.js",
"vudash": {
"component": "./component.html"
}
}
```
component.html
```html
<h1 class="vudash-hello">{{ greeting }}</h1>
<style>
.vudash-hello {
text-align: right;
}
</style>
<script>
export default {
data () {
return {
greeting: 'hello'
}
},
methods: {
/**
* This is the really important bit.
* This update method is called whenever the widget emits data.
*
* data: the actual update data given by the datasource
* meta: metadata about the update. This currently contains 'updated' which is the data fetch time.
* history: the last x events, where `x` is defined by the 'history' option in the widget's config.
**/
update ({ data, meta, history }) {
this.set(data)
}
}
}
</script>
```
See the [Svelte Documentation](https://svelte.technology/guide) for information on how to build svelte components.
#### Third party dependencies
All components and their dependencies are processed by `rollup` and bundled into a browser-friendly script.
You can use third party dependencies in your component by importing them using es6 import syntax. First, install the module as a dependency of your widget module:
```bash
npm install thing-maker
```
Then, import it to your component:
```html
<span>Hello {{ thing }}</span>
<script>
import { world } from 'thing-maker'
export default {
data () {
return {
thing: world()
}
}
}
</script>
```
It doesn't matter if the module you want to use isn't an es6 module (i.e. doesn't export a default object), because the `rollup` plugin [rollup-plugin-commonjs](https://www.npmjs.com/package/rollup-plugin-commonjs) is used which can convert traditional node modules into es6 modules for you.
#### Images
You can bundle SVG images as dependencies in your widgets too - the [rollup-plugin-svg](https://www.npmjs.com/package/rollup-plugin-svg) plugin is also included:
<img alt="logo" src="{{ logo }}" />
<script>
import { logo } from './logo.svg'
export default {
data () {
return {
logo
}
}
}
</script>
You can [read more about Rollup.js](https://rollupjs.org/) in order to better understand how to optimise your component's client side code.
### Using datasources
Datasources are how most widgets get data. Datasources provide an abstraction for fetching data from a multitude of sources, and deliver it as a single blob of json to the widgets. As a developer you should strongly consider providing datasource support in your widget.
#### Benefits
* Consumer chooses where your widget gets its data
* Don't need to implement any data fetching code yourself
* Focus your widget on displaying data, not fetching it
* Shared configuration for consumers, datasources are configured globally, and/or on a widget level.
#### How to
1. When the datasource emits data, it will then be sent to your widget's serverside `update` method where you can make further modifications to it before returning it.
```javascript
update(value) {
// do something with the value, like add an emoji!
return `🌟 ${value}`
}
```
1. The dashboard will then re-emit your data with some metadata, and a history, if configured. It will call your Svelte component's `update` method with the data. You can then add it to your Svelte component's data model for use in the template.
```javascript
update ({ data, meta, history }) {
this.set({ someValue: data.myValue })
}
```
1. Then simply use it in your markup
```html
<h1>{{ someValue }}</h1>
```
### Writing a component without a datasource
Your component doesn't have to use a datasource. It can simply fetch data by itself. This can be done any way you like, but an approach which works as an example is the `vudash-widget-health` widget:
```javascript
'use strict'
class HealthWidget {
constructor (options, emitter) {
this.emitter = emitter
this.on = false
this.timer = setInterval(function () {
this.run()
}.bind(this), options.schedule || 1000)
this.run()
}
run () {
this.on = !this.on
this.emitter.emit('update', { on: this.on })
}
destroy () {
clearInterval(this.timer)
}
}
exports.register = function (options, emitter) {
return new HealthWidget(options, emitter)
}
// Validation is optional
exports.validation = Joi.object({
'some-option': Joi.string().required()
})
```
Important things to note here are:
* We emit our own data using `emitter`. Emitting an event called `update` with your data will result in the widget's view receving the `data` you emit into its `update({ data, meta, history })` method.
* We have to call our `run()` method somehow to update the data. This is done using a `setInterval`
* In case the user doesn't configure a schedule for the widget in its `options`, we default to 1000ms.
* We provide a `destroy()` hook to destroy our timer. This is useful to avoid memory leaks, unecessary fetching, and is especically useful for unit-testing.
* We export an optional `validation` schema, which is a Joi schema. It can include default values, too! If this is not exported, it is not called, and options will be passed to your register method verbatim.
## Developing Datasources
TBC - for now, have a look at the source code in `vudash/packages/datasource-*`
## Developing Transformers
Data can come from datasources in a variety of different formats, not all of which can directly translate to something usable by a widget. [Transformers](/#/transformers) are designed for a dashboard consumer to manipulate data fetched using datasources (or directly inside widgets) before it is emitted to the dashboard.
The format of a transformer is relatively simple. Here's a very simple transformer:
```javascript
'use strict'
class AddingTransformer {
constructor (numberToAdd) {
this.numberToAdd = numberToAdd
}
transform (data) {
return data + this.numberToAdd
}
}
module.exports = AddingTransformer
```
A transformer takes the configuration provided by the dashboard (//widgets[]/transformations) as its only constructor argument.
When new data is fetched by a datasource or a widget and sent to the dashboard for consumption by listening widgets, it is first transformed by the list of transformers configured in the widget's configuration by calling each transformer's `transform` method. The argument passed to transform is the result of the previous transformer's transform method.
You might configure the transformer as part of the following dashboard:
```javascript
{
"datasources": {
"my-datasource-hundred": {
"module": "@vudash/datasource-value",
"options": {
"value": 100
}
}
}
},
{
"datasource": "my-datasource-hundred",
"transformations": [
{
"transformer": "./adding-transformer.js",
"options": 100
}
],
"options": {
"description": "Shows two-hundred"
},
"widget": "vudash-widget-statistic"
}
```
The resulting dashboard widget would show the value `200` as the value `100` is transformed by adding `100` to it.
## Dashboard Events
Events can be emitted using the event emitter which is passed into the register method. These events will cause dashboard-wide actions to happen.
```
emit('plugin', 'audio:play', {data: data})
```
The current list of events that can be triggered are:
| Event | Data | Description |
| ------------- |------------------| --------------------------------------------------------------------|
| audio:play | `{ data: data }` | Plays an audio clip (once). `data` is a data-uri of the audio file. |
## Working on the Vudash project
Vudash uses
* ES6
* Svelte
* StandardJS
Contributions are always welcome, please open a PR.
================================================
FILE: docs/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vudash</title>
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/themes/vue.css">
<style>
@import url('https://fonts.googleapis.com/css?family=Gruppo');
</style>
</head>
<body>
<nav>
<a href="#/">Vudash</a>
<a href="#/widgets/">Widgets</a>
<a href="#/datasources/">Datasources</a>
<a href="#/transformers/">Transformers</a>
<a href="#/developers/">Developers</a>
<a href="#/api/">API</a>
</nav>
<div id="app">
</div>
</body>
<script>
window.$docsify = {
coverpage: true,
ga: 'UA-117504415-1',
name: 'vudash',
search: {
noData: {
'/': 'No results!'
},
paths: 'auto',
placeholder: {
'/': 'Search'
}
}
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/ga.min.js"></script>
</html>
================================================
FILE: docs/transformers/README.md
================================================
# Transformers
Transformers allow you to retrieve information from a data source or widget, and modify it before it is sent to the dashboard.
## How to install a transformer
Transformers, like widgets and datasources, are just node modules. Install the module required:
```bash
npm install --save @vudash/transformer-map
```
## Provided Transformers
We provide a selection of transformers, or you can [write your own](/#/developers), very simply.
### Map Transformer (@vudash/transformer-map)
Maps json data from one structure to another.
Selection is done using [Hoek.reach](https://www.npmjs.com/package/hoek) via [reorient](https://www.npmjs.com/package/reorient), so null values, function traversal etc is automatically handled, and won't throw errors.
You should consult the [reorient](https://www.npmjs.com/package/reorient) documentation for advanced mapping features such as default values, but in a pinch:
Say that your desired API returns the following payload in JSON:
```javascript
{
"one": {
"two": {
"three": "abcde"
}
}
}
```
Lets say we wanted the value of "three" buried down in the middle there. It's easy:
```javascript
{
"position": {
...
},
"datasource": "ds-rest",
"transformations": [
{
"transformer": "@vudash/transformer-map",
"options": {
"value": "one.two.three"
}
}
],
"options": {
"description": "Value of three"
},
"widget": "vudash-widget-statistic"
}
```
And if you actually want the contents of two? (As an object of course):
```javascript
{
"position": {
...
},
"datasource": "ds-rest",
"transformations": [
{
"transformer": "@vudash/transformer-map",
"options": {
"value": "one.two"
}
}
],
"options": {
"description": "Value of two"
},
"widget": "vudash-widget-statistic"
}
```
### JQ Transformer (@vudash/transformer-jq)
The JQ transformer uses the module [jq.node](https://www.npmjs.com/package/jq.node), which is an 'improved, faster' version of jq specifically for node.
Usage is a matter of compiling a selector to modify the data as you please
Supposing your data looks like this:
```json
{
"a": {
"big": {
"json": {
"jeff": {
"email": "jeff@example.net"
},
"joe": {
"phone": "+447721981546"
},
"emma": {
"email": "emma@example.com"
},
"grayson": {
"email": "wailo@example.net"
}
}
}
}
}
```
You could use the following configuration to group the above users by email domain.
```javascript
{
"position": {
...
},
"datasource": "ds-rest",
"transformations": [
{
"transformer": "@vudash/transformer-jq",
"options": {
"value": "filter(has('email')) | groupBy(flow(get('email'), split('@'), get(1)))"
}
}
],
"options": {
"description": "People grouped by email domain"
},
"widget": "vudash-widget-statistic"
}
```
================================================
FILE: docs/widgets/README.md
================================================
# Predefined widgets
Vudash has a number of widgets which are available on npm, these are in the `packages/` directory of the monorepo, and also available on npm.
You will notice that widget definitions `position` and `datasource` attributes. These refer to the position of the widget on the display, and how the widget gets its data, and these are documented in [Datasources](/#/datasource) and the [Main Readme](/#/) respectively.
## Chart Widget
Shows Bar, Line, Chart, and Donut graphs for data series.
### Screenshot
Line Chart

### Configuration
The chart widget has a number of configuration options:
| Option | Default | Allowed Values | Description |
| --- | --- |
| `description` | `<empty string>` | any string | Widget description, shown at the bottom of the widget
| `type` | `line` | `line, bar, pie, donut` | Graph type. Lowercased version of [chartist graph types](https://gionkunz.github.io/chartist-js/examples.html). Also applies some sensible styling to each graph type to make it fit with a Vudash dashboard. |
| `labels` | `[]` | an array of label names | Labels which run along the X axis of a chart. Each label relates to its corresponding number in the data provided to the chart. |
#### Configuration example
You can configure any status page which uses [Atlassian StatusPage](https://www.atlassian.com/software/statuspage) easily:
```javascript
{
"position": { ... },
"widget": "@vudash/widget-chart",
"datasource": { ... },
"options": {
"type": "pie",
"description": "My Pie Chart",
"labels": ["Apples", "Pears", "Peaches", "Lemons", "Oranges"]
}
}
```
## CI Widget
Connects to CI Providers and displays build results.
Currently supports [CircleCI](http://www.circleci.com) and [TravisCI](http://www.travis-ci.org)
### Screenshot
Building:

Failing, and Passing:

### Configuration
Add to a [Vudash](https://www.npmjs.com/package/vudash) dashboard with the following configuration:
#### Simple Configuration
The simplest configuration is very straightforward
```javascript
{ "position": { ... },
"widget": "vudash-widget-ci",
"datasource": "some-data-source-id",
"options": {
"provider": "circleci",
"repo": "some-repo",
"user": "some-user"
}
}
```
#### Display
You can turn the display of the repository owner on and off, which might be useful if all your repositories belong to a single organisation:
```javascript
{ "position": { ... },
"widget": "vudash-widget-ci",
"datasource": "some-data-source-id",
"options": {
"hideOwner": true
...
}
}
```
#### Build Noises
```javascript
{
"widget": "vudash-widget-ci",
"options": {
"provider": "travis", // CI Provider (travis or circleci)
"user": "your-user", // username, mandatory
"repo": "your-repo", // repository name, mandatory
"branch": "your-branch" // branch to monitor, optional.
"schedule": 60000 // Update frequency in MS (optional),
"sounds": { // Sound to play on build state changes (optional)
"passed": "/some/local/path/sound.ogg",
"failed": "data:audio/ogg;base64, ...",
"unknown": "data:audio/ogg;base64, ..."
},
"options": {
"auth": "xxx" // circleci auth token, only required for circleci
}
}
}
```
Where `your-user` is your github organisation or user name, and `your-repo` is your build/repository name.
* The travis plugin currently only deals with public repositories (i.e. travis-ci.org, not .com)
## Gauge Widget
Shows a VU-Meter like Gauge which represents numerical figures like percentages
### Screenshot

### Configuration
Simply include in your dashboard, and configure as required:
```javascript
{
"widget": "vudash-widget-statistic",
"datasource": "some-data-source-id",
"options": {
"description": "Gauge", // Optional. Description shown below statistic,
"maximum": 3983 // Required, the maximum value the gauge can ever reach
}
}
```
## Progress Widget
Similar to VU Meter, but with a linear progress bar
### Screenshot

### Configuration
The only configuration for the progress widget is the description.
```javascript
{
"widget": "vudash-widget-progress"
"datasource": "some-data-source-id",
"options": {
"description": "Stuff", // Optional. Default "Progress" Description shown below statistic
}
}
```
The datasource connected to the progress widget should ideally return numbers between 1 and 100, anything over 100 will be represented as 100% anyway.
## Statistics Widget
Shows a statistic, which can be a number, a word, or anything else representable on screen.
Optionally can draw a graph of the previous results behind the main one.
### Screenshot

### Configuration
Simply include in your dashboard, and configure as required:
```javascript
{
"widget": "vudash-widget-statistic",
"datasource": "some-data-source-id",
"options": {
"description": "Visitor Count", // Optional. Default "Statistics" Description shown below statistic,
"format": "%s", // Optional. Default %s. Format the incoming data (using sprintf-js),
"font-ratio": 4 // Optional. Default 4. Scaling ratio for main statistic (for longer text, increase this number),
"colour": "#86797d", // Optional. Defaults to a random colour from a pre-selected "pretty" list. Colour for line / fill-area of graph, if shown.
"historyView": "chart" // Optional, defaults to "chart". How historical figures are represented.
}
}
```
Note that `datasource` tells the widget how to get data, and is using a datasource, which is documented in the [Datasources documentation](/#/datasources)
#### History Views
There are two ways to represent previous values that the widget has received:
* `chart`: Displays a line-graph of the previous historical values which floats behind the widget's content.
* `ticker`: Shows a stock-market type ticker, in either red or green depending on the direction the value is heading. Shows difference from previous value, and the same difference represented as a percentage.
#### Graphs
This widget will graph data which is passed in as an array.
This means that if your data-source resolves an array of numbers as data, the last number in the array
will be shown as the statistic value, and a line graph will be drawn behind the widget using the remaining numbers.
For example
`[1,2,3,4,5,6,7]` will result in a widget value of 7, and a graph of 1-6 behind it.
## Status Widget
Shows the status of an external service like github, or any API which uses Atlassian StatusPage
### Screenshot

### Configuration
Currently this widget has two integrations.
#### Atlassian Statuspage
You can configure any status page which uses [Atlassian StatusPage](https://www.atlassian.com/software/statuspage) easily:
```javascript
{
"position": { ... },
"widget": "vudash-widget-status",
"datasource": "some-data-source-id",
"options": {
"schedule": 300000,
"type": "statuspageio",
"config": {
"url": "https://status.newrelic.com/", // URL to the status page
"components": [ // List the names of components you want to monitor the status of
"APM",
"Data Collection",
"Alerts"
]
}
}
}
```
#### Github
Github status page monitoring is no-configuration. It will tell you when it is up, down, or otherwise.
```javascript
{
"position": { ... },
"datasource": "some-data-source-id",
"widget": "vudash-widget-status",
"options": {
"schedule": 300000,
"type": "github"
}
}
```
## Time Widget
Simply shows the time, and has optional audiable alarams
### Screenshot

### Configuration
Simply include in your dashboard:
```javascript
{
"widget": "vudash-widget-time",
"options": { ... }
}
```
#### Timezone support
The timezone can be set via configuration. The list of allowed timezones is that of the `moment-timezone` library.
```javascript
"options": {
"timezone": "Europe/London"
}
```
#### Alarms
This widget can play sounds! Simply pass 'alarms' into your configuration:
```javascript
"options": {
"alarms": [
{
"expression": "5 * * * * *",
"actions": [
{
"action": "sound",
"options": {
"data": "data:audio/ogg;base64, ..."
}
}
]
}
]
}
```
`expression` is a cron expression which determines when the sound will be played.
`actions` is the action to perform when the alarm is triggered. Supported actions are listed below:
#### Actions
Action: `sound`
Options: `data` is a data-uri which contains the clip of audio to be played. You can use a tool like: `http://dopiaza.org/tools/datauri/index.php` to convert your audio clips to data-uris.
## Health Widget
A simple widget with a beating heart, to let you know that the dashboard is alive.
### Screenshot

### Configuration
There is no configuration for this widget. It runs every second, and the heart will change shape.
If the heart stops... your vudash client's websocket has become disconnected from the backend, and your data is out of date.
================================================
FILE: lerna.json
================================================
{
"lerna": "2.0.0-beta.36",
"packages": [
"packages/*"
],
"version": "9.9.0"
}
================================================
FILE: package.json
================================================
{
"scripts": {
"lint": "lerna run lint",
"start": "lerna run start --scope vudash",
"heroku-postbuild": "./node_modules/.bin/lerna bootstrap",
"docs:preview": "docsify serve docs",
"postinstall": "lerna bootstrap --hoist"
},
"devDependencies": {
"chance": "^1.0.4",
"cheerio": "^0.22.0",
"code": "^4.0.0",
"docsify": "^4.6.10",
"docsify-cli": "^4.2.1",
"glob": "^7.0.6",
"lerna": "2.0.0-beta.36",
"marked": "^0.3.19",
"mocha": "^4.0.1",
"nock": "^9.0.2",
"nodemon": "^1.9.2",
"prismjs": "^1.14.0",
"sinon": "^1.17.4",
"sinon-as-promised": "^4.0.2",
"standard": "^11.0.1"
},
"engines": {
"node": ">=9.x"
},
"standard": {
"globals": [
"describe",
"context",
"it",
"before",
"after",
"beforeEach",
"afterEach"
]
},
"dependencies": {
"app-module-path": "^2.2.0",
"bluebird": "^3.5.2",
"boom": "^6.0.0",
"browser-sync": "^2.24.7",
"buble": "^0.19.3",
"catbox-memory": "^3.1.2",
"chartist": "^0.11.0",
"circleci": "^0.3.3",
"cron": "^1.4.1",
"export-dir": "^0.1.2",
"fit-text": "^2.0.1",
"fs-extra": "^0.30.0",
"handlebars": "^4.0.12",
"hapi": "^16.6.3",
"hapi-api-secret-key": "^1.1.0",
"inert": "^4.2.1",
"izitoast": "^1.4.0",
"joi": "^13.7.0",
"jq.node": "^2.1.1",
"json-to-css": "^0.1.0",
"moment": "^2.22.2",
"moment-timezone": "^0.5.21",
"npm": "^5.10.0",
"npm-programmatic": "0.0.8",
"ora": "^1.4.0",
"reorient": "^2.1.0",
"rollup": "^0.43.1",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-postcss": "^0.4.3",
"rollup-plugin-svelte": "^4.3.2",
"rollup-plugin-svg": "^1.0.1",
"rollup-plugin-uglify-es": "0.0.1",
"rollup-plugin-virtual": "^1.0.1",
"slash": "^1.0.0",
"socket.io": "^2.1.1",
"spreadsheet-to-json": "^1.3.1",
"svelte": "^1.64.1",
"travis-ci": "^2.2.0",
"unhandled-rejection": "^1.0.0",
"vision": "^4.1.1"
}
}
================================================
FILE: packages/core/README.md
================================================
[](https://gitter.im/vudash/vudash-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/vudash/vudash) [](http://standardjs.com/)
# Vudash
A dashboard, like dashing, but written in NodeJS.
## Documentation
Documentation has moved [here](http://vudash.github.io/vudash/)
## What does it look like?


## Demo
http://vudash.herokuapp.com/demo.dashboard
## Features
* will happily run on a free heroku instance
* es6
* all cross-origin requests done server side
* websockets rather than polling
* websocket fallback to long-poll when sockets aren't available
* Custom widgets
* Custom dashboards
* Simple row-by-row layout
* Dashboard arrangement is simply the config order (see below)
* Super simple widget structure
================================================
FILE: packages/core/app.js
================================================
'use strict'
const { register, start, stop } = require('./src/server')
let server
register()
.then(registered => {
server = registered
start(server)
})
process.on('SIGUSR2', () => {
stop(server)
.then(() => {
process.exit()
})
})
================================================
FILE: packages/core/bin/vudash.js
================================================
#!/usr/bin/env node
const create = require('../src/cli/create')
const help = require('../src/cli/help')
const logo = require('../src/cli/logo')
const [ , , arg ] = process.argv
logo.run()
if (!arg) {
require('../app')
}
if (arg === 'create') {
create.run()
}
if (['help', '--help'].includes(arg)) {
help.run()
}
================================================
FILE: packages/core/dashboards/simple.json
================================================
{
"name": "simple-dashboard",
"layout": {
"columns": 5,
"rows": 4
},
"datasources": {
"ds-rnd": {
"module": "../datasource-random",
"schedule": 1000,
"options": {
"method": "string"
}
}
},
"widgets": [
{
"position": {"x": 3, "y": 0, "w": 2, "h": 1},
"widget": "../widget-statistic",
"datasource": "ds-rnd"
}
]
}
================================================
FILE: packages/core/dashboards/template.json
================================================
{
"name": "simple-dashboard",
"layout": {
"columns": 5,
"rows": 4
},
"widgets": [
{ "position": { "x": 1, "y": 0, "w": 3, "h":1 }, "widget": "vudash-widget-time" }
]
}
================================================
FILE: packages/core/package.json
================================================
{
"name": "vudash",
"version": "9.9.0",
"keywords": [
"vudash",
"dashboard",
"dashing",
"analytics",
"monitoring",
"websockets",
"geckoboard",
"widget",
"stats",
"dash",
"dashing-js",
"statistics",
"big screen",
"display",
"home automation",
"automation",
"ha"
],
"repository": {
"type": "git",
"url": "https://github.com/vudash/vudash"
},
"description": "Easy to use, flexible dashboard software for monitoring, analytics, and more.",
"main": "app.js",
"scripts": {
"lint": "../../node_modules/.bin/standard",
"watch": "BROWSER_SYNC=true ../../node_modules/.bin/nodemon -e html,js .",
"start": "./bin/vudash.js",
"test": "PORT=3418 NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js",
"link": "npm link"
},
"bin": {
"vudash": "bin/vudash.js"
},
"engines": {
"node": ">=9.x"
},
"author": "Antony Jones",
"license": "MIT",
"dependencies": {
"app-module-path": "^2.2.0",
"bluebird": "^3.5.0",
"boom": "^6.0.0",
"browser-sync": "^2.12.9",
"buble": "^0.19.3",
"catbox-memory": "^3.0.0",
"chalk": "^1.1.3",
"figlet": "^1.2.0",
"find-root": "^1.1.0",
"fs-extra": "^0.30.0",
"handlebars": "^4.0.10",
"hapi": "^16.6.2",
"hapi-api-secret-key": "^1.1.0",
"hoek": "^4.1.1",
"inert": "^4.2.0",
"izitoast": "^1.2.0",
"joi": "^10.5.2",
"json-to-css": "^0.1.0",
"lodash": "^4.16.4",
"npm": "^5.0.4",
"npm-programmatic": "0.0.8",
"ora": "^1.3.0",
"require-directory": "^2.1.1",
"rollup": "^0.43.0",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-postcss": "^0.4.3",
"rollup-plugin-svelte": "^4.1.0",
"rollup-plugin-svg": "^1.0.1",
"rollup-plugin-uglify-es": "0.0.1",
"rollup-plugin-virtual": "^1.0.1",
"slash": "^1.0.0",
"socket.io": "^2.0.1",
"svelte": "^1.60.2",
"unhandled-rejection": "^1.0.0",
"vision": "^4.1.1"
},
"standard": {
"globals": [
"describe",
"context",
"it",
"before",
"after",
"beforeEach",
"afterEach"
]
}
}
================================================
FILE: packages/core/src/cli/create.js
================================================
'use strict'
const npm = require('npm-programmatic')
const ora = require('ora')
const Path = require('path')
const fs = require('fs-extra')
const { green, yellow } = require('chalk')
const dockerFileContents = `
FROM node:10-alpine
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 3300
ENV SERVER_URL http://localhost:3300
CMD npm start
`
exports.run = function () {
const dashboard = require('../../dashboards/template.json')
const cwd = process.cwd()
const spinner = ora().start('Creating dashboard layout')
const configFile = Path.join(cwd, 'dashboards', 'default.json')
const dockerFile = Path.join(cwd, 'Dockerfile')
const packageJson = Path.join(cwd, 'package.json')
const dashboardsDir = Path.join(cwd, 'dashboards')
fs.ensureDirSync(dashboardsDir)
fs.writeJsonSync(packageJson, {
name: 'my-vudash-dashboard',
main: 'vudash',
scripts: { start: 'vudash' },
'engines': {
'node': '>=9.x'
}
})
fs.writeJsonSync(configFile, dashboard)
fs.outputFileSync(dockerFile, dockerFileContents)
spinner.succeed('Created dashboard layout')
spinner.start('Installing dependencies. This could take a minute or two...')
npm.install([
'vudash',
'vudash-widget-time'
], {
cwd,
save: true
})
.then(() => {
spinner.succeed('Installed dependencies.')
console.log(
green(
'Created sample dashboard. Run "vudash" or "npm start" to view'
)
)
console.log(
yellow(
'Dockerfile written. Use `docker build -t my-dashboard-name .` to build'
)
)
})
.catch(e => {
spinner.fail('Failed to install some dependencies.')
console.log(e)
})
}
================================================
FILE: packages/core/src/cli/help.js
================================================
'use strict'
const { bold } = require('chalk')
exports.run = function () {
console.log('Usage: vudash [action]')
console.log('with no action, runs the dashboard configured in the current working directory.')
console.log('\nactions:')
console.log('\n', bold('create'), 'Create a new dashboard')
}
================================================
FILE: packages/core/src/cli/logo.js
================================================
'use strict'
const { textSync } = require('figlet')
const { yellow, blue } = require('chalk')
const { version } = require('../../package.json')
exports.run = function () {
console.log(
yellow(
textSync('vudash', {
font: 'Slant'
})
),
blue(
`v${version}`
)
)
}
================================================
FILE: packages/core/src/config-validator/config-validator.spec.js
================================================
'use strict'
const { expect } = require('code')
const { validate } = require('.')
const Joi = require('joi')
describe('config-validator', () => {
it('returns validated values', () => {
const result = validate('some-name', Joi.string().required(), 'hello')
expect(result).to.equal('hello')
})
it('throws validation errors', () => {
expect(() => {
validate('some-name', Joi.number().required(), 'hello')
}).to.throw()
})
it('with no json', () => {
const result = validate('some-name', {}, undefined)
expect(result).to.equal({})
})
it('with no rules', () => {
const result = validate('some-name', null, 'hello')
expect(result).to.equal('hello')
})
it('with empty rules', () => {
const result = validate('some-name', {}, 'hello')
expect(result).to.equal('hello')
})
it('with options', () => {
const result = validate(
'some-name',
Joi.object({ a: Joi.string() }),
{ a: 'x', b: 'y' },
{ allowUnknown: true }
)
expect(result).to.equal({a: 'x', b: 'y'})
})
})
================================================
FILE: packages/core/src/config-validator/index.js
================================================
'use strict'
const Joi = require('joi')
const { ConfigurationError } = require('../errors')
exports.validate = function (name, rules = {}, json = {}, options = {}) {
if (!rules || (typeof rules === 'object' && !Object.keys(rules).length)) {
return json
}
const { error, value } = Joi.validate(json, rules, options)
if (error) {
throw new ConfigurationError(
`Could not register ${name} due to invalid configuration: ${error}`
)
}
return value
}
================================================
FILE: packages/core/src/dashboard/bundler/index.js
================================================
'use strict'
const base = `
import iziToast from 'izitoast'
import 'izitoast/dist/css/iziToast.css'
const VUDASH = window.VUDASH
const socket = io(VUDASH.config.serverUrl)
socket.on('error', function (e) {
iziToast.show({
title: 'Socket Error',
theme: 'dark',
color: 'red',
message: e.message,
timeout: 5000,
onOpen: function () {
console.error(e)
}
})
})
socket.on('disconnect', function () {
iziToast.show({
id: 'disconnect',
title: 'Socket Disconnected',
theme: 'light',
color: 'red',
message: 'Will reload soon to restore connection...',
timeout: ${process.env.DISCONNECT_RELOAD_TIMEOUT} || 30000,
onClosed: function () {
window.location.reload()
}
})
})
socket.on('audio:play', function (data) {
VUDASH.player.play(data.data)
})
socket.on('view:current', function (data) {
window.location.pathname = '/' + data.dashboard + '.dashboard'
})
`
exports.build = function (widgets) {
const model = widgets.reduce((curr, { name, markup, css, js, componentPath }) => {
curr.imports.push(`import ${name} from '${componentPath}'`)
curr.containers.push(markup)
curr.css.push(css)
curr.events.push(js)
return curr
}, { imports: [], containers: [], events: [], css: [] })
const imports = [ ...new Set(model.imports) ]
const js = `
'use strict'
${imports.join('\n')}
${base}
${model.events.join('\n')}
`
const html = `
<style>
${model.css.join('\n')}
</style>
${model.containers.join('\n')}
`
return { js, html }
}
================================================
FILE: packages/core/src/dashboard/compiler/compiler.spec.js
================================================
'use strict'
const compiler = require('.')
const { stub } = require('sinon')
const rollup = require('rollup')
const { expect } = require('code')
describe('dashboard/compiler', () => {
context('Bundle', () => {
const compiled = 'abc123'
let js
before(async () => {
stub(rollup, 'rollup')
const bundleStub = { generate: stub().returns(compiled) }
rollup.rollup.resolves(bundleStub)
js = await compiler.compile('zzz')
})
after(() => {
rollup.rollup.restore()
})
it('Returns compiled js', () => {
expect(js).to.exist().and.to.equal(compiled)
})
})
})
================================================
FILE: packages/core/src/dashboard/compiler/configuration-builder/configuration-builder.js
================================================
'use strict'
const svelte = require('rollup-plugin-svelte')
const resolve = require('rollup-plugin-node-resolve')
const commonjs = require('rollup-plugin-commonjs')
const virtual = require('rollup-plugin-virtual')
const css = require('rollup-plugin-postcss')
const svg = require('rollup-plugin-svg')
const buble = require('rollup-plugin-buble')
const uglify = require('rollup-plugin-uglify-es')
exports.build = function (source) {
const inputConfig = {
entry: '__input__',
plugins: [
svg(),
virtual({
'__input__': source
}),
commonjs(),
resolve({
customResolveOptions: {
moduleDirectory: 'node_modules'
}
}),
css(),
svelte(),
buble(),
uglify()
]
}
const outputConfig = {
format: 'iife',
file: 'bundle.js'
}
return { inputConfig, outputConfig }
}
================================================
FILE: packages/core/src/dashboard/compiler/configuration-builder/configuration-builder.spec.js
================================================
'use strict'
const builder = require('.')
const { reach } = require('hoek')
const { expect } = require('code')
describe('dashboard/compiler/configuration-builder', () => {
context('Dynamic Contents', () => {
it('Contents correctly set', () => {
const expected = '__input__'
const { inputConfig } = builder.build(expected)
const actual = reach(inputConfig, 'entry')
expect(actual).to.exist().and.to.equal(expected)
})
})
})
================================================
FILE: packages/core/src/dashboard/compiler/configuration-builder/index.js
================================================
'use strict'
module.exports = require('./configuration-builder')
================================================
FILE: packages/core/src/dashboard/compiler/index.js
================================================
'use strict'
const rollup = require('rollup')
const { build } = require('./configuration-builder')
exports.compile = function (source) {
const { inputConfig, outputConfig } = build(source)
return rollup
.rollup(inputConfig)
.then(bundle => {
const js = bundle.generate(outputConfig)
return js
})
}
================================================
FILE: packages/core/src/dashboard/dashboard.js
================================================
'use strict'
const { reach } = require('hoek')
const Emitter = require('./emitter')
const id = require('../id-gen')
const { schema } = require('./schema')
const configValidator = require('../config-validator')
const parser = require('./parser')
const datasourceLoader = require('../datasource-loader')
const widgetBinder = require('../widget-binder')
const renderer = require('./renderer')
function isWidgetEvent (eventId) {
return eventId.endsWith(':update')
}
class Dashboard {
constructor (json, io) {
const preprocessed = parser.parse(json)
const descriptor = configValidator.validate(preprocessed.name, schema, preprocessed, { allowUnknown: true })
const { name, layout, css } = descriptor
this.id = id()
this.name = name
this.additionalCss = css || {}
this.emitter = new Emitter(io, this.id)
this.layout = layout
this.descriptor = descriptor
}
emit (eventId, data, historical) {
if (!isWidgetEvent(eventId)) {
return this.emitter.emit(eventId, data, historical)
}
const widgetId = eventId.split(':')[0]
const widget = this.widgets[widgetId]
if (!historical && widget) {
widget.history.insert(data)
}
const history = widget ? widget.history.fetch() : {}
const update = Object.assign({ history }, data)
this.emitter.emit(eventId, update, historical)
}
loadDatasources () {
const datasources = reach(this, 'descriptor.datasources', { default: {} })
const hasDatasources = Object.keys(datasources).length
this.datasources = hasDatasources ? datasourceLoader.load(datasources) : {}
}
loadWidgets () {
const widgets = reach(this, 'descriptor.widgets', { default: [] })
const hasWidgets = Object.keys(widgets).length
this.widgets = hasWidgets ? widgetBinder.load(this, widgets, this.datasources) : {}
}
destroy () {
const datasources = Object.values(this.datasources)
console.log(`Dashboard ${this.id} cleaning up ${datasources.length} datasources.`)
datasources.forEach(datasource => {
clearInterval(datasource.timer)
})
const widgets = Object.values(this.widgets)
console.log(`Dashboard ${this.id} attempting cleanup of ${widgets.length} widgets.`)
widgets.forEach(widget => {
widget.hasOwnProperty('destroy') && widget.destroy()
})
}
async toRenderModel () {
const model = await renderer.buildRenderModel(
this.name, this.widgets, this.layout
)
model.css = renderer.compileAdditionalCss(this.additionalCss)
return model
}
}
exports.create = function (descriptor, io) {
return new Dashboard(descriptor, io)
}
================================================
FILE: packages/core/src/dashboard/dashboard.spec.js
================================================
'use strict'
const Emitter = require('./emitter')
const widgetBinder = require('../widget-binder')
const renderer = require('./renderer')
const datasourceLoader = require('../datasource-loader')
const parser = require('./parser')
const configValidator = require('../config-validator')
const { stub, useFakeTimers } = require('sinon')
const { expect } = require('code')
const { create } = require('.')
describe('dashboard', () => {
describe('constructor', () => {
let dashboard
const descriptor = { name: 'bar', layout: { columns: 4, rows: 6 } }
beforeEach(() => {
stub(parser, 'parse').returns(descriptor)
stub(configValidator, 'validate').returns(descriptor)
dashboard = create({}, {
on: stub()
})
})
afterEach(() => {
parser.parse.restore()
configValidator.validate.restore()
})
it('generates a dashboard id', () => {
expect(dashboard.id).to.exist()
})
it('assigns dashboard name', () => {
expect(dashboard.name).to.equal(descriptor.name)
})
it('assigns dashboard layout', () => {
expect(dashboard.layout).to.equal(descriptor.layout)
})
it('assigns descriptor for future use', () => {
expect(dashboard.descriptor).to.equal(descriptor)
})
it('creates emitter', () => {
expect(dashboard.emitter).to.be.an.instanceOf(Emitter)
})
})
describe('#loadDatasources()', () => {
let dashboard
const emitter = { on: stub() }
beforeEach(() => {
stub(parser, 'parse')
stub(configValidator, 'validate')
})
afterEach(() => {
parser.parse.restore()
configValidator.validate.restore()
})
context('empty datasource stanza', () => {
it('empty datasources when none are specified', () => {
parser.parse.returns({})
configValidator.validate.returns({})
dashboard = create({}, emitter)
dashboard.loadDatasources()
expect(dashboard.datasources).to.equal({})
})
})
context('list of datasources', () => {
beforeEach(() => {
const descriptor = {
datasources: {
foo: { foo: 'bar' }
}
}
parser.parse.returns(descriptor)
configValidator.validate.returns(descriptor)
stub(datasourceLoader, 'load').returns('bar')
dashboard = create({}, emitter)
dashboard.loadDatasources()
})
afterEach(() => {
datasourceLoader.load.restore()
})
it('calls loader to load datasources', () => {
expect(datasourceLoader.load.callCount).to.equal(1)
})
it('calls loader to load datasources', () => {
expect(dashboard.datasources).to.equal('bar')
})
})
})
describe('#loadWidgets()', () => {
let dashboard
const emitter = { on: stub() }
beforeEach(() => {
stub(parser, 'parse')
stub(configValidator, 'validate')
})
afterEach(() => {
parser.parse.restore()
configValidator.validate.restore()
})
context('empty widget stanza', () => {
it('empty widgets when none are specified', () => {
parser.parse.returns({})
configValidator.validate.returns({})
dashboard = create({}, emitter)
dashboard.loadWidgets()
expect(dashboard.widgets).to.equal({})
})
})
context('list of widgets', () => {
beforeEach(() => {
const descriptor = {
widgets: [
{ foo: 'bar' }
]
}
parser.parse.returns(descriptor)
configValidator.validate.returns(descriptor)
stub(widgetBinder, 'load').returns('bar')
dashboard = create({}, emitter)
dashboard.loadWidgets()
})
afterEach(() => {
widgetBinder.load.restore()
})
it('calls loader to load widgets', () => {
expect(widgetBinder.load.callCount).to.equal(1)
})
it('calls loader to load widgets', () => {
expect(dashboard.widgets).to.equal('bar')
})
})
})
describe('#destroy()', () => {
let dashboard
let clock
beforeEach(() => {
clock = useFakeTimers()
stub(parser, 'parse').returns({})
stub(configValidator, 'validate').returns({})
dashboard = create({}, {
on: stub()
})
})
afterEach(() => {
parser.parse.restore()
configValidator.validate.restore()
clock.restore()
})
context('with list of datasources', () => {
let stub1 = stub()
let stub2 = stub()
beforeEach(() => {
const timer1 = setInterval(stub1, 1)
const timer2 = setInterval(stub2, 1)
dashboard.datasources = {
foo: { timer: timer1 },
bar: { timer: timer2 }
}
dashboard.widgets = {}
clock.tick(1)
})
it('clears all timers', () => {
dashboard.destroy()
clock.tick(1)
expect(stub1.callCount).to.equal(1)
expect(stub1.callCount).to.equal(1)
})
})
context('when no datasources exist', () => {
it('succeeds silently', () => {
dashboard.datasources = {}
dashboard.widgets = {}
expect(() => {
dashboard.destroy()
}).not.to.throw()
})
})
context('with list of widgets', () => {
const widgets = {
abc: { destroy: stub() },
def: { }
}
beforeEach(() => {
dashboard.widgets = widgets
dashboard.datasources = {}
})
it('calls destroy on widgets which support it', () => {
dashboard.destroy()
expect(widgets.abc.destroy.callCount).to.equal(1)
})
})
context('when no widgets exist', () => {
it('succeeds silently', () => {
dashboard.datasources = {}
dashboard.widgets = {}
expect(() => {
dashboard.destroy()
}).not.to.throw()
})
})
})
describe('#toRenderModel()', () => {
context('no additiona css', () => {
let dashboard
const descriptor = {
name: 'some-name',
layout: 'some-layout'
}
beforeEach(() => {
stub(parser, 'parse').returns(descriptor)
stub(configValidator, 'validate').returns(descriptor)
stub(renderer, 'buildRenderModel').returns({})
dashboard = create({}, {
on: stub()
})
dashboard.widgets = { abc: { foo: 'bar' } }
dashboard.toRenderModel()
})
afterEach(() => {
parser.parse.restore()
configValidator.validate.restore()
renderer.buildRenderModel.restore()
})
it('calls renderer with name', () => {
expect(renderer.buildRenderModel.firstCall.args[0]).to.equal(dashboard.name)
})
it('calls renderer with widgets', () => {
expect(renderer.buildRenderModel.firstCall.args[1]).to.equal(dashboard.widgets)
})
it('calls renderer with layout', () => {
expect(renderer.buildRenderModel.firstCall.args[2]).to.equal(dashboard.layout)
})
})
context('additional css', () => {
let dashboard
let renderModel
const descriptor = {
name: 'some-name',
layout: 'some-layout'
}
beforeEach(async () => {
stub(parser, 'parse').returns(descriptor)
stub(configValidator, 'validate').returns({})
stub(renderer, 'buildRenderModel').returns({})
stub(renderer, 'compileAdditionalCss').returns('some: css')
dashboard = create({}, {
on: stub()
})
dashboard.additionalCss = { some: 'css' }
dashboard.widgets = {}
renderModel = await dashboard.toRenderModel()
})
afterEach(() => {
renderer.compileAdditionalCss.restore()
parser.parse.restore()
configValidator.validate.restore()
renderer.buildRenderModel.restore()
})
it('calls css transpiler', () => {
expect(renderer.compileAdditionalCss.callCount).to.equal(1)
})
it('css is compiled', () => {
expect(renderer.compileAdditionalCss.firstCall.args[0]).to.equal({ some: 'css' })
})
it('dashboard css contains additional css', () => {
expect(renderModel.css).to.equal('some: css')
})
})
})
describe('#emit()', () => {
const exampleEvent = { some: 'data' }
let dashboard
const socketEmitter = {
on: stub()
}
const dashboardEmitter = {
emit: stub()
}
beforeEach(() => {
stub(parser, 'parse').returns({})
stub(configValidator, 'validate').returns({})
dashboard = create({}, socketEmitter)
dashboard.emitter = dashboardEmitter
dashboard.widgets = {
xyz: {
history: {
insert: stub(),
fetch: stub().returns([{ foo: 'bar' }])
}
}
}
})
afterEach(() => {
parser.parse.restore()
configValidator.validate.restore()
dashboardEmitter.emit.reset()
})
context('a widget event', () => {
it('emits event', () => {
dashboard.emit('xyz:update', exampleEvent)
expect(dashboardEmitter.emit.callCount).to.equal(1)
})
it('calls widget event history', () => {
dashboard.emit('xyz:update', exampleEvent)
expect(dashboard.widgets.xyz.history.insert.callCount).to.equal(1)
})
it('returns existing history', () => {
dashboard.emit('xyz:update', exampleEvent)
expect(
dashboardEmitter.emit.firstCall.args[1].history
).to.exist()
.and.to.equal([{ foo: 'bar' }])
})
it('widget does not exist', () => {
expect(() => {
dashboard.emit('abc:update', exampleEvent)
}).not.to.throw()
})
it('stores non-historical event', () => {
dashboard.emit('xyz:update', exampleEvent)
expect(
dashboard.widgets.xyz.history.insert.firstCall.args[0]
).to.equal(exampleEvent)
})
it('does not store non-historical event', () => {
dashboard.emit('xyz:update', exampleEvent, true)
expect(dashboard.widgets.xyz.history.insert.callCount).to.equal(0)
})
})
context('a non widget event', () => {
it('emits event', () => {
dashboard.emit('xyz:abc', exampleEvent)
expect(dashboardEmitter.emit.callCount).to.equal(1)
})
it('does not add event to history', () => {
dashboard.emit('xyz:update', exampleEvent, true)
expect(
dashboard.widgets.xyz.history.insert.callCount
).to.equal(0)
})
})
})
})
================================================
FILE: packages/core/src/dashboard/emitter/emitter.js
================================================
'use strict'
const chalk = require('chalk')
class Emitter {
constructor (socketio, room) {
this.io = socketio
this.room = room
this.recentEvents = {}
this.io.on('connection', (socket) => {
this.clientJoinHandler(socket)
})
}
clientJoinHandler (socket) {
socket.join(this.room)
const historicalEvents = this.recentEvents
const eventIds = Object.keys(historicalEvents)
console.log(`Client ${chalk.bold.green(socket.id)}
connected to ${chalk.bold.red(this.room)}.
Receives ${chalk.bold.yellow(eventIds.length)} historical events.`)
eventIds.map(eventId => {
this.emit(eventId, historicalEvents[eventId], true)
})
}
emit (event, data, historical) {
this.io.to(this.room).emit(event, data)
if (!historical) {
this.recentEvents[event] = data
}
}
}
module.exports = Emitter
================================================
FILE: packages/core/src/dashboard/emitter/emitter.spec.js
================================================
'use strict'
const Emitter = require('.')
const { expect } = require('code')
const { stub, spy } = require('sinon')
describe('dashboard/emitter', () => {
const room = 'my-room'
const broadcastStub = {
emit: spy()
}
const socketSpy = {
on: stub(),
to: stub().withArgs(room).returns(broadcastStub)
}
const emitter = new Emitter(socketSpy, room)
it('Instantiation of emitter binds handler', () => {
expect(socketSpy.on.callCount).to.equal(1)
expect(socketSpy.on.firstCall.args[0]).to.equal('connection')
})
it('Emitted events are saved', () => {
emitter.emit('abc', {id: 'abc'})
emitter.emit('def', {id: 'def'})
emitter.emit('ghi', {id: 'ghi'})
const recentEvents = emitter.recentEvents
expect(Object.keys(recentEvents)).to.have.length(3)
})
it('Repeated event updates previous event', () => {
emitter.emit('def', {id: 'pqr'})
const recentEvents = emitter.recentEvents
expect(Object.keys(recentEvents)).to.have.length(3)
expect(recentEvents.def).to.equal({id: 'pqr'})
})
it('On connect, socket is joined to a room', () => {
const mockSocket = { id: 'xyz', join: spy() }
emitter.clientJoinHandler(mockSocket)
expect(mockSocket.join.callCount).to.equal(1)
expect(mockSocket.join.firstCall.args[0]).to.equal(room)
})
it('On connect, socket is joined to a room', () => {
const emit = broadcastStub.emit
emit.reset()
const mockSocket = { id: 'xyz', join: spy() }
emitter.clientJoinHandler(mockSocket)
expect(emit.callCount).to.equal(3)
expect(emit.firstCall.args).to.equal(['abc', {id: 'abc'}])
expect(emit.secondCall.args).to.equal(['def', {id: 'pqr'}])
expect(emit.thirdCall.args).to.equal(['ghi', {id: 'ghi'}])
})
})
================================================
FILE: packages/core/src/dashboard/emitter/index.js
================================================
'use strict'
module.exports = require('./emitter')
================================================
FILE: packages/core/src/dashboard/index.js
================================================
'use strict'
module.exports = require('./dashboard')
================================================
FILE: packages/core/src/dashboard/loader/index.js
================================================
'use strict'
module.exports = require('./loader')
================================================
FILE: packages/core/src/dashboard/loader/loader.js
================================================
'use strict'
const { NotFoundError } = require('../../errors')
const Dashboard = require('..')
const fs = require('fs')
const { join } = require('path')
function load (cache, name, io) {
const path = join(process.cwd(), 'dashboards', `${name}.json`)
if (!fs.existsSync(path)) {
throw new NotFoundError(`Dashboard ${name} does not exist.`)
}
const descriptor = require(path)
return add(cache, name, io, descriptor)
}
function add (cache, name, io, descriptor) {
const dashboard = Dashboard.create(descriptor, io)
dashboard.loadDatasources()
dashboard.loadWidgets()
cache[name] = dashboard
return cache[name]
}
function find (cache, name, io) {
return cache[name] || load(cache, name, io)
}
function has (cache, name) {
return !!cache[name]
}
module.exports = {
find,
has,
add,
load
}
================================================
FILE: packages/core/src/dashboard/loader/loader.spec.js
================================================
'use strict'
const { NotFoundError } = require('../../errors')
const loader = require('.')
const { expect } = require('code')
describe('dashboard/loader', () => {
it('Dashboard is not found', () => {
expect(() => {
return loader.load({}, 'xyz')
}).to.throw(NotFoundError, 'Dashboard xyz does not exist.')
})
context('#add()', () => {
const cache = {}
const emitter = { on: () => { } }
const descriptor = { layout: { columns: 0, rows: 0 }, widgets: [] }
beforeEach(() => {
loader.add(cache, 'xyz', emitter, descriptor)
})
it('Add dashboard to cache', () => {
expect(cache.xyz.descriptor).to.equal(descriptor)
})
it('Find dashboard from cache', () => {
expect(loader.find(cache, 'xyz', emitter).descriptor).to.equal(descriptor)
})
})
context('#has()', () => {
const cache = { 'abc': {} }
it('is contained in cache', () => {
expect(loader.has(cache, 'abc')).to.be.true()
})
it('is not contained in cache', () => {
expect(loader.has(cache, 'xyz')).to.be.false()
})
})
})
================================================
FILE: packages/core/src/dashboard/parser/index.js
================================================
'use strict'
module.exports = require('./parser')
================================================
FILE: packages/core/src/dashboard/parser/parser.js
================================================
'use strict'
const { ConfigurationError } = require('../../errors')
function get (directive) {
if (process.env.hasOwnProperty(directive)) {
return process.env[directive]
}
throw new ConfigurationError(`Environment variable ${directive} does not exist`)
}
function parse (json) {
const root = Object.keys(json)
root.forEach(key => {
const child = json[key]
if (typeof child !== 'object') { return }
const directive = child['$env']
if (directive) {
json[key] = get(directive)
} else {
parse(json[key])
}
})
return json
}
exports.parse = parse
================================================
FILE: packages/core/src/dashboard/parser/parser.spec.js
================================================
'use strict'
const { expect } = require('code')
const { parse } = require('.')
const { ConfigurationError } = require('../../errors')
describe('dashboard/parser', () => {
beforeEach(() => {
process.env.SOME_KEY = 'abcde'
})
afterEach(() => {
process.env.SOME_KEY = undefined
})
it('replaces config element with environmental variable', () => {
const config = {
some: {
value: { $env: 'SOME_KEY' }
}
}
expect(parse(config)).to.equal({
some: {
value: 'abcde'
}
})
})
it('replaces multiple elements', () => {
const config = {
some: {
value: { $env: 'SOME_KEY' },
other: {
value: { $env: 'SOME_KEY' }
}
}
}
expect(parse(config)).to.equal({
some: {
value: 'abcde',
other: {
value: 'abcde'
}
}
})
})
it('replaces entire element', () => {
const config = {
some: {
value: {
$env: 'SOME_KEY',
invalid: 'entry'
}
}
}
expect(parse(config)).to.equal({
some: {
value: 'abcde'
}
})
})
it('throws error if variable does not exist', () => {
const config = {
some: {
value: { $env: 'NONEXISTENT_KEY' }
}
}
expect(() => {
parse(config)
}).to.throw(
ConfigurationError,
'Environment variable NONEXISTENT_KEY does not exist'
)
})
})
================================================
FILE: packages/core/src/dashboard/renderer/index.js
================================================
'use strict'
module.exports = require('./renderer')
================================================
FILE: packages/core/src/dashboard/renderer/renderer.js
================================================
'use strict'
const bundler = require('../bundler')
const compiler = require('../compiler')
const Css = require('json-to-css')
function renderWidgets (widgets, layout) {
const widgetModel = Object.values(widgets)
return widgetModel.map(widget => {
return widget.toRenderModel(layout)
})
}
exports.buildRenderModel = async function (name, widgets, layout) {
const renderedWidgets = renderWidgets(widgets, layout)
const { js, html } = bundler.build(renderedWidgets)
const script = await compiler.compile(js)
return {
name,
html,
js: script
}
}
exports.compileAdditionalCss = function (css) {
return Css.of(css)
}
================================================
FILE: packages/core/src/dashboard/renderer/renderer.spec.js
================================================
'use strict'
describe('dashboard/renderer', () => {
})
================================================
FILE: packages/core/src/dashboard/schema/index.js
================================================
'use strict'
module.exports = require('./schema')
================================================
FILE: packages/core/src/dashboard/schema/schema.js
================================================
'use strict'
const Joi = require('joi')
const layoutSchema = Joi.object({
columns: Joi.number().required().description('Number of columns'),
rows: Joi.number().required().description('Number of rows')
}).required().description('Layout')
const widgetPositionSchema = Joi.object({
x: Joi.number().required().description('Column for widget in dashboard, zero indexed'),
y: Joi.number().required().description('Row for widget in dashboard, zero indexed'),
w: Joi.number().required().description('Widget width in dashboard columns'),
h: Joi.number().required().description('Widget height in dashboard rows')
}).required().description('Widget position data')
const widgetSchema = Joi.object({
position: widgetPositionSchema,
widget: Joi.any().required().description('Path to widget, Node package name, or Class'),
datasource: Joi.string().optional().description('Datasource name'),
options: Joi.object().optional().description('Widget configuration'),
background: Joi.string().optional().description('Optional background styling, used as css background')
}).description('Widget Configuration')
const widgetsSchema = Joi.array().required().items(widgetSchema).description('List of widgets')
const datasourcesSchema = Joi.object().pattern(/.*/, Joi.object({
module: Joi.string().required().description('Datasource module name or directory path'),
schedule: Joi.number().required().description('Update frequency, milliseconds'),
options: Joi.object().optional().description('Datasource specific options')
})).optional().description('Hash of datasources')
const dashboardSchema = Joi.object({
name: Joi.string().optional().description('Dashboard name'),
layout: layoutSchema,
datasources: datasourcesSchema,
widgets: widgetsSchema
}).description('Dashboard Descriptor')
exports.schema = dashboardSchema
================================================
FILE: packages/core/src/dashboard/schema/schema.spec.js
================================================
'use strict'
const { schema } = require('.')
const fs = require('fs')
const { join } = require('path')
const { expect } = require('code')
const Joi = require('joi')
describe('dashboard/schema', () => {
context('Parse', () => {
it('Throws on invalid schema', () => {
const { error } = Joi.validate({}, schema)
expect(error.message).to.include('"layout" is required')
})
const dashboardsDir = join(__dirname, '..', '..', '..', 'dashboards')
const boards = fs.readdirSync(dashboardsDir)
boards.forEach(board => {
it(`Parses valid schema ${board}`, () => {
const json = require(join(dashboardsDir, board))
const { error } = Joi.validate(json, schema)
expect(error).not.to.exist()
})
})
})
context('validation', () => {
context('datasources', () => {
it('are optional', () => {
const descriptor = {
layout: {
columns: 1,
rows: 1
},
widgets: []
}
const { value } = Joi.validate(descriptor, schema)
expect(value).to.equal(descriptor)
})
it('must be datasources', () => {
const descriptor = {
layout: {
columns: 1,
rows: 1
},
widgets: [],
datasources: {
'some-datasource': {}
}
}
const { error } = Joi.validate(descriptor, schema)
expect(error.message).to.include('fails because [child "module" fails')
})
})
it('datasource options is optional', () => {
const descriptor = {
layout: {
columns: 1,
rows: 1
},
widgets: [],
datasources: {
'some-datasource': {
module: 'a',
schedule: 30000
}
}
}
const { value } = Joi.validate(descriptor, schema)
expect(value).to.equal(descriptor)
})
it('must include update schedule', () => {
const descriptor = {
layout: {
columns: 1,
rows: 1
},
widgets: [],
datasources: {
'some-datasource': {
module: 'a'
}
}
}
const { error } = Joi.validate(descriptor, schema)
expect(error.message).to.include('"schedule" is required')
})
it('update schedule must be in milliseconds', () => {
const descriptor = {
layout: {
columns: 1,
rows: 1
},
widgets: [],
datasources: {
'some-datasource': {
module: 'a',
schedule: 'aaa'
}
}
}
const { error } = Joi.validate(descriptor, schema)
expect(error.message).to.include('"schedule" must be a number')
})
it('must include module name', () => {
const descriptor = {
layout: {
columns: 1,
rows: 1
},
widgets: [],
datasources: {
'some-datasource': {
schedule: 30000
}
}
}
const { error } = Joi.validate(descriptor, schema)
expect(error.message).to.include('"module" is required')
})
})
})
================================================
FILE: packages/core/src/dashboard-event/dashboard-event.js
================================================
'use strict'
exports.build = function (data) {
return {
meta: {
updated: new Date()
},
data
}
}
================================================
FILE: packages/core/src/dashboard-event/index.js
================================================
'use strict'
module.exports = require('./dashboard-event')
================================================
FILE: packages/core/src/datasource/datasource.spec.js
================================================
'use strict'
const loader = require('.')
const locator = require('./locator')
const DatasourceBuilder = require('util/datasource.builder')
const validator = require('./validator')
const { expect } = require('code')
const { stub } = require('sinon')
context('datasource.validator', () => {
const widget = DatasourceBuilder.create().build()
context('Datasource specified', () => {
beforeEach(() => {
stub(locator, 'locate')
stub(validator, 'validate').returns({})
})
afterEach(() => {
locator.locate.restore()
validator.validate.restore()
})
it('Registers widget data source', () => {
locator.locate.returns({ Constructor: widget, options: {} })
loader.load('some-widget', {}, 'a-datasource')
expect(locator.locate.callCount).to.equal(1)
expect(locator.locate.firstCall.args[1]).to.equal('a-datasource')
})
it('calls for widget validation on load', () => {
const options = { foo: 'bar' }
locator.locate.returns({
Constructor: widget,
options,
validation: {}
})
loader.load('some-widget', {}, 'a-datasource')
expect(validator.validate.callCount).to.equal(1)
expect(validator.validate.firstCall.args[2]).to.equal(options)
})
it('no validation specified', () => {
locator.locate.returns({
Constructor: widget
})
loader.load('some-widget', {}, 'a-datasource')
expect(validator.validate.callCount).to.equal(0)
})
})
context('no datasource specified', () => {
it('will polyfill a fetch method which throws', () => {
const loaded = loader.load('some-widget', {}, undefined)
function fn () { return loaded.fetch() }
expect(fn).to.throw('Widget some-widget requested data, but no datasource was configured. Check the widget configuration in your dashboard config.')
})
})
})
================================================
FILE: packages/core/src/datasource/dummy-datasource/dummy-datasource.spec.js
================================================
'use strict'
const DummyDatasource = require('.')
const { expect } = require('code')
describe('datasource.dummy-datasource', () => {
it('can be constructed', () => {
expect(DummyDatasource).to.be.a.function()
})
it('Does not fetch', () => {
const widgetName = 'abczys'
const ds = new DummyDatasource({ widgetName })
expect(ds.fetch.bind(ds)).to.throw(Error, `Widget ${widgetName} requested data, but no datasource was configured. Check the widget configuration in your dashboard config.`)
})
})
================================================
FILE: packages/core/src/datasource/dummy-datasource/index.js
================================================
'use strict'
class DummyDatasource {
constructor (options) {
this.config = options
}
fetch () {
throw new Error(`Widget ${this.config.widgetName} requested data, but no datasource was configured. Check the widget configuration in your dashboard config.`)
}
}
module.exports = DummyDatasource
================================================
FILE: packages/core/src/datasource/index.js
================================================
'use strict'
const Joi = require('joi')
const { applyToDefaults } = require('hoek')
const DummyDatasource = require('./dummy-datasource')
const validator = require('./validator')
const locator = require('./locator')
const datasourceValidation = {
schedule: Joi.number().min(0).description('Datasource refresh schedule')
}
function extendValidation (validation) {
return validation
? applyToDefaults(datasourceValidation, validation)
: datasourceValidation
}
function loadValidOptions (widgetName, validation, options) {
return options
? validator.validate(widgetName, validation, options)
: {}
}
exports.load = function (widgetName, dashboard, datasourceName) {
if (!datasourceName) {
return new DummyDatasource({ widgetName })
}
const { Constructor, validation, options } = locator.locate(dashboard.datasources, datasourceName)
const datasourceValidation = extendValidation(validation)
const config = loadValidOptions(widgetName, datasourceValidation, options)
return new Constructor(config)
}
================================================
FILE: packages/core/src/datasource/locator/index.js
================================================
'use strict'
const { WidgetRegistrationError } = require('../../errors')
exports.locate = function (datasources, datasource) {
const resolved = datasources[datasource]
if (!resolved) {
throw new WidgetRegistrationError(`Unable to use datasource ${datasource} as it does not exist`)
}
return resolved
}
================================================
FILE: packages/core/src/datasource/locator/locator.spec.js
================================================
'use strict'
const { WidgetRegistrationError } = require('../../errors')
const locator = require('.')
const { expect } = require('code')
describe('datasource.locator', () => {
it('loads datasource', () => {
const datasources = { abcde: { foo: 'bar' } }
expect(
locator.locate(datasources, 'abcde')
).to.equal(datasources.abcde)
})
it('Cannot load datasource', () => {
expect(() => {
return locator.locate({}, 'non-existent')
}).to.throw(WidgetRegistrationError, 'Unable to use datasource non-existent as it does not exist')
})
})
================================================
FILE: packages/core/src/datasource/validator/index.js
================================================
'use strict'
const configValidator = require('../../config-validator')
exports.validate = function (widgetName, validation, options = {}) {
return validation
? configValidator.validate(`widget:${widgetName}`, validation, options)
: options
}
================================================
FILE: packages/core/src/datasource/validator/validator.spec.js
================================================
'use strict'
const validator = require('.')
const configValidator = require('../../config-validator')
const { stub } = require('sinon')
const { expect } = require('code')
describe('datasource.validator', () => {
context('No validation specified', () => {
let options
beforeEach(() => {
stub(configValidator, 'validate')
options = validator.validate('a', null, { a: 'b' })
})
afterEach(() => {
configValidator.validate.restore()
})
it('returns datasource options', () => {
expect(
options
).to.equal({ a: 'b' })
})
it('validation is not called as it does not exist', () => {
expect(configValidator.validate.callCount).to.equal(0)
})
})
context('Validation specified', () => {
beforeEach(() => {
stub(configValidator, 'validate')
})
afterEach(() => {
configValidator.validate.restore()
})
it('validation is called if available', () => {
validator.validate('a', {}, {})
expect(configValidator.validate.callCount).to.equal(1)
})
})
})
================================================
FILE: packages/core/src/datasource-binder/datasource-binder.js
================================================
'use strict'
const { createEmitter } = require('./datasource-emitter')
const chalk = require('chalk')
exports.bind = function (name, datasource, schedule = 30000) {
const emitter = createEmitter()
function fetchFunction () {
datasource
.fetch()
.then(data => {
emitter.emit('update', data)
})
.catch(e => {
console.error(chalk.red.bold(`Error updating datasource ${name}`), chalk.yellow(e.message))
console.error(chalk.red(e.stack))
})
}
const timer = setInterval(fetchFunction, schedule)
fetchFunction()
return {
timer,
emitter
}
}
================================================
FILE: packages/core/src/datasource-binder/datasource-binder.spec.js
================================================
'use strict'
const { expect } = require('code')
const binder = require('.')
const { stub, useFakeTimers } = require('sinon')
const datasourceEmitter = require('./datasource-emitter')
describe('datasource-binder', () => {
describe('timers', () => {
let clock
let timer
const emitter = {
emit: stub()
}
const datasource = {
fetch: stub().resolves({ foo: 'bar' })
}
beforeEach(() => {
stub(datasourceEmitter, 'createEmitter').returns(emitter)
clock = useFakeTimers()
const result = binder.bind('xyz', datasource, 500)
timer = result.timer
})
afterEach(() => {
datasourceEmitter.createEmitter.restore()
clock.restore()
datasource.fetch.reset()
clearInterval(timer)
})
it('fetch is called immediately', () => {
expect(datasource.fetch.callCount).to.equal(1)
})
it('fetch when interval is reached', () => {
clock.tick(500)
expect(datasource.fetch.callCount).to.equal(2)
})
it('on each subsequent interval', () => {
clock.tick(1000)
expect(datasource.fetch.callCount).to.equal(3)
})
})
context('missing schedule', () => {
let clock
let timer
const emitter = {
emit: stub()
}
const datasource = {
fetch: stub().resolves({ foo: 'bar' })
}
beforeEach(() => {
stub(datasourceEmitter, 'createEmitter').returns(emitter)
clock = useFakeTimers()
const result = binder.bind('xyz', datasource)
timer = result.timer
})
afterEach(() => {
datasourceEmitter.createEmitter.restore()
clock.restore()
datasource.fetch.reset()
clearInterval(timer)
})
it('default interval is set to 30 seconds', () => {
clock.tick(30000)
expect(datasource.fetch.callCount).to.equal(2)
})
})
describe('event emitters', () => {
let timer
let emitter
const expectedData = { foo: 'bar' }
const datasource = {
fetch: stub().resolves(expectedData)
}
beforeEach(() => {
const result = binder.bind('xyz', datasource, 500)
timer = result.timer
emitter = result.emitter
})
afterEach(() => {
datasource.fetch.reset()
clearInterval(timer)
})
it('fetch emits data', done => {
emitter.on('update', data => {
expect(data).to.equal(expectedData)
done()
})
})
})
})
================================================
FILE: packages/core/src/datasource-binder/datasource-emitter.js
================================================
'use strict'
const EventEmitter = require('events')
class DatasourceEmitter extends EventEmitter {}
exports.createEmitter = function () {
return new DatasourceEmitter()
}
================================================
FILE: packages/core/src/datasource-binder/index.js
================================================
'use strict'
module.exports = require('./datasource-binder')
================================================
FILE: packages/core/src/datasource-loader/datasource-loader.js
================================================
'use strict'
const resolver = require('../resolver')
const configValidator = require('../config-validator')
const datasourceBinder = require('../datasource-binder')
const loadError = 'Cannot load datasource someDatasource because it does not look like a datasource'
function resolveDatasource (path) {
return resolver.resolve(path)
}
function parseConfiguration (datasourceName, validation, options) {
return configValidator.validate(datasourceName, validation, options)
}
function register (registrationFn, configuration) {
if (typeof registrationFn !== 'function') {
throw new Error(`${loadError} (no registration function)`)
}
return registrationFn(configuration)
}
function initialise (name, registrationFn, configuration = {}, schedule) {
const datasource = register(registrationFn, configuration)
if (typeof datasource.fetch !== 'function') {
throw new Error(`${loadError} (no fetch function)`)
}
return datasourceBinder.bind(name, datasource, schedule)
}
exports.load = function (descriptor) {
const datasourceNames = Object.keys(descriptor)
return datasourceNames.reduce((datasources, name) => {
const { module: path, options, schedule } = descriptor[name]
const { validation, register } = resolveDatasource(path)
const configuration = parseConfiguration(name, validation, options)
datasources[name] = initialise(name, register, configuration, schedule)
return datasources
}, {})
}
================================================
FILE: packages/core/src/datasource-loader/datasource-loader.spec.js
================================================
'use strict'
const { expect } = require('code')
const { stub } = require('sinon')
const datasourceLoader = require('.')
const resolver = require('../resolver')
const binder = require('../datasource-binder')
const validator = require('../config-validator')
describe('datasource-loader', () => {
describe('datasource prototype', () => {
const descriptor = {
someDatasource: {
module: '../some/path',
schedule: 1000,
options: {
foo: 'bar'
}
}
}
context('missing fetch function', () => {
const someDatasource = {
register: options => {
return {}
}
}
beforeEach(() => {
stub(resolver, 'resolve').returns(someDatasource)
})
it('fails to load', () => {
expect(() => {
datasourceLoader.load(descriptor)
}).to.throw(
Error,
'Cannot load datasource someDatasource because it does not look like a datasource (no fetch function)'
)
})
afterEach(() => {
resolver.resolve.restore()
})
})
context('missing registration function', () => {
const someDatasource = {}
beforeEach(() => {
stub(resolver, 'resolve').returns(someDatasource)
})
it('fails to load', () => {
expect(() => {
datasourceLoader.load(descriptor)
}).to.throw(
Error,
'Cannot load datasource someDatasource because it does not look like a datasource (no registration function)'
)
})
afterEach(() => {
resolver.resolve.restore()
})
})
})
context('no datasource options specified', () => {
const descriptor = {
someDatasource: {
module: '../some/path',
schedule: 1000
}
}
const someDatasource = {
register: stub().returns({ fetch: stub() })
}
beforeEach(() => {
stub(resolver, 'resolve').returns(someDatasource)
stub(binder, 'bind')
datasourceLoader.load(descriptor)
})
it('should call register', () => {
expect(someDatasource.register.callCount).to.equal(1)
})
it('should pass empty options', () => {
expect(someDatasource.register.firstCall.args[0]).to.equal({})
})
afterEach(() => {
resolver.resolve.restore()
binder.bind.restore()
})
})
context('multiple datasources', () => {
let datasources
const descriptor = {
'plugin-a': {
module: '../some/path'
},
'plugin-b': {
module: '../some/path'
}
}
const bound = { some: 'value' }
const datasource = {
register: () => {
return {
fetch: () => {}
}
}
}
beforeEach(() => {
stub(resolver, 'resolve').returns(datasource)
stub(validator, 'validate').returns({})
stub(binder, 'bind').returns(bound)
datasources = datasourceLoader.load(descriptor)
})
afterEach(() => {
resolver.resolve.restore()
validator.validate.restore()
binder.bind.restore()
})
it('contains two datasources', () => {
const datasourceNames = Object.keys(datasources)
expect(datasourceNames.length).to.equal(2)
})
it('datasources are exposed', () => {
expect(datasources['plugin-a']).to.equal(bound)
})
})
})
================================================
FILE: packages/core/src/datasource-loader/index.js
================================================
'use strict'
module.exports = require('./datasource-loader')
================================================
FILE: packages/core/src/errors/configuration.error.js
================================================
'use strict'
module.exports = class ConfigurationError extends Error {
}
================================================
FILE: packages/core/src/errors/index.js
================================================
'use strict'
const { upperCamel } = require('../upper-camel')
const requireDirectory = require('require-directory')
const errors = requireDirectory(module, {
rename: upperCamel
})
module.exports = errors
================================================
FILE: packages/core/src/errors/not-found.error.js
================================================
'use strict'
module.exports = class NotFoundError extends Error {
}
================================================
FILE: packages/core/src/errors/plugin-registration.error.js
================================================
'use strict'
module.exports = class PluginRegistrationError extends Error {
}
================================================
FILE: packages/core/src/errors/widget-registration.error.js
================================================
'use strict'
module.exports = class WidgetRegistrati extends Error {
}
================================================
FILE: packages/core/src/id-gen/id-gen.js
================================================
'use strict'
module.exports = () => {
const random = Math.random() * 0xFFFFFFFFFFFF << 0
const positive = Math.abs(random)
return positive.toString(16)
}
================================================
FILE: packages/core/src/id-gen/id-gen.spec.js
================================================
'use strict'
const id = require('.')
const { expect } = require('code')
describe('id-gen', () => {
it('generates a string id', () => {
expect(id())
.to.exist()
.and.to.be.a.string()
})
})
================================================
FILE: packages/core/src/id-gen/index.js
================================================
'use strict'
module.exports = require('./id-gen')
================================================
FILE: packages/core/src/plugins/api/api.js
================================================
'use strict'
const Joi = require('joi')
const viewCurrentHandlers = require('./handlers/view/current')
const dashboardHandlers = require('./handlers/dashboards')
const ApiPlugin = {
register: function (server, options, next) {
server.route({
method: 'PUT',
path: '/api/v1/view/current',
config: {
tags: ['api'],
validate: {
payload: {
dashboard: Joi.string().required().description('Dashboard to switch to')
}
}
},
handler: viewCurrentHandlers.put
})
server.route({
method: 'PUT',
path: '/api/v1/dashboards/{name}',
config: {
tags: ['api'],
validate: {
params: {
name: Joi.string().required().description('Dashboard id')
},
payload: {
descriptor: Joi.object().required().description('Dashboard descriptor')
}
}
},
handler: dashboardHandlers.put
})
next()
}
}
ApiPlugin.register.attributes = {
name: 'api',
version: '1.0.0',
dependencies: ['hapi-api-secret-key']
}
module.exports = ApiPlugin
================================================
FILE: packages/core/src/plugins/api/handlers/dashboards/index.js
================================================
'use strict'
exports.put = require('./put')
================================================
FILE: packages/core/src/plugins/api/handlers/dashboards/put.js
================================================
'use strict'
const loader = require('../../../../dashboard/loader')
module.exports = function (request, reply) {
const { dashboards } = request.server.plugins.ui
const { io } = request.server.plugins.socket
const { name } = request.params
const { descriptor } = request.payload
const isUpdate = loader.has(dashboards, name)
loader.add(dashboards, name, io, descriptor)
reply().code(isUpdate ? 200 : 201)
}
================================================
FILE: packages/core/src/plugins/api/handlers/dashboards/put.spec.js
================================================
'use strict'
const { stub } = require('sinon')
const { put } = require('.')
const { expect } = require('code')
describe('core/plugins/api', () => {
describe('v1', () => {
describe('dashboards', () => {
const name = 'xyz'
const descriptor = { layout: { columns: 0, rows: 0 }, widgets: [] }
const dashboards = {}
const server = {
plugins: {
ui: {
dashboards
},
socket: {
io: {
on: stub()
}
}
}
}
const reply = stub()
.returns({
code: stub()
})
beforeEach(() => {
const request = {
server,
params: {
name
},
payload: {
descriptor
}
}
put(request, reply)
})
afterEach(() => {
reply.reset()
})
it('reply is called', () => {
expect(reply.callCount).to.equal(1)
})
it('dashboards contains new dashboard', () => {
expect(Object.keys(dashboards)).to.include(name)
})
})
})
})
================================================
FILE: packages/core/src/plugins/api/handlers/view/current/index.js
================================================
'use strict'
exports.put = require('./put')
================================================
FILE: packages/core/src/plugins/api/handlers/view/current/put.js
================================================
'use strict'
module.exports = function (request, reply) {
const { io } = request.server.plugins.socket
const { dashboard } = request.payload
io.emit('view:current', { dashboard })
reply()
}
================================================
FILE: packages/core/src/plugins/api/handlers/view/current/put.spec.js
================================================
'use strict'
const { stub } = require('sinon')
const { put } = require('.')
const { expect } = require('code')
describe('core/plugins/api', () => {
describe('v1', () => {
describe('view/current', () => {
const dashboard = 'xyz'
const server = {
plugins: {
socket: {
io: {
emit: stub()
}
}
}
}
const reply = stub()
beforeEach(() => {
const request = {
server,
payload: {
dashboard
}
}
put(request, reply)
})
afterEach(() => {
server.plugins.socket.io.emit.reset()
reply.reset()
})
it('reply is called', () => {
expect(reply.callCount).to.equal(1)
})
it('emits a broadcast event', () => {
expect(server.plugins.socket.io.emit.callCount).to.equal(1)
})
it('broadcast event has dashboard switch event', () => {
expect(server.plugins.socket.io.emit.firstCall.args[0]).to.equal('view:current')
})
it('broadcast event has payload data', () => {
expect(server.plugins.socket.io.emit.firstCall.args[1]).to.equal({ dashboard })
})
})
})
})
================================================
FILE: packages/core/src/plugins/api/index.js
================================================
'use strict'
module.exports = require('./api')
================================================
FILE: packages/core/src/plugins/socket/index.js
================================================
'use strict'
module.exports = require('./socket')
================================================
FILE: packages/core/src/plugins/socket/socket.js
================================================
'use strict'
const socketio = require('socket.io')
const SocketPlugin = {
register: function (server, options, next) {
server.expose('io', socketio(server.listener))
next()
}
}
SocketPlugin.register.attributes = {
name: 'socket',
version: '1.0.0'
}
module.exports = SocketPlugin
================================================
FILE: packages/core/src/plugins/static/index.js
================================================
'use strict'
module.exports = require('./static')
================================================
FILE: packages/core/src/plugins/static/static.js
================================================
'use strict'
const { join } = require('path')
const AssetsPlugin = {
register: function (server, options, next) {
server.route({
method: 'GET',
path: '/assets/{param*}',
handler: {
directory: {
path: join(__dirname, '..', '..', '..', 'src/public')
}
}
})
next()
}
}
AssetsPlugin.register.attributes = {
name: 'assets',
version: '1.0.0'
}
module.exports = AssetsPlugin
================================================
FILE: packages/core/src/plugins/ui/handlers/dashboard/index.js
================================================
'use strict'
const dashboardLoader = require('../../../../dashboard/loader')
const { NotFoundError } = require('../../../../errors')
const Boom = require('boom')
async function buildViewModel (dashboard, server) {
const serverUrl = server.settings.app.serverUrl
const { name, html, js, css } = await dashboard.toRenderModel()
return {
serverUrl,
html,
name,
bundle: js.code,
map: js.map,
css
}
}
exports.handler = async function (request, reply) {
const { board } = request.params
const { server } = request
const { io } = server.plugins.socket
const { dashboards } = server.plugins.ui
try {
const dashboard = dashboardLoader.find(dashboards, board, io)
const model = await buildViewModel(dashboard, server)
return reply.view('dashboard', model)
} catch (e) {
console.error(e)
if (e instanceof NotFoundError) {
return reply.redirect('/')
}
return reply(Boom.boomify(e, { statusCode: 400 }))
}
}
================================================
FILE: packages/core/src/plugins/ui/handlers/index/index.js
================================================
'use strict'
const Path = require('path')
const { readdirSync } = require('fs')
const { internal } = require('boom')
const { join } = require('path')
function loadFromDisk (boards) {
const path = Path.join(process.cwd(), 'dashboards')
const files = readdirSync(path)
return files.reduce((curr, d) => {
const descriptor = require(join(path, d))
curr.add({
name: descriptor.name,
path: '/' + d.replace('.json', '') + '.dashboard'
})
return curr
}, boards)
}
function loadFromCache (boards, request) {
const { dashboards } = request.server.plugins.ui
return Object.keys(dashboards).reduce((curr, d) => {
curr.add(d)
return curr
}, boards)
}
exports.handler = function (request, reply) {
const defaultDashboard = process.env.DEFAULT_DASHBOARD
if (defaultDashboard) {
return reply.redirect(`/${defaultDashboard}.dashboard`)
}
try {
const boards = new Set()
loadFromDisk(boards)
loadFromCache(boards, request)
reply.view('listing', { boards: Array.from(boards) })
} catch (e) {
return reply(internal(e))
}
}
================================================
FILE: packages/core/src/plugins/ui/handlers/index/index.spec.js
================================================
'use strict'
const { expect } = require('code')
const { handler } = require('.')
describe('plugins/ui/handlers/index', () => {
context('Default Dashboard', () => {
before(() => {
process.env.DEFAULT_DASHBOARD = 'xxyyzz'
})
after(() => {
process.env.DEFAULT_DASHBOARD = undefined
})
it('Index points to default dashboard', done => {
const replyFunc = {
redirect: (uri) => {
expect(uri).to.equal('/xxyyzz.dashboard')
done()
}
}
handler({}, replyFunc)
})
})
})
================================================
FILE: packages/core/src/plugins/ui/index.js
================================================
'use strict'
module.exports = require('./ui')
================================================
FILE: packages/core/src/plugins/ui/ui.js
================================================
'use strict'
const Joi = require('joi')
const { handler: indexHandler } = require('./handlers/index')
const { handler: dashboardHandler } = require('./handlers/dashboard')
const DashboardPlugin = {
register: function (server, options, next) {
server.route({
method: 'GET',
path: '/',
handler: indexHandler
})
server.route({
method: 'GET',
path: '/{board}.dashboard',
config: {
validate: {
params: {
board: Joi.string().required().description('Board name')
}
},
cache: {
expiresIn: 15 * 60 * 1000,
privacy: 'private'
}
},
handler: dashboardHandler
})
server.route({
method: '*',
path: '/{p*}',
handler: indexHandler
})
server.expose('dashboards', {})
next()
}
}
DashboardPlugin.register.attributes = {
name: 'ui',
version: '1.0.0',
dependencies: ['socket']
}
module.exports = DashboardPlugin
================================================
FILE: packages/core/src/plugins/ui/ui.spec.js
================================================
'use strict'
const server = require('server')
const { expect } = require('code')
describe('core/plugins/ui', function () {
this.timeout(10000)
let app
const dashboard = 'simple'
before(async () => {
app = await server.register()
})
after(async () => {
await server.stop(app)
})
it('Loads dashboards into memory', () => {
return app.inject({ url: `/${dashboard}.dashboard` })
.then(({ statusCode }) => {
expect(statusCode).to.equal(200)
})
})
it('Builds dashboard cache', () => {
const cachedDashboards = Object.keys(app.plugins.ui.dashboards)
expect(cachedDashboards).to.only.include('simple')
})
})
================================================
FILE: packages/core/src/public/css/listing.css
================================================
body {
font-family: 'Ubuntu', sans-serif;
color: #fff;
background-color: #191919;
text-align: center;
}
.logo {
width: 25vw;
margin: 5vh;
}
path {
fill: #fff;
stroke: transparent;
}
rect {
fill: tomato;
}
.sub-page {
font-size: 20px;
}
ul {
list-style-type: none;
padding: 0;
}
ul > li {
margin: 5px 0;
}
a, a:visited, a:active {
color: #fff;
text-decoration: none;
}
a:hover {
color: tomato;
}
================================================
FILE: packages/core/src/public/css/style.css
================================================
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'),
url('/assets/fonts/ubuntu-v11-latin-regular.woff2') format('woff2'),
url('/assets/fonts/ubuntu-v11-latin-regular.woff') format('woff');
}
body {
font-family: 'Ubuntu', sans-serif;
color: #fff;
overflow: hidden;
background-color: #000;
}
.widget-container {
position: absolute;
display: block;
box-sizing: border-box;
background-color: #191919;
border: 3px solid #000;
border-radius: 10px;
}
.widget-container > *:first-child {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.widget-container > *:first-child {
height: 100%;
width: 100%;
}
================================================
FILE: packages/core/src/public/js/audio.js
================================================
'use strict'
var VUDASH = window.VUDASH
var Player = function () {
this.audio = new window.Audio()
}
Player.prototype.play = function (data) {
this.audio.src = data
this.audio.addEventListener('canplaythrough', function () {
this.play()
})
}
VUDASH.player = new Player()
================================================
FILE: packages/core/src/public/js/object-assign.polyfill.js
================================================
'use strict'
if (typeof Object.assign !== 'function') {
Object.assign = function (target, varArgs) {
'use strict'
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
var to = Object(target)
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index]
if (nextSource != null) {
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey]
}
}
}
}
return to
}
}
================================================
FILE: packages/core/src/resolver/index.js
================================================
'use strict'
module.exports = require('./resolver')
================================================
FILE: packages/core/src/resolver/resolver.js
================================================
'use strict'
const fs = require('fs')
const { join } = require('path')
function discoverNpmModule (moduleName) {
try {
return require.resolve(moduleName)
} catch (e) {
return null
}
}
function discoverLocalModule (moduleName) {
const path = join(process.cwd(), moduleName)
return fs.existsSync(path) ? path : null
}
function throwNotFound (moduleName) {
throw new Error(`Module ${moduleName} could not be resolved as an NPM module or a local module`)
}
function discover (moduleName) {
return [
discoverNpmModule,
discoverLocalModule,
throwNotFound
].find(method => {
return method(moduleName)
})(moduleName)
}
function resolve (moduleName) {
return require(discover(moduleName))
}
module.exports = {
discover,
resolve
}
================================================
FILE: packages/core/src/resolver/resolver.spec.js
================================================
'use strict'
const { discover } = require('.')
const { expect } = require('code')
const { resolve } = require('path')
describe('resolver', () => {
context('An npm module', () => {
const rootDir = resolve(process.cwd(), '../..')
it('returns path', () => {
expect(
discover('code')
).to.equal(
`${rootDir}/node_modules/code/lib/index.js`
)
})
})
context('A local module', () => {
it('returns path', () => {
expect(
discover('test/resources/widgets/example')
).to.equal(
`${process.cwd()}/test/resources/widgets/example`
)
})
})
})
================================================
FILE: packages/core/src/server.js
================================================
'use strict'
const requirePaths = require('app-module-path')
requirePaths.addPath(process.cwd())
requirePaths.addPath(`${process.cwd()}/node_modules`)
const Hapi = require('hapi')
const fs = require('fs')
const Path = require('path')
const chalk = require('chalk')
const unhandled = require('unhandled-rejection')
const id = require('./id-gen')
const rejectionEmitter = unhandled({
timeout: 5
})
rejectionEmitter.on('unhandledRejection', (error, promise) => {
console.error(error)
})
function register () {
const server = new Hapi.Server()
server.connection({ port: process.env.PORT || 3300 })
server.settings.app = { serverUrl: process.env.SERVER_URL || server.info.uri }
const apiKey = process.env['API_KEY'] || id()
return server.register([
require('vision'),
require('inert'),
require('./plugins/socket'),
require('./plugins/static'),
require('./plugins/ui'),
{
register: require('hapi-api-secret-key').plugin,
options: {
secrets: [ apiKey ]
}
},
require('./plugins/api')
])
.then(() => {
server.views({
engines: {
html: require('handlebars')
},
relativeTo: __dirname,
path: './views'
})
console.log(`Loading dashboards from ${chalk.blue(process.cwd())}`)
console.log(`Server ${chalk.green.bold('running')}`)
console.log(`Api key: ${chalk.magenta.bold(apiKey)}`)
console.log('Dashboards available:')
const dashboardDir = Path.join(process.cwd(), 'dashboards')
const boards = fs.readdirSync(dashboardDir)
for (let board of boards) {
const loaded = require(Path.join(dashboardDir, board))
const boardUrl = `${Path.basename(board, '.json')}.dashboard`
console.log(chalk.blue.bold(loaded.name), 'at', chalk.cyan.underline(`${server.settings.app.serverUrl}/${boardUrl}`))
}
return Promise.resolve(server)
})
}
function start (server) {
return server.start()
.then(() => {
if (process.env.BROWSER_SYNC) {
const bs = require('browser-sync').create()
bs.init({
open: false,
proxy: server.info.uri,
files: ['src/public/**/*.{js,css}']
})
}
return Promise.resolve()
})
}
function cleanup (server) {
const cache = Object.values(server.plugins.ui.dashboards)
cache.forEach(dashboard => {
dashboard.destroy()
})
}
function stop (server) {
server.stop({
timeout: 60000
}, () => {
cleanup(server)
return Promise.resolve()
})
}
module.exports = {
register,
start,
stop
}
================================================
FILE: packages/core/src/transform-loader/index.js
================================================
'use strict'
module.exports = require('./transform-loader')
================================================
FILE: packages/core/src/transform-loader/transform-loader.js
================================================
'use strict'
const resolver = require('../resolver')
const configValidator = require('../config-validator')
const Joi = require('joi')
function validate (widgetName, configuration) {
const transformerSchema = Joi.object({
transformer: Joi.string().required().label('Transformer module name'),
options: Joi.object().optional().label('Transform configuration')
})
const schema = Joi.array().items(
transformerSchema
).required()
return configValidator.validate(widgetName, schema, configuration)
}
exports.load = function (widgetName, configuration) {
validate(widgetName, configuration)
return configuration.map(({ transformer, options }) => {
const Constructor = resolver.resolve(transformer)
return new Constructor(options)
})
}
================================================
FILE: packages/core/src/transform-loader/transform-loader.spec.js
================================================
'use strict'
const { load } = require('.')
const { expect } = require('code')
const { stub } = require('sinon')
const resolver = require('../resolver')
const { ConfigurationError } = require('../errors')
describe('transform-loader', () => {
class SomeTransformer {}
class OtherTransformer {}
context('transformers with configuration', () => {
let transformers
const configuration = [
{
transformer: 'some-transformer',
options: {
foo: 'bar'
}
},
{
transformer: 'other-transformer',
options: {}
}
]
beforeEach(() => {
stub(resolver, 'resolve')
resolver.resolve
.withArgs('some-transformer')
.returns(SomeTransformer)
resolver.resolve
.withArgs('other-transformer')
.returns(OtherTransformer)
transformers = load('x', configuration)
})
afterEach(() => {
resolver.resolve.restore()
})
it('loads two transformers', () => {
expect(transformers.length).to.equal(2)
})
it('loads first transformer', () => {
expect(transformers[0]).to.be.an.instanceof(SomeTransformer)
})
it('loads second transformer', () => {
expect(transformers[1]).to.be.an.instanceof(OtherTransformer)
})
it('requests transformer modules', () => {
expect(resolver.resolve.callCount).to.equal(2)
})
})
context('empty transformer list', () => {
const configuration = []
it('loads no transformers', () => {
expect(
load('x', configuration)
)
.to.be.an.array()
.and.to.have.length(0)
})
})
describe('validation', () => {
context('missing transformer name', () => {
const configuration = [{}]
beforeEach(() => {
stub(resolver, 'resolve')
})
afterEach(() => {
resolver.resolve.restore()
})
it('throws a validation error', () => {
expect(() => {
load('x', configuration)
}).to.throw(ConfigurationError, /"Transformer module name" is required/)
})
})
})
})
================================================
FILE: packages/core/src/upper-camel/index.js
================================================
'use strict'
const { flow, camelCase, upperFirst } = require('lodash')
exports.upperCamel = function (name) {
const rename = flow([camelCase, upperFirst])
return rename(name)
}
================================================
FILE: packages/core/src/upper-camel/upper-camel.spec.js
================================================
'use strict'
const { upperCamel } = require('.')
const { expect } = require('code')
describe('upper-camel', () => {
const scenarios = [
{ input: 'some-name', output: 'SomeName' },
{ input: 'SomeName', output: 'SomeName' },
{ input: '@scope/package-name', output: 'ScopePackageName' },
{ input: '__some/name%&', output: 'SomeName' }
]
scenarios.forEach(({ input, output }) => {
it(`${input} becomes ${output}`, () => {
expect(upperCamel(input))
.to.exist()
.and.to.be.a.string()
.and.to.equal(output)
})
})
})
================================================
FILE: packages/core/src/views/dashboard.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<script src="/assets/js/object-assign.polyfill.js"></script>
<title>{{name}} Dashboard</title>
<link rel="stylesheet" type="text/css" href="/assets/css/style.css">
</head>
<body>
{{{html}}}
<script src="/socket.io/socket.io.js"></script>
<script>
window.VUDASH = {};
window.VUDASH.config = {};
window.VUDASH.config.serverUrl = '{{{serverUrl}}}';
{{{bundle}}}
</script>
<style>
{{{css}}}
</style>
<script src="/assets/js/audio.js"></script>
</body>
</html>
================================================
FILE: packages/core/src/views/listing.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Available Boards</title>
<style>
@import 'https://fonts.googleapis.com/css?family=Ubuntu';
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';
</style>
<link rel="stylesheet" type="text/css" href="/assets/css/listing.css" />
</head>
<body>
<svg class="logo" viewBox="0 0 176.08649 106.1699">
<g transform="translate(-24.377661,-51.731245)">
<g>
<path d="M 52.739091,115.62048 41.50574,141.57211 H 35.573938 L 24.377661,115.62048 h 6.487909 l 7.896712,18.53688 8.007933,-18.53688 z" />
<path d="m 66.978891,142.017 q -5.561064,0 -8.675261,-3.07712 -3.077122,-3.07713 -3.077122,-8.78649 v -14.53291 h 6.00595 v 14.31047 q 0,6.96987 5.783507,6.96987 2.817606,0 4.300557,-1.66832 1.48295,-1.70539 1.48295,-5.30155 v -14.31047 h 5.931803 v 14.53291 q 0,5.70936 -3.114196,8.78649 -3.077123,3.07712 -8.638188,3.07712 z" />
<path d="m 86.409602,128.55922 q -1.520025,0 -2.55809,-1.03806 -1.038066,-1.03807 -1.038066,-2.59517 0,-1.59417 1.038066,-2.55809 1.038065,-1.00099 2.55809,-1.00099 1.520024,0 2.558089,1.00099 1.038066,0.96392 1.038066,2.55809 0,1.5571 -1.038066,2.59517 -1.038065,1.03806 -2.558089,1.03806 z m 0,13.30948 q -1.520025,0 -2.55809,-1.03806 -1.038066,-1.03807 -1.038066,-2.59517 0,-1.59417 1.038066,-2.55809 1.038065,-1.00099 2.55809,-1.00099 1.520024,0 2.558089,1.00099 1.038066,0.96392 1.038066,2.55809 0,1.5571 -1.038066,2.59517 -1.038065,1.03806 -2.558089,1.03806 z" />
<path d="m 94.332957,115.62048 h 11.789453 q 4.22641,0 7.45183,1.63124 3.26249,1.59418 5.04203,4.523 1.81662,2.92883 1.81662,6.82158 0,3.89274 -1.81662,6.82157 -1.77954,2.92883 -5.04203,4.56007 -3.22542,1.59417 -7.45183,1.59417 H 94.332957 Z m 11.492863,21.02082 q 3.89275,0 6.19132,-2.15028 2.33565,-2.18735 2.33565,-5.89472 0,-3.70738 -2.33565,-5.85766 -2.29857,-2.18735 -6.19132,-2.18735 h -5.48691 v 16.09001 z" />
<path d="m 141.69122,136.01105 h -12.04897 l -2.29858,5.56106 h -6.15424 l 11.56701,-25.95163 h 5.93181 l 11.60408,25.95163 h -6.30254 z m -1.89076,-4.56007 -4.11519,-9.93577 -4.11519,9.93577 z" />
<path d="m 161.51178,142.017 q -3.07712,0 -5.96888,-0.81562 -2.85468,-0.8527 -4.59714,-2.18736 l 2.03905,-4.523 q 1.66832,1.22344 3.9669,1.96491 2.29857,0.74148 4.59714,0.74148 2.55809,0 3.78153,-0.74148 1.22343,-0.77855 1.22343,-2.03905 0,-0.92685 -0.74147,-1.52003 -0.70441,-0.63025 -1.85369,-1.00099 -1.11222,-0.37074 -3.04005,-0.81562 -2.9659,-0.7044 -4.85666,-1.40881 -1.89077,-0.7044 -3.2625,-2.2615 -1.33465,-1.55709 -1.33465,-4.15226 0,-2.2615 1.22343,-4.07811 1.22344,-1.85369 3.67031,-2.92883 2.48394,-1.07514 6.04302,-1.07514 2.48394,0 4.85666,0.59318 2.37272,0.59318 4.15226,1.7054 l -1.85368,4.56007 q -3.59616,-2.03906 -7.19231,-2.03906 -2.52102,0 -3.74446,0.81562 -1.18636,0.81563 -1.18636,2.15028 0,1.33466 1.37173,2.00199 1.40881,0.63025 4.26349,1.2605 2.9659,0.70441 4.85666,1.40881 1.89076,0.7044 3.22542,2.22442 1.37173,1.52003 1.37173,4.11519 0,2.22443 -1.26051,4.07812 -1.22344,1.81661 -3.70738,2.89175 -2.48394,1.07514 -6.04302,1.07514 z" />
<path d="m 200.46415,115.62048 v 25.95163 h -6.00595 v -10.64017 h -11.78946 v 10.64017 h -6.00595 v -25.95163 h 6.00595 v 10.23236 h 11.78946 v -10.23236 z" />
</g>
<g transform="matrix(0.69851314,0,0,0.7944649,208.52468,40.27109)">
<g>
<rect y="14.424998" x="-177.00626" height="18.785416" width="37.570831" />
<rect y="36.782299" x="-177.27081" height="43.65625" width="37.570835" />
</g>
<g transform="matrix(1,0,0,-1,41.539557,94.863551)">
<rect y="14.424998" x="-177.00626" height="18.785416" width="37.570831" />
<rect y="36.782299" x="-177.27081" height="43.65625" width="37.570835" />
</g>
</g>
<g>
<path d="m 60.611902,151.0302 h 2.492399 q 0.981562,0 1.741792,0.40418 0.76023,0.39455 1.183649,1.1644 0.423419,0.76023 0.423419,1.79953 0,1.0393 -0.423419,1.80916 -0.423419,0.76023 -1.183649,1.1644 -0.76023,0.39455 -1.741792,0.39455 h -2.492399 z m 2.492399,5.90863 q 1.12591,0 1.732169,-0.68325 0.615882,-0.69287 0.615882,-1.85727 0,-1.1644 -0.615882,-1.84765 -0.606259,-0.69286 -1.732169,-0.69286 h -1.578199 v 5.08103 z" />
<path d="m 70.974544,155.94764 h -2.579008 l -0.644752,1.81878 h -0.962316 l 2.415414,-6.73622 h 0.962316 l 2.415414,6.73622 h -0.962316 z m -0.288695,-0.82759 -1.000809,-2.80996 -1.000809,2.80996 z" />
<path d="m 75.336092,157.90114 q -0.596636,0 -1.212519,-0.13472 -0.615882,-0.1251 -0.991185,-0.29832 l 0.125101,-0.97194 q 0.481158,0.23096 1.087417,0.40417 0.606259,0.17322 1.202895,0.17322 0.673622,0 1.039302,-0.24058 0.375303,-0.2502 0.375303,-0.74098 0,-0.36568 -0.192463,-0.60626 -0.18284,-0.2502 -0.54852,-0.4138 -0.356057,-0.17321 -1.000809,-0.36568 -0.654375,-0.19246 -1.087418,-0.41379 -0.433042,-0.22134 -0.70249,-0.60626 -0.259826,-0.38493 -0.259826,-0.97194 0,-0.81797 0.625506,-1.31837 0.635129,-0.50041 1.780285,-0.50041 0.615882,0 1.164402,0.13472 0.558144,0.13473 0.97194,0.32719 l -0.09623,0.97194 q -0.538897,-0.31756 -1.039301,-0.46191 -0.500405,-0.14435 -1.039302,-0.14435 -0.625505,0 -1.000809,0.23096 -0.36568,0.23095 -0.36568,0.71211 0,0.32719 0.163594,0.5389 0.173217,0.21171 0.490781,0.36568 0.317565,0.14435 0.885331,0.31756 1.135533,0.34644 1.693677,0.8276 0.558143,0.47153 0.558143,1.30875 0,0.88533 -0.663998,1.38573 -0.663998,0.49078 -1.963125,0.49078 z" />
<path d="m 79.031236,151.0302 h 0.9142 v 2.7811 h 3.425846 v -2.7811 h 0.914201 v 6.73622 h -0.914201 v -3.08904 h -3.425846 v 3.08904 h -0.9142 z" />
<path d="m 89.194046,154.09999 q 0.635129,0.15397 0.971939,0.59664 0.336811,0.43304 0.336811,1.14516 0,0.89495 -0.57739,1.4146 -0.567766,0.51003 -1.568575,0.51003 h -2.540515 v -6.73622 h 2.126719 q 0.923823,0 1.424228,0.4138 0.510027,0.4138 0.510027,1.23177 0,0.48115 -0.173216,0.85646 -0.173217,0.3753 -0.510028,0.56776 z m -2.46353,-0.26944 h 1.097041 q 0.538897,0 0.817969,-0.21171 0.288695,-0.22134 0.288695,-0.77948 0,-0.55814 -0.288695,-0.76985 -0.279072,-0.22134 -0.817969,-0.22134 h -1.097041 z m 1.54933,3.10828 q 0.567766,0 0.894954,-0.27908 0.327187,-0.28869 0.327187,-0.84683 0,-1.15478 -1.222141,-1.15478 h -1.54933 v 2.28069 z" />
<path d="m 94.502725,157.90114 q -0.962316,0 -1.722546,-0.43304 -0.750607,-0.43304 -1.174026,-1.22214 -0.423419,-0.79872 -0.423419,-1.84765 0,-1.04892 0.423419,-1.83802 0.423419,-0.79872 1.174026,-1.23177 0.76023,-0.43304 1.722546,-0.43304 0.962316,0 1.712923,0.43304 0.76023,0.43305 1.183649,1.23177 0.423419,0.7891 0.423419,1.83802 0,1.04893 -0.423419,1.84765 -0.423419,0.7891 -1.183649,1.22214 -0.750607,0.43304 -1.712923,0.43304 z m 0,-0.88533 q 0.750607,0 1.279881,-0.33681 0.529274,-0.34643 0.789099,-0.93345 0.269449,-0.59663 0.269449,-1.34724 0,-0.7506 -0.269449,-1.33762 -0.259825,-0.59663 -0.789099,-0.93344 -0.529274,-0.34644 -1.279881,-0.34644 -0.750606,0 -1.27988,0.34644 -0.529274,0.33681 -0.798723,0.93344 -0.259825,0.58702 -0.259825,1.33762 0,0.75061 0.259825,1.34724 0.269449,0.58702 0.798723,0.93345 0.529274,0.33681 1.27988,0.33681 z" />
<path d="m 102.3438,155.94764 h -2.579011 l -0.644752,1.81878 h -0.962316 l 2.415409,-6.73622 h 0.96232 l 2.41541,6.73622 h -0.96231 z m -0.2887,-0.82759 -1.00081,-2.80996 -1.00081,2.80996 z" />
<path d="m 104.78071,151.0302 h 2.18446 q 1.09704,0 1.71292,0.51003 0.61589,0.50041 0.61589,1.53971 0,1.34724 -1.17403,1.89576 l 1.51084,2.79072 h -1.11629 l -1.36649,-2.62712 h -1.40498 v 2.62712 h -0.96232 z m 2.17484,3.29113 q 0.67362,0 1.02968,-0.32719 0.35605,-0.32719 0.35605,-0.9142 0,-0.58701 -0.35605,-0.90458 -0.34644,-0.32719 -1.02968,-0.32719 h -1.21252 v 2.47316 z" />
<path d="m 110.58844,151.0302 h 2.4924 q 0.98156,0 1.74179,0.40418 0.76023,0.39455 1.18365,1.1644 0.42342,0.76023 0.42342,1.79953 0,1.0393 -0.42342,1.80916 -0.42342,0.76023 -1.18365,1.1644 -0.76023,0.39455 -1.74179,0.39455 h -2.4924 z m 2.4924,5.90863 q 1.12591,0 1.73217,-0.68325 0.61588,-0.69287 0.61588,-1.85727 0,-1.1644 -0.61588,-1.84765 -0.60626,-0.69286 -1.73217,-0.69286 h -1.5782 v 5.08103 z" />
<path d="m 123.22531,155.94764 h -2.57901 l -0.64475,1.81878 h -0.96232 l 2.41542,-6.73622 h 0.96231 l 2.41542,6.73622 h -0.96232 z m -0.2887,-0.82759 -1.00081,-2.80996 -1.0008,2.80996 z" />
<path d="m 125.66223,151.0302 h 0.99118 l 3.31999,5.04254 v -5.04254 h 0.92383 v 6.73622 h -0.77948 l -3.54132,-5.32161 v 5.32161 h -0.9142 z" />
<path d="m 137.21122,151.0302 -2.3673,3.8204 v 2.91582 h -0.9142 v -2.91582 l -2.37692,-3.8204 h 1.0393 l 1.78991,2.92545 1.78991,-2.92545 z" />
<path d="m 139.21675,151.8578 h -1.96313 v -0.8276 h 4.84045 v 0.8276 h -1.96312 v 5.90862 h -0.9142 z" />
<path d="m 143.01023,151.0302 h 0.9142 v 2.7811 h 3.42585 v -2.7811 h 0.9142 v 6.73622 h -0.9142 v -3.08904 h -3.42585 v 3.08904 h -0.9142 z" />
<path d="m 149.79531,151.0302 h 0.9142 v 6.73622 h -0.9142 z" />
<path d="m 152.25749,151.0302 h 0.99118 l 3.31999,5.04254 v -5.04254 h 0.92383 v 6.73622 h -0.77948 l -3.54132,-5.32161 v 5.32161 h -0.9142 z" />
<path d="m 161.92997,157.90114 q -0.93345,0 -1.68405,-0.43304 -0.75061,-0.43304 -1.18365,-1.23176 -0.43305,-0.79873 -0.43305,-1.84765 0,-1.04893 0.44267,-1.83803 0.45229,-0.78909 1.24139,-1.22214 0.7891,-0.43304 1.79953,-0.43304 0.51003,0 1.00081,0.10586 0.5004,0.10585 0.89495,0.27907 l -0.0866,0.79872 q -0.93344,-0.35606 -1.78028,-0.35606 -0.83722,0 -1.40498,0.35606 -0.55815,0.34643 -0.83722,0.95269 -0.26945,0.59664 -0.26945,1.36649 0,1.21252 0.64476,1.94388 0.65437,0.73136 1.915,0.73136 0.26945,0 0.57739,-0.0385 0.30795,-0.0481 0.54852,-0.14435 v -1.67443 h -1.05854 v -0.82759 h 1.97275 v 3.00243 q -0.42342,0.23095 -1.02006,0.3753 -0.58701,0.13472 -1.27988,0.13472 z" />
</g>
</g>
</svg>
<h1>
<span class="sub-page">Available Boards</span>
</h1>
<ul>
{{#each boards}}
<li>
<a href="{{this.path}}">{{this.name}}</a>
</li>
{{/each}}
</ul>
</body>
</html>
================================================
FILE: packages/core/src/widget/history/history.js
================================================
'use strict'
class History {
constructor (size = 10) {
this.size = size
this.items = []
}
insert (entry) {
this.items.push(entry)
if (this.items.length > this.size) {
this.items.shift()
}
}
fetch () {
return this.items
}
}
exports.create = function (size) {
return new History(size)
}
================================================
FILE: packages/core/src/widget/history/history.spec.js
================================================
'use strict'
const { create } = require('.')
const { times } = require('lodash')
const { expect } = require('code')
describe('widget-binder/history', () => {
context('three items', () => {
let contents
beforeEach(() => {
const history = create()
history.insert('a')
history.insert('b')
history.insert('c')
contents = history.fetch()
})
it('has all entries', () => {
expect(contents[2]).to.equal('c')
})
it('is earliest first', () => {
expect(contents[0]).to.equal('a')
})
})
context('history overflow', () => {
let contents
beforeEach(() => {
const history = create(2)
history.insert('a')
history.insert('b')
history.insert('c')
contents = history.fetch()
})
it('has length of two', () => {
expect(contents.length).to.equal(2)
})
it('has all entries', () => {
expect(contents[1]).to.equal('c')
})
it('is earliest first', () => {
expect(contents[0]).to.equal('b')
})
})
context('size not specified', () => {
let contents
let history
beforeEach(() => {
history = create()
times(11, i => {
history.insert(i)
})
contents = history.fetch()
})
it('history size is 10', () => {
expect(history.size).to.equal(10)
})
it('is limited by size', () => {
expect(contents).to.have.length(10)
})
})
})
================================================
FILE: packages/core/src/widget/history/index.js
================================================
'use strict'
module.exports = require('./history')
================================================
FILE: packages/core/src/widget/index.js
================================================
'use strict'
const id = require('../id-gen')
const WidgetPosition = require('./widget-position')
const loader = require('./loader')
const renderer = require('./renderer')
const History = require('./history')
const validator = require('./validator')
class Widget {
constructor (widgetPath, config) {
const { position, background, options = {}, history } = config
this.id = id()
this.widgetPath = widgetPath
this.options = options
this.background = background
this.position = position
this.history = History.create(history)
}
register (emitter) {
const { widget, name, componentPath } = loader.load(this.widgetPath)
this.componentPath = componentPath
this.name = name
this.options = validator.validate(name, widget.validation, this.options)
this.widget = widget.register(this.options, emitter)
}
update (value) {
return this.widget.update ? this.widget.update(value) : value
}
toRenderModel (dashboardLayout) {
const {
id,
name,
options,
componentPath,
background,
position
} = this
const widgetPosition = new WidgetPosition(dashboardLayout, position)
return {
id,
name,
componentPath,
markup: renderer.renderHtml(id),
css: renderer.renderStyles(id, widgetPosition, background),
js: renderer.renderScript(id, name, options)
}
}
}
exports.create = function (widgetPath, config) {
return new Widget(widgetPath, config)
}
================================================
FILE: packages/core/src/widget/loader/index.js
================================================
'use strict'
const { reach } = require('hoek')
const { join } = require('path')
const resolver = require('../../resolver')
const { upperCamel } = require('../../upper-camel')
const { ConfigurationError } = require('../../errors')
const slash = require('slash')
const findRoot = require('find-root')
function discoverComponentPath (packagePath, packageJson) {
const relativeComponentPath = readComponentStanza(packageJson)
const absoluteComponentPath = join(packagePath, relativeComponentPath)
const localPath = slash(absoluteComponentPath)
if (!localPath) {
const packageName = reach(packageJson, 'name')
throw new ConfigurationError(`Cannot find component at ${localPath} for widget ${packageName}.`)
}
return localPath
}
function findPackageRoot (directory) {
const moduleEntrypoint = resolver.discover(directory)
return findRoot(moduleEntrypoint)
}
function loadPackageJson (packageRoot) {
return require(join(packageRoot, 'package.json'))
}
function readPackage (directory) {
const packageRoot = findPackageRoot(directory)
const widget = require(packageRoot)
const packageJson = loadPackageJson(packageRoot)
const componentPath = discoverComponentPath(packageRoot, packageJson)
const name = upperCamel(packageJson.name)
return { widget, name, componentPath }
}
function readComponentStanza (packageJson) {
const path = 'vudash.component'
const componentPath = reach(packageJson, path)
if (!componentPath) {
const packageName = reach(packageJson, 'name')
throw new ConfigurationError(`Widget ${packageName} is missing '${path}' in package.json`)
}
return componentPath
}
exports.load = function (pathOrDescriptor) {
const isPreParsed = typeof pathOrDescriptor === 'object'
if (isPreParsed) {
return pathOrDescriptor
}
return readPackage(pathOrDescriptor)
}
================================================
FILE: packages/core/src/widget/loader/loader.spec.js
================================================
'use strict'
const loader = require('.')
const { ComponentCompilationError } = require('errors')
const { expect } = require('code')
describe('widget/loader', () => {
context('Programmatic Config', () => {
const pkg = {
Module: { register: () => {} },
component: 'some-component',
name: 'vudash-some-component'
}
it('Parses Component', () => {
const resolved = loader.load(pkg)
expect(resolved).to.equal(pkg)
})
})
context('Vudash Metadata', () => {
it('fails to read component metadata', () => {
const message = "Widget vudash-widget-missing is missing 'vudash.component' in package.json"
const fn = () => { loader.load('test/resources/widgets/missing') }
expect(fn).to.throw(ComponentCompilationError, message)
})
})
context('Valid Component', () => {
let component
beforeEach(() => {
component = loader.load('test/resources/widgets/example')
})
it('returns registration method', () => {
expect(component.widget.register).to.be.a.function()
})
it('returns markup path', () => {
expect(component.componentPath).to.endWith('test/resources/widgets/example/markup.html')
})
it('returns registration method', () => {
expect(component.name).to.exist().and.to.equal('VudashWidgetExample')
})
})
})
================================================
FILE: packages/core/src/widget/renderer/index.js
================================================
'use strict'
exports.renderScript = function (id, name, config) {
return `
const widget_${id} = new ${name}({
target: document.getElementById("widget-container-${id}"),
data: { config: ${JSON.stringify(config)} }
});
socket.on('${id}:update', ($data) => {
if ($data.error) {
console.error('Widget "${id}" encountered error: ' + $data.error.message)
}
widget_${id}.update($data)
})
`.trim()
}
exports.renderHtml = function (id) {
return `<div id="widget-container-${id}" class="widget-container"></div>`
}
exports.renderStyles = function (id, widgetPosition, background) {
const { top, left, width, height } = widgetPosition
const rules = [
`top:${top}%`,
`left:${left}%`,
`width:${width}%`,
`height:${height}%`
]
if (background) {
rules.push(`background:${background}`)
}
return `#widget-container-${id}{${rules.join(';')}}`
}
================================================
FILE: packages/core/src/widget/renderer/renderer.spec.js
================================================
'use strict'
const renderer = require('.')
const WidgetPosition = require('../widget-position')
const Cheerio = require('cheerio')
const { expect } = require('code')
describe('widget/renderer', () => {
context('#renderScript()', () => {
const id = 'abc'
const config = { a: 'b' }
let rendered
before(() => {
rendered = renderer.renderScript(id, 'AbcWidget', config)
})
it('update method is rendered', () => {
expect(rendered).to.include("socket.on('abc:update', ($data) => {")
})
it('target is set correctly', () => {
expect(rendered).to.include('target: document.getElementById("widget-container-abc")')
})
it('error handling exists', () => {
expect(rendered).to.include('Widget "abc" encountered error')
})
it('default data contains config', () => {
expect(rendered).to.include('data: { config: {"a":"b"} }')
})
it('widget update method is called', () => {
expect(rendered).to.include('widget_abc.update($data)')
})
it('component is rendered', () => {
expect(rendered).to.startWith('const widget_abc = new AbcWidget')
})
})
context('#renderHtml()', () => {
let $
before(() => {
const widget = { id: 'xyz' }
const markup = renderer.renderHtml(widget.id)
$ = Cheerio.load(markup)
})
it('Has correct id', () => {
expect($('div').attr('id')).to.equal('widget-container-xyz')
})
it('Has correct class', () => {
expect($('div').hasClass('widget-container')).to.be.true()
})
})
describe('#renderStyles()', () => {
const widgetPosition = new WidgetPosition({
rows: 4,
columns: 5
}, {
x: 1, y: 2, w: 3, h: 4
})
const background = '#fff'
context('Css', () => {
let css
before(() => {
css = renderer.renderStyles('xyz', widgetPosition, background)
})
it('Renders widget id', () => {
expect(css).to.startWith('#widget-container-xyz{')
})
it('Renders background correctly', () => {
expect(css).to.contain('background:#fff')
})
it('Renders position correctly', () => {
expect(css).to.contain('left:20%;')
expect(css).to.contain('top:50%;')
expect(css).to.contain('width:60%;')
expect(css).to.contain('height:100%;')
})
})
context('No Background', () => {
let css
before(() => {
css = renderer.renderStyles('abc', widgetPosition, undefined)
})
it('Does not contain background rule', () => {
expect(css).not.to.contain('background:')
})
})
})
})
================================================
FILE: packages/core/src/widget/validator/index.js
================================================
'use strict'
module.exports = require('./validator')
================================================
FILE: packages/core/src/widget/validator/validator.js
================================================
'use strict'
const configValidator = require('../../config-validator')
const Joi = require('joi')
const defaultSchema = {
description: Joi.string().optional().description('Widget display label')
}
exports.validate = function (name, baseSchema, options) {
if (!baseSchema) { return options }
const schema = baseSchema.keys(defaultSchema)
return configValidator.validate(name, schema, options)
}
================================================
FILE: packages/core/src/widget/validator/validator.spec.js
================================================
'use strict'
const { expect } = require('code')
const { validate } = require('.')
describe('widget/validator', () => {
it('has no validation', () => {
const descriptor = { a: 'b' }
expect(
validate('xyz', null, descriptor)
).to.equal(descriptor)
})
})
================================================
FILE: packages/core/src/widget/widget-position/index.js
================================================
class WidgetPosition {
constructor (dashboardLayout, position) {
this.columns = dashboardLayout.columns
this.rows = dashboardLayout.rows
this.position = position
}
get rowHeight () {
return 100 / this.rows
}
get columnWidth () {
return 100 / this.columns
}
get height () {
return this.position.h * this.rowHeight
}
get width () {
return this.position.w * this.columnWidth
}
get left () {
return this.position.x * this.columnWidth
}
get top () {
return this.position.y * this.rowHeight
}
}
module.exports = WidgetPosition
================================================
FILE: packages/core/src/widget/widget-position/widget-position.spec.js
================================================
'use strict'
const WidgetPosition = require('.')
const { expect } = require('code')
describe('css-builder/widget-position', () => {
const dashboard = { columns: 5, rows: 4 }
it('Calculates first widget dimensions', () => {
const position = { x: 0, y: 0, w: 1, h: 1 }
const widgetPosition = new WidgetPosition(dashboard, position)
expect(widgetPosition.top).to.equal(0)
expect(widgetPosition.left).to.equal(0)
expect(widgetPosition.width).to.equal(20)
expect(widgetPosition.height).to.equal(25)
})
it('Calculates middle widget dimensions', () => {
const position = { x: 2, y: 4, w: 2, h: 1 }
const widgetPosition = new WidgetPosition(dashboard, position)
expect(widgetPosition.top).to.equal(100)
expect(widgetPosition.left).to.equal(40)
expect(widgetPosition.width).to.equal(40)
expect(widgetPosition.height).to.equal(25)
})
})
================================================
FILE: packages/core/src/widget/widget.spec.js
================================================
'use strict'
const { create } = require('.')
const { stub } = require('sinon')
const loader = require('./loader')
const { expect } = require('code')
const WidgetPosition = require('./widget-position')
const renderer = require('./renderer')
describe('widget', () => {
describe('#create()', () => {
it('has an auto-generated id', () => {
const widget = create('xyz', {})
expect(widget.id).to.exist()
})
it('has options', () => {
const widget = create('xyz', {})
expect(widget.options).to.equal({})
})
it('has options', () => {
const widget = create('xyz', {})
expect(widget.history).to.exist()
})
})
describe('#register()', () => {
let widget
const register = stub()
const options = { foo: 'bar' }
const dashboardEmitter = { xyz: 'abc' }
beforeEach(() => {
stub(loader, 'load').returns({
widget: { register }
})
widget = create('xyz', { options })
widget.register(dashboardEmitter)
})
afterEach(() => {
loader.load.restore()
})
it('widget is registered', () => {
expect(register.callCount).to.equal(1)
})
it('registers options with widget', () => {
expect(register.firstCall.args[0]).to.equal(options)
})
it('passes the emitter to the register method', () => {
expect(register.firstCall.args[1]).to.equal(dashboardEmitter)
})
})
describe('update', () => {
let widget
const data = { foo: 'bar' }
context('widget implements update function', () => {
const modified = { foo: 'bar' }
beforeEach(() => {
widget = create('xyz', {})
widget.widget = { update: stub().returns(modified) }
})
it('update calls widget update', () => {
widget.update(data)
expect(widget.widget.update.callCount).to.equal(1)
})
it('calls with update data', () => {
widget.update(data)
expect(widget.widget.update.firstCall.args[0]).to.equal(data)
})
it('returns modified data', () => {
expect(widget.update(data)).to.equal(modified)
})
})
context('widget does not implement update function', () => {
it('returns update data', () => {
widget = create('xyz', {})
widget.widget = {}
expect(widget.update(data)).to.equal(data)
})
})
})
describe('#toRenderModel()', () => {
let widget
let renderModel
const dashboardLayout = { rows: 1, columns: 1 }
const background = '#fff'
const options = { foo: 'bar' }
beforeEach(() => {
stub(renderer, 'renderHtml').returns('html')
stub(renderer, 'renderStyles').returns('css')
stub(renderer, 'renderScript').returns('js')
widget = create('xxx', { options, background })
widget.id = 'my-id'
widget.name = 'some-widget'
widget.componentPath = 'xyz'
renderModel = widget.toRenderModel(dashboardLayout)
})
afterEach(() => {
renderer.renderHtml.restore()
renderer.renderStyles.restore()
renderer.renderScript.restore()
})
describe('renders HTML', () => {
it('renders html', () => {
expect(renderer.renderHtml.callCount).to.equal(1)
})
it('passes id to html renderer', () => {
expect(renderer.renderHtml.firstCall.args[0]).to.equal(widget.id)
})
})
describe('renders styles', () => {
it('renders css', () => {
expect(renderer.renderStyles.callCount).to.equal(1)
})
it('passes id to style renderer', () => {
expect(renderer.renderStyles.firstCall.args[0]).to.equal(widget.id)
})
it('passes position to style renderer', () => {
expect(renderer.renderStyles.firstCall.args[1]).to.be.an.instanceOf(WidgetPosition)
})
it('passes background to style renderer', () => {
expect(renderer.renderStyles.firstCall.args[2]).to.equal(background)
})
})
describe('renders scripts', () => {
it('renders js', () => {
expect(renderer.renderScript.callCount).to.equal(1)
})
it('passes id to style renderer', () => {
expect(renderer.renderScript.firstCall.args[0]).to.equal(widget.id)
})
it('passes position to style renderer', () => {
expect(renderer.renderScript.firstCall.args[1]).to.equal(widget.name)
})
it('passes background to style renderer', () => {
expect(renderer.renderScript.firstCall.args[2]).to.equal(options)
})
})
describe('rendered model', () => {
it('outputs model for renderer', () => {
expect(renderModel).to.equal({
id: widget.id,
name: widget.name,
componentPath: widget.componentPath,
markup: 'html',
css: 'css',
js: 'js'
})
})
})
})
})
================================================
FILE: packages/core/src/widget-binder/index.js
================================================
'use strict'
module.exports = require('./widget-binder')
================================================
FILE: packages/core/src/widget-binder/widget-binder.js
================================================
'use strict'
const Widget = require('../widget')
const EventEmitter = require('events')
const transformLoader = require('../transform-loader')
const widgetDatasourceBinding = require('../widget-datasource-binding')
function fetchDatasource (datasources, datasourceId) {
const loopbackDatasource = {
emitter: new EventEmitter()
}
return datasources[datasourceId] || loopbackDatasource
}
exports.load = function (dashboard, widgets = [], datasources = {}) {
return widgets.reduce((curr, descriptor) => {
const {
name,
position,
background,
datasource: datasourceId,
transformations,
widget: widgetPath,
options
} = descriptor
const widget = Widget.create(widgetPath, { position, background, options })
const datasource = fetchDatasource(datasources, datasourceId)
widget.register(datasource.emitter)
const transforms = transformations ? transformLoader.load(name, transformations) : []
widgetDatasourceBinding.bindEvent(dashboard, widget, datasource, transforms)
datasource.emitter.on('plugin', (eventName, data) => {
dashboard.emit(eventName, data)
})
curr[widget.id] = widget
return curr
}, {})
}
================================================
FILE: packages/core/src/widget-binder/widget-binder.spec.js
================================================
'use strict'
const { load } = require('.')
const { expect } = require('code')
const { stub } = require('sinon')
const Widget = require('../widget')
const widgetDatasourceBinding = require('../widget-datasource-binding')
const EventEmitter = require('events')
const transformLoader = require('../transform-loader')
describe('widget-binder', () => {
context('no widgets specified', () => {
it('has empty widget list', () => {
const widgets = load({}, [], {})
expect(widgets).to.equal({})
})
})
context('widget specified without datasource', () => {
let result
let stubWidget
beforeEach(() => {
const widgets = [{}]
stubWidget = { register: stub() }
stub(Widget, 'create').returns(stubWidget)
stub(widgetDatasourceBinding, 'bindEvent')
result = load({}, widgets, {})
})
afterEach(() => {
widgetDatasourceBinding.bindEvent.restore()
Widget.create.restore()
})
it('returns a list of initialised widgets', () => {
const widgetIds = Object.keys(result)
expect(widgetIds).to.have.length(1)
})
it('widget is registered', () => {
expect(stubWidget.register.callCount).to.equal(1)
})
it('loopback datasource is wired up', () => {
expect(stubWidget.register.firstCall.args[0]).to.be.an.instanceof(EventEmitter)
})
})
context('widget and datasource specified', () => {
const datasources = { xyz: { emitter: new EventEmitter() } }
let stubWidget
beforeEach(() => {
const widgets = [{ datasource: 'xyz' }]
stubWidget = { register: stub() }
stub(Widget, 'create').returns(stubWidget)
stub(widgetDatasourceBinding, 'bindEvent')
load({}, widgets, datasources)
})
afterEach(() => {
widgetDatasourceBinding.bindEvent.restore()
Widget.create.restore()
})
it('widget is registered', () => {
expect(stubWidget.register.callCount).to.equal(1)
})
it('datasource is wired up', () => {
expect(stubWidget.register.firstCall.args[0]).to.equal(datasources.xyz.emitter)
})
})
context('loads transformations from configuration', () => {
let stubWidget
const widgets = [{
name: 'xyz',
transformations: [{
transformer: 'xxx',
options: {
foo: 'bar'
}
}]
}]
beforeEach(() => {
stubWidget = { register: stub() }
stub(Widget, 'create').returns(stubWidget)
stub(transformLoader, 'load').returns([{ baz: 'qux' }])
stub(widgetDatasourceBinding, 'bindEvent')
load({}, widgets, {})
})
afterEach(() => {
transformLoader.load.restore()
widgetDatasourceBinding.bindEvent.restore()
Widget.create.restore()
})
it('widget is registered', () => {
expect(stubWidget.register.callCount).to.equal(1)
})
it('transformations are loaded', () => {
expect(transformLoader.load.callCount).to.equal(1)
})
it('transformation loader gets widget name', () => {
expect(transformLoader.load.firstCall.args[0]).to.equal('xyz')
})
it('transformation loader gets transformation configuration', () => {
expect(transformLoader.load.firstCall.args[1]).to.equal(widgets[0].transformations)
})
it('calls bindEvent with transformer map', () => {
expect(widgetDatasourceBinding.bindEvent.firstCall.args[3]).to.equal([{ baz: 'qux' }])
})
})
context('plugin event fired', () => {
const dashboard = { emit: null }
beforeEach(() => {
const widgets = [{ datasource: 'xyz' }]
const datasources = { xyz: { emitter: new EventEmitter() } }
stub(Widget, 'create').returns({ register: stub() })
dashboard.emit = stub()
load(dashboard, widgets, datasources)
datasources.xyz.emitter.emit('plugin', 'xyzzy', 'abcde')
})
afterEach(() => {
Widget.create.restore()
})
it('plugin event from widget causes dashboard emit', () => {
expect(dashboard.emit.callCount).to.equal(1)
})
it('dashboard plugin event has correct name', () => {
expect(dashboard.emit.firstCall.args[0]).to.equal('xyzzy')
})
it('dashboard plugin event has correct data', () => {
expect(dashboard.emit.firstCall.args[1]).to.equal('abcde')
})
})
})
================================================
FILE: packages/core/src/widget-datasource-binding/index.js
================================================
'use strict'
module.exports = require('./widget-datasource-binding')
================================================
FILE: packages/core/src/widget-datasource-binding/widget-datasource-binding.js
================================================
'use strict'
const dashboardEvent = require('../dashboard-event')
function transform (data, transformers) {
return transformers.reduce((current, next) => {
return next.transform(current)
}, data)
}
exports.bindEvent = function (dashboard, widget, datasource, transformers) {
datasource.emitter.on('update', value => {
const event = `${widget.id}:update`
const hasTransformers = !!(transformers && transformers.length)
const transformed = hasTransformers ? transform(value, transformers) : value
const result = widget.update(transformed)
const payload = dashboardEvent.build(result)
dashboard.emit(event, payload)
})
}
================================================
FILE: packages/core/src/widget-datasource-binding/widget-datasource-binding.spec.js
================================================
'use strict'
const widgetDatasourceBinder = require('.')
const { stub } = require('sinon')
const { expect } = require('code')
const EventEmitter = require('events')
describe('widget-datasource-binding', () => {
context('datasource has emitter', () => {
const widget = {}
const dashboard = {}
it('event is bound', () => {
const datasource = { emitter: { on: stub() } }
widgetDatasourceBinder.bindEvent(dashboard, widget, datasource, [])
expect(datasource.emitter.on.callCount).to.equal(1)
})
})
context('widget emits data', () => {
const rawData = {
user: {
firstName: 'Alfred',
lastName: 'Wilks'
}
}
const widget = { id: 'abc', update: stub().returns(rawData) }
const dashboard = { emit: stub() }
before(() => {
const emitter = new EventEmitter()
const datasource = { emitter }
widgetDatasourceBinder.bindEvent(dashboard, widget, datasource, [])
dashboard.emit = stub()
datasource.emitter.emit('update')
})
it('dashboard event is emitted', () => {
expect(dashboard.emit.callCount).to.equal(1)
})
it('emitted event has widget id', () => {
expect(dashboard.emit.firstCall.args[0]).to.equal('abc:update')
})
it('emitted event has data', () => {
expect(dashboard.emit.firstCall.args[1].data).to.equal(rawData)
})
it('emitted event has metaData', () => {
expect(dashboard.emit.firstCall.args[1].meta).to.include('updated')
})
})
context('output is transformed', () => {
const widgetOutput = {
fullName: 'Alfred Wilks'
}
const transformedData = { foo: 'bar' }
const transformers = [{
transform: stub().returns(transformedData)
}]
const widget = {
update: stub()
.withArgs(transformedData)
.returns(widgetOutput)
}
const dashboard = { emit: stub() }
const datasource = { emitter: new EventEmitter() }
before(() => {
widgetDatasourceBinder.bindEvent(dashboard, widget, datasource, transformers)
dashboard.emit = stub()
datasource.emitter.emit('update')
})
it('dashboard event is emitted', () => {
expect(dashboard.emit.callCount).to.equal(1)
})
it('emitted event has transformed data', () => {
expect(
dashboard.emit.firstCall.args[1].data
).to.equal(widgetOutput)
})
})
})
================================================
FILE: packages/core/test/resources/widgets/broken/package.json
================================================
{
"name": "vudash-widget-broken",
"main": "widget.js"
}
================================================
FILE: packages/core/test/resources/widgets/broken/widget.js
================================================
'use strict'
exports.register = () => {
return {}
}
================================================
FILE: packages/core/test/resources/widgets/configurable/component.html
================================================
<h1>hi</h1>
================================================
FILE: packages/core/test/resources/widgets/configurable/package.json
================================================
{
"name": "vudash-widget-configurable",
"main": "widget.js",
"vudash": {
"component": "./component.html"
}
}
================================================
FILE: packages/core/test/resources/widgets/configurable/widget.js
================================================
'use strict'
const { Promise } = require('bluebird')
const defaults = {
foo: 'bar',
working: false
}
class ExampleWidget {
constructor (options) {
this.options = Object.assign({}, defaults, options)
}
update (data) {
return Promise.resolve(this.options)
}
}
exports.register = (options) => {
return new ExampleWidget(options)
}
================================================
FILE: packages/core/test/resources/widgets/example/markup.html
================================================
<h1>Hello</h1>
<script>
console.log('hello');
</script>
<style>
body { color: rgb(255,255,255); }
</style>
================================================
FILE: packages/core/test/resources/widgets/example/package.json
================================================
{
"name": "vudash-widget-example",
"main": "widget.js",
"vudash": {
"component": "./markup.html"
}
}
================================================
FILE: packages/core/test/resources/widgets/example/widget.js
================================================
'use strict'
const { Promise } = require('bluebird')
class ExampleWidget {
update (data) {
return Promise.resolve({x: 'y'})
}
}
exports.register = () => {
return new ExampleWidget()
}
================================================
FILE: packages/core/test/resources/widgets/missing/package.json
================================================
{
"name": "vudash-widget-missing",
"main": "widget.js"
}
================================================
FILE: packages/core/test/resources/widgets/missing/widget.js
================================================
'use strict'
exports.register = () => {
return {}
}
================================================
FILE: packages/core/test/util/dashboard.builder.js
================================================
'use strict'
const WidgetBuilder = require('./widget.builder')
class DashboardBuilder {
constructor () {
this.overrides = {
widgets: []
}
}
withName (name = 'Some Dashboard') {
this.overrides.name = name
return this
}
addDatasource (moduleName, options) {
this.overrides.datasources = this.datasources || {}
this.overrides.datasources[moduleName] = {
module: moduleName,
options
}
return this
}
addWidget (widget = WidgetBuilder.create().build()) {
this.overrides.widgets.push(widget)
return this
}
build () {
return Object.assign({}, {
layout: {
rows: 4,
columns: 5
}
}, this.overrides)
}
}
module.exports = {
create: () => { return new DashboardBuilder() }
}
================================================
FILE: packages/core/test/util/datasource.builder.js
================================================
'use strict'
class Datasource {
constructor (options) {
this.options = options
}
fetch () {
return this.options
}
}
class DatasourceBuilder {
constructor () {
this.ds = Datasource
}
build () {
return this.ds
}
}
module.exports = {
create: () => { return new DatasourceBuilder() }
}
================================================
FILE: packages/core/test/util/widget.builder.js
================================================
'use strict'
const { Promise } = require('bluebird')
class WidgetBuilder {
constructor () {
this.widget = this._createWidgetModule()
this.position = { x: 0, y: 0, w: 0, h: 0 }
}
_createWidgetModule (internals = { schedule: 1000, job: () => { return Promise.resolve({}) } }) {
const Module = class MyWidget {
register (options) {
return Object.assign({}, internals, { config: options })
}
}
const component = './src/component.html'
const name = 'VudashMyWidget'
return {
Module,
component,
name
}
}
withJob (job = Promise.resolve({}), schedule = 1000) {
this.widget = this._createWidgetModule({ job, schedule })
return this
}
withOptions (options = {}) {
this.options = options
return this
}
withWidget (widget) {
this.widget = widget
return this
}
build () {
return {
position: this.position,
widget: this.widget,
options: this.options
}
}
}
module.exports = {
create: () => { return new WidgetBuilder() }
}
================================================
FILE: packages/datasource-google-sheets/datasource.js
================================================
'use strict'
const GoogleSheetsTransport = require('./src/google-sheets-transport')
const { validation } = require('./src/config-validator')
exports.validation = validation
exports.register = function (options) {
return new GoogleSheetsTransport(options)
}
================================================
FILE: packages/datasource-google-sheets/datasource.spec.js
================================================
'use strict'
const datasource = require('./datasource')
const { expect } = require('code')
describe('datasource-google-sheets/datasource', () => {
it('has register method', () => {
expect(datasource.register).to.exist().and.to.be.a.function()
})
it('exports validation', () => {
expect(datasource.validation).to.exist()
})
})
================================================
FILE: packages/datasource-google-sheets/package.json
================================================
{
"name": "@vudash/datasource-google-sheets",
"version": "9.9.0",
"description": "Google Sheets datasource for Vudash",
"main": "datasource.js",
"scripts": {
"lint": "../../node_modules/.bin/standard",
"test": "NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js",
"link": "npm link"
},
"author": "Antony Jones",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/vudash/vudash"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"bluebird": "^3.4.7",
"hoek": "^4.1.0",
"joi": "^10.2.1",
"spreadsheet-to-json": "^1.0.5"
},
"standard": {
"globals": [
"describe",
"context",
"it",
"before",
"after",
"beforeEach",
"afterEach"
]
}
}
================================================
FILE: packages/datasource-google-sheets/src/config-validator/config-validator.spec.js
================================================
'use strict'
const Joi = require('joi')
const configUtil = require('../../test/config.util')
const sinon = require('sinon')
const validator = require('.')
const { expect } = require('code')
describe('datasource-google-sheets.config-validator', () => {
const sandbox = sinon.sandbox.create()
afterEach(() => {
sandbox.restore()
})
it('With invalid config', () => {
const { error } = Joi.validate({}, validator.validation)
expect(error).to.be.an.error(/fails because/)
})
it('With valid single-cell config', () => {
const { error } = Joi.validate(configUtil.getSingleCellConfig(), validator.validation)
expect(error).not.to.exist()
})
it('With valid range config', () => {
const { error } = Joi.validate(configUtil.getRangeConfig(), validator.validation)
expect(error).not.to.exist()
})
it('Invalid credentials file', () => {
const credentials = 'xxx:yyy'
const config = configUtil.getSingleCellConfig(credentials)
const { error } = Joi.validate(config, validator.validation)
expect(error).to.be.an.error()
})
})
================================================
FILE: packages/datasource-google-sheets/src/config-validator/index.js
==
gitextract_gganvaua/
├── .codeclimate.json
├── .gitignore
├── .travis.yml
├── README.MD
├── docs/
│ ├── .nojekyll
│ ├── README.md
│ ├── _coverpage.md
│ ├── api/
│ │ └── README.md
│ ├── datasources/
│ │ └── README.md
│ ├── developers/
│ │ └── README.md
│ ├── index.html
│ ├── transformers/
│ │ └── README.md
│ └── widgets/
│ └── README.md
├── lerna.json
├── package.json
├── packages/
│ ├── core/
│ │ ├── README.md
│ │ ├── app.js
│ │ ├── bin/
│ │ │ └── vudash.js
│ │ ├── dashboards/
│ │ │ ├── simple.json
│ │ │ └── template.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli/
│ │ │ │ ├── create.js
│ │ │ │ ├── help.js
│ │ │ │ └── logo.js
│ │ │ ├── config-validator/
│ │ │ │ ├── config-validator.spec.js
│ │ │ │ └── index.js
│ │ │ ├── dashboard/
│ │ │ │ ├── bundler/
│ │ │ │ │ └── index.js
│ │ │ │ ├── compiler/
│ │ │ │ │ ├── compiler.spec.js
│ │ │ │ │ ├── configuration-builder/
│ │ │ │ │ │ ├── configuration-builder.js
│ │ │ │ │ │ ├── configuration-builder.spec.js
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── dashboard.js
│ │ │ │ ├── dashboard.spec.js
│ │ │ │ ├── emitter/
│ │ │ │ │ ├── emitter.js
│ │ │ │ │ ├── emitter.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ ├── loader/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── loader.js
│ │ │ │ │ └── loader.spec.js
│ │ │ │ ├── parser/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── parser.js
│ │ │ │ │ └── parser.spec.js
│ │ │ │ ├── renderer/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── renderer.js
│ │ │ │ │ └── renderer.spec.js
│ │ │ │ └── schema/
│ │ │ │ ├── index.js
│ │ │ │ ├── schema.js
│ │ │ │ └── schema.spec.js
│ │ │ ├── dashboard-event/
│ │ │ │ ├── dashboard-event.js
│ │ │ │ └── index.js
│ │ │ ├── datasource/
│ │ │ │ ├── datasource.spec.js
│ │ │ │ ├── dummy-datasource/
│ │ │ │ │ ├── dummy-datasource.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ ├── locator/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── locator.spec.js
│ │ │ │ └── validator/
│ │ │ │ ├── index.js
│ │ │ │ └── validator.spec.js
│ │ │ ├── datasource-binder/
│ │ │ │ ├── datasource-binder.js
│ │ │ │ ├── datasource-binder.spec.js
│ │ │ │ ├── datasource-emitter.js
│ │ │ │ └── index.js
│ │ │ ├── datasource-loader/
│ │ │ │ ├── datasource-loader.js
│ │ │ │ ├── datasource-loader.spec.js
│ │ │ │ └── index.js
│ │ │ ├── errors/
│ │ │ │ ├── configuration.error.js
│ │ │ │ ├── index.js
│ │ │ │ ├── not-found.error.js
│ │ │ │ ├── plugin-registration.error.js
│ │ │ │ └── widget-registration.error.js
│ │ │ ├── id-gen/
│ │ │ │ ├── id-gen.js
│ │ │ │ ├── id-gen.spec.js
│ │ │ │ └── index.js
│ │ │ ├── plugins/
│ │ │ │ ├── api/
│ │ │ │ │ ├── api.js
│ │ │ │ │ ├── handlers/
│ │ │ │ │ │ ├── dashboards/
│ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ ├── put.js
│ │ │ │ │ │ │ └── put.spec.js
│ │ │ │ │ │ └── view/
│ │ │ │ │ │ └── current/
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── put.js
│ │ │ │ │ │ └── put.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── socket/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── socket.js
│ │ │ │ ├── static/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── static.js
│ │ │ │ └── ui/
│ │ │ │ ├── handlers/
│ │ │ │ │ ├── dashboard/
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── index.spec.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ui.js
│ │ │ │ └── ui.spec.js
│ │ │ ├── public/
│ │ │ │ ├── css/
│ │ │ │ │ ├── listing.css
│ │ │ │ │ └── style.css
│ │ │ │ └── js/
│ │ │ │ ├── audio.js
│ │ │ │ └── object-assign.polyfill.js
│ │ │ ├── resolver/
│ │ │ │ ├── index.js
│ │ │ │ ├── resolver.js
│ │ │ │ └── resolver.spec.js
│ │ │ ├── server.js
│ │ │ ├── transform-loader/
│ │ │ │ ├── index.js
│ │ │ │ ├── transform-loader.js
│ │ │ │ └── transform-loader.spec.js
│ │ │ ├── upper-camel/
│ │ │ │ ├── index.js
│ │ │ │ └── upper-camel.spec.js
│ │ │ ├── views/
│ │ │ │ ├── dashboard.html
│ │ │ │ └── listing.html
│ │ │ ├── widget/
│ │ │ │ ├── history/
│ │ │ │ │ ├── history.js
│ │ │ │ │ ├── history.spec.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ ├── loader/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── loader.spec.js
│ │ │ │ ├── renderer/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── renderer.spec.js
│ │ │ │ ├── validator/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── validator.js
│ │ │ │ │ └── validator.spec.js
│ │ │ │ ├── widget-position/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── widget-position.spec.js
│ │ │ │ └── widget.spec.js
│ │ │ ├── widget-binder/
│ │ │ │ ├── index.js
│ │ │ │ ├── widget-binder.js
│ │ │ │ └── widget-binder.spec.js
│ │ │ └── widget-datasource-binding/
│ │ │ ├── index.js
│ │ │ ├── widget-datasource-binding.js
│ │ │ └── widget-datasource-binding.spec.js
│ │ └── test/
│ │ ├── resources/
│ │ │ └── widgets/
│ │ │ ├── broken/
│ │ │ │ ├── package.json
│ │ │ │ └── widget.js
│ │ │ ├── configurable/
│ │ │ │ ├── component.html
│ │ │ │ ├── package.json
│ │ │ │ └── widget.js
│ │ │ ├── example/
│ │ │ │ ├── markup.html
│ │ │ │ ├── package.json
│ │ │ │ └── widget.js
│ │ │ └── missing/
│ │ │ ├── package.json
│ │ │ └── widget.js
│ │ └── util/
│ │ ├── dashboard.builder.js
│ │ ├── datasource.builder.js
│ │ └── widget.builder.js
│ ├── datasource-google-sheets/
│ │ ├── datasource.js
│ │ ├── datasource.spec.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── config-validator/
│ │ │ │ ├── config-validator.spec.js
│ │ │ │ └── index.js
│ │ │ └── google-sheets-transport/
│ │ │ ├── google-sheets-transport.spec.js
│ │ │ └── index.js
│ │ └── test/
│ │ ├── config.util.js
│ │ ├── example.credentials.test.json
│ │ └── example.invalid-credentials.test.json
│ ├── datasource-random/
│ │ ├── datasource.js
│ │ ├── datasource.spec.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── datasource-validation/
│ │ │ └── index.js
│ │ └── random-transport/
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── datasource-rest/
│ │ ├── datasource.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── datasource-validation/
│ │ │ ├── datasource-validation.js
│ │ │ ├── datasource-validation.spec.js
│ │ │ └── index.js
│ │ └── rest-transport/
│ │ ├── index.js
│ │ └── rest-transport.spec.js
│ ├── datasource-value/
│ │ ├── datasource.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── datasource-validation/
│ │ │ ├── datasource-validation.spec.js
│ │ │ └── index.js
│ │ └── value-transport/
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── transformer-jq/
│ │ ├── index.js
│ │ ├── lib/
│ │ │ ├── transformer.js
│ │ │ └── transformer.spec.js
│ │ └── package.json
│ ├── transformer-map/
│ │ ├── package.json
│ │ └── transformer.js
│ ├── widget-chart/
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ ├── chart-options.js
│ │ │ └── markup.html
│ │ └── server/
│ │ ├── index.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-ci/
│ │ ├── .gitignore
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── build-status.enum.js
│ │ ├── client/
│ │ │ └── markup.html
│ │ ├── engines/
│ │ │ ├── circleci/
│ │ │ │ ├── circleci.spec.js
│ │ │ │ └── index.js
│ │ │ ├── factory.js
│ │ │ └── travis/
│ │ │ ├── index.js
│ │ │ └── travis.spec.js
│ │ └── server/
│ │ ├── index.js
│ │ ├── validation.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-gauge/
│ │ ├── .gitignore
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── markup.html
│ │ └── server/
│ │ ├── index.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-health/
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── component.html
│ │ └── server/
│ │ ├── index.js
│ │ └── widget.js
│ ├── widget-progress/
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── component.html
│ │ └── server/
│ │ ├── index.js
│ │ └── widget.js
│ ├── widget-statistic/
│ │ ├── .gitignore
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── markup.html
│ │ └── server/
│ │ ├── index.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ ├── widget-status/
│ │ ├── README.MD
│ │ ├── package.json
│ │ └── src/
│ │ ├── client/
│ │ │ └── markup.html
│ │ ├── health-status.js
│ │ ├── providers/
│ │ │ ├── github/
│ │ │ │ ├── github.spec.js
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── statuspageio/
│ │ │ ├── index.js
│ │ │ └── statuspageio.spec.js
│ │ └── server/
│ │ ├── index.js
│ │ ├── validator.js
│ │ └── widget.js
│ └── widget-time/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ └── src/
│ ├── client/
│ │ └── markup.html
│ ├── server/
│ │ ├── alarms.js
│ │ ├── index.js
│ │ ├── validation.js
│ │ ├── widget.js
│ │ └── widget.spec.js
│ └── time/
│ ├── index.js
│ └── time.spec.js
└── test/
└── unit.lab.js
SYMBOL INDEX (185 symbols across 51 files)
FILE: packages/core/src/dashboard/dashboard.js
function isWidgetEvent (line 13) | function isWidgetEvent (eventId) {
class Dashboard (line 17) | class Dashboard {
method constructor (line 18) | constructor (json, io) {
method emit (line 32) | emit (eventId, data, historical) {
method loadDatasources (line 49) | loadDatasources () {
method loadWidgets (line 55) | loadWidgets () {
method destroy (line 61) | destroy () {
method toRenderModel (line 75) | async toRenderModel () {
FILE: packages/core/src/dashboard/emitter/emitter.js
class Emitter (line 5) | class Emitter {
method constructor (line 6) | constructor (socketio, room) {
method clientJoinHandler (line 15) | clientJoinHandler (socket) {
method emit (line 27) | emit (event, data, historical) {
FILE: packages/core/src/dashboard/loader/loader.js
function load (line 8) | function load (cache, name, io) {
function add (line 19) | function add (cache, name, io, descriptor) {
function find (line 28) | function find (cache, name, io) {
function has (line 32) | function has (cache, name) {
FILE: packages/core/src/dashboard/parser/parser.js
function get (line 5) | function get (directive) {
function parse (line 13) | function parse (json) {
FILE: packages/core/src/dashboard/renderer/renderer.js
function renderWidgets (line 7) | function renderWidgets (widgets, layout) {
FILE: packages/core/src/datasource-binder/datasource-binder.js
function fetchFunction (line 9) | function fetchFunction () {
FILE: packages/core/src/datasource-binder/datasource-emitter.js
class DatasourceEmitter (line 5) | class DatasourceEmitter extends EventEmitter {}
FILE: packages/core/src/datasource-loader/datasource-loader.js
function resolveDatasource (line 9) | function resolveDatasource (path) {
function parseConfiguration (line 13) | function parseConfiguration (datasourceName, validation, options) {
function register (line 17) | function register (registrationFn, configuration) {
function initialise (line 25) | function initialise (name, registrationFn, configuration = {}, schedule) {
FILE: packages/core/src/datasource/datasource.spec.js
function fn (line 55) | function fn () { return loaded.fetch() }
FILE: packages/core/src/datasource/dummy-datasource/index.js
class DummyDatasource (line 3) | class DummyDatasource {
method constructor (line 4) | constructor (options) {
method fetch (line 8) | fetch () {
FILE: packages/core/src/datasource/index.js
function extendValidation (line 13) | function extendValidation (validation) {
function loadValidOptions (line 19) | function loadValidOptions (widgetName, validation, options) {
FILE: packages/core/src/plugins/ui/handlers/dashboard/index.js
function buildViewModel (line 7) | async function buildViewModel (dashboard, server) {
FILE: packages/core/src/plugins/ui/handlers/index/index.js
function loadFromDisk (line 8) | function loadFromDisk (boards) {
function loadFromCache (line 21) | function loadFromCache (boards, request) {
FILE: packages/core/src/resolver/resolver.js
function discoverNpmModule (line 6) | function discoverNpmModule (moduleName) {
function discoverLocalModule (line 14) | function discoverLocalModule (moduleName) {
function throwNotFound (line 19) | function throwNotFound (moduleName) {
function discover (line 23) | function discover (moduleName) {
function resolve (line 33) | function resolve (moduleName) {
FILE: packages/core/src/server.js
function register (line 22) | function register () {
function start (line 68) | function start (server) {
function cleanup (line 85) | function cleanup (server) {
function stop (line 92) | function stop (server) {
FILE: packages/core/src/transform-loader/transform-loader.js
function validate (line 7) | function validate (widgetName, configuration) {
FILE: packages/core/src/transform-loader/transform-loader.spec.js
class SomeTransformer (line 10) | class SomeTransformer {}
class OtherTransformer (line 11) | class OtherTransformer {}
FILE: packages/core/src/widget-binder/widget-binder.js
function fetchDatasource (line 8) | function fetchDatasource (datasources, datasourceId) {
FILE: packages/core/src/widget-datasource-binding/widget-datasource-binding.js
function transform (line 5) | function transform (data, transformers) {
FILE: packages/core/src/widget/history/history.js
class History (line 3) | class History {
method constructor (line 4) | constructor (size = 10) {
method insert (line 9) | insert (entry) {
method fetch (line 16) | fetch () {
FILE: packages/core/src/widget/index.js
class Widget (line 10) | class Widget {
method constructor (line 11) | constructor (widgetPath, config) {
method register (line 22) | register (emitter) {
method update (line 31) | update (value) {
method toRenderModel (line 35) | toRenderModel (dashboardLayout) {
FILE: packages/core/src/widget/loader/index.js
function discoverComponentPath (line 11) | function discoverComponentPath (packagePath, packageJson) {
function findPackageRoot (line 22) | function findPackageRoot (directory) {
function loadPackageJson (line 27) | function loadPackageJson (packageRoot) {
function readPackage (line 31) | function readPackage (directory) {
function readComponentStanza (line 41) | function readComponentStanza (packageJson) {
FILE: packages/core/src/widget/widget-position/index.js
class WidgetPosition (line 1) | class WidgetPosition {
method constructor (line 2) | constructor (dashboardLayout, position) {
method rowHeight (line 8) | get rowHeight () {
method columnWidth (line 12) | get columnWidth () {
method height (line 16) | get height () {
method width (line 20) | get width () {
method left (line 24) | get left () {
method top (line 28) | get top () {
FILE: packages/core/test/resources/widgets/configurable/widget.js
class ExampleWidget (line 9) | class ExampleWidget {
method constructor (line 10) | constructor (options) {
method update (line 14) | update (data) {
FILE: packages/core/test/resources/widgets/example/widget.js
class ExampleWidget (line 5) | class ExampleWidget {
method update (line 6) | update (data) {
FILE: packages/core/test/util/dashboard.builder.js
class DashboardBuilder (line 5) | class DashboardBuilder {
method constructor (line 6) | constructor () {
method withName (line 12) | withName (name = 'Some Dashboard') {
method addDatasource (line 17) | addDatasource (moduleName, options) {
method addWidget (line 26) | addWidget (widget = WidgetBuilder.create().build()) {
method build (line 31) | build () {
FILE: packages/core/test/util/datasource.builder.js
class Datasource (line 3) | class Datasource {
method constructor (line 4) | constructor (options) {
method fetch (line 8) | fetch () {
class DatasourceBuilder (line 13) | class DatasourceBuilder {
method constructor (line 14) | constructor () {
method build (line 18) | build () {
FILE: packages/core/test/util/widget.builder.js
class WidgetBuilder (line 5) | class WidgetBuilder {
method constructor (line 6) | constructor () {
method _createWidgetModule (line 11) | _createWidgetModule (internals = { schedule: 1000, job: () => { return...
method withJob (line 27) | withJob (job = Promise.resolve({}), schedule = 1000) {
method withOptions (line 32) | withOptions (options = {}) {
method withWidget (line 37) | withWidget (widget) {
method build (line 42) | build () {
FILE: packages/datasource-google-sheets/src/config-validator/index.js
class ConfigValidator (line 5) | class ConfigValidator {
method inlineCredentialsValidation (line 6) | get inlineCredentialsValidation () {
method fileCredentialsValidation (line 21) | get fileCredentialsValidation () {
method validation (line 25) | get validation () {
method columnSchema (line 38) | get columnSchema () {
method rowSchema (line 45) | get rowSchema () {
FILE: packages/datasource-google-sheets/src/google-sheets-transport/index.js
class GoogleSheetsTransport (line 9) | class GoogleSheetsTransport {
method constructor (line 10) | constructor (options) {
method widgetValidation (line 20) | static get widgetValidation () {
method loadCredentialsFromDisk (line 24) | loadCredentialsFromDisk () {
method validate (line 41) | validate (schema, credentials) {
method fetch (line 47) | fetch () {
method _extractCellData (line 59) | _extractCellData (data) {
method _toMatrix (line 67) | _toMatrix (tab) {
method _extractCellValue (line 83) | _extractCellValue (tab) {
FILE: packages/datasource-google-sheets/test/config.util.js
function getConfig (line 3) | function getConfig (credentialsOverride) {
FILE: packages/datasource-random/src/random-transport/index.js
class RandomTransport (line 6) | class RandomTransport {
method constructor (line 7) | constructor (options, seed = new Date().getTime()) {
method prepareOptions (line 12) | prepareOptions () {
method validateMethod (line 30) | validateMethod (method) {
method fetch (line 36) | fetch () {
FILE: packages/datasource-rest/src/rest-transport/index.js
method prepareRequest (line 8) | prepareRequest (config) {
class RestTransport (line 25) | class RestTransport {
method constructor (line 26) | constructor (options) {
method fetch (line 30) | fetch () {
FILE: packages/datasource-value/src/value-transport/index.js
class ValueTransport (line 5) | class ValueTransport {
method constructor (line 6) | constructor (options) {
method fetch (line 10) | fetch () {
FILE: packages/transformer-jq/lib/transformer.js
class JqTransformer (line 6) | class JqTransformer {
method constructor (line 7) | constructor (transformation) {
method transform (line 11) | transform (data) {
FILE: packages/transformer-map/transformer.js
class MapTransformer (line 5) | class MapTransformer {
method constructor (line 6) | constructor (schema) {
method transform (line 10) | transform (data) {
FILE: packages/widget-chart/src/server/widget.js
class ChartWidget (line 9) | class ChartWidget {
method constructor (line 10) | constructor (options) {
method update (line 14) | update (data) {
FILE: packages/widget-ci/src/build-status.enum.js
class BuildStatus (line 1) | class BuildStatus {
method passed (line 2) | static get passed () {
method failed (line 6) | static get failed () {
method unknown (line 10) | static get unknown () {
method queued (line 14) | static get queued () {
method running (line 18) | static get running () {
FILE: packages/widget-ci/src/engines/circleci/index.js
class CircleCIEngine (line 6) | class CircleCIEngine {
method constructor (line 7) | constructor (options) {
method fetchBuildStatus (line 25) | fetchBuildStatus () {
FILE: packages/widget-ci/src/engines/factory.js
class Factory (line 4) | class Factory {
method constructor (line 5) | constructor () {
method getEngine (line 12) | getEngine (engine) {
method availableEngines (line 16) | get availableEngines () {
FILE: packages/widget-ci/src/engines/travis/index.js
class TravisEngine (line 7) | class TravisEngine {
method constructor (line 8) | constructor (options) {
method fetchBuildStatus (line 23) | fetchBuildStatus () {
FILE: packages/widget-ci/src/server/widget.js
class CiWidget (line 8) | class CiWidget {
method constructor (line 9) | constructor (config, emitter) {
method run (line 28) | run () {
method destroy (line 42) | destroy () {
FILE: packages/widget-gauge/src/server/widget.js
class GaugeWidget (line 10) | class GaugeWidget {
method constructor (line 11) | constructor (options) {
method update (line 15) | update (value) {
FILE: packages/widget-health/src/server/widget.js
class HealthWidget (line 3) | class HealthWidget {
method constructor (line 4) | constructor (options, emitter) {
method run (line 14) | run () {
method destroy (line 19) | destroy () {
FILE: packages/widget-progress/src/server/widget.js
class ProgressWidget (line 3) | class ProgressWidget {
method constructor (line 4) | constructor (options) {
method update (line 8) | update (data) {
FILE: packages/widget-statistic/src/server/widget.js
function format (line 10) | function format (format, value) {
class StatisticWidget (line 14) | class StatisticWidget {
method constructor (line 15) | constructor (options) {
method update (line 19) | update (value) {
FILE: packages/widget-status/src/providers/github/index.js
class Github (line 7) | class Github {
method configValidation (line 8) | static get configValidation () {
method mapHealth (line 12) | mapHealth (body) {
method fetch (line 23) | fetch () {
FILE: packages/widget-status/src/providers/statuspageio/index.js
function mapHealthStatus (line 9) | function mapHealthStatus (status) {
class StatuspageIo (line 19) | class StatuspageIo {
method constructor (line 20) | constructor (config) {
method configValidation (line 25) | static get configValidation () {
method filterComponentList (line 32) | filterComponentList (all, component) {
method fetch (line 41) | fetch () {
FILE: packages/widget-status/src/server/validator.js
function getBasicValidation (line 6) | function getBasicValidation () {
function validate (line 16) | function validate (schema, config) {
FILE: packages/widget-status/src/server/widget.js
class StatusWidget (line 6) | class StatusWidget {
method constructor (line 7) | constructor (options, emitter) {
method run (line 22) | run () {
method destroy (line 31) | destroy () {
FILE: packages/widget-time/src/server/widget.js
class TimeWidget (line 7) | class TimeWidget {
method constructor (line 8) | constructor (options, emitter) {
method run (line 19) | run () {
method destroy (line 24) | destroy () {
Condensed preview — 244 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (289K chars).
[
{
"path": ".codeclimate.json",
"chars": 72,
"preview": "{\n \"version\": \"2\",\n \"exclude_patterns\": [\n \"**/**/**.spec.js\"\n ]\n}"
},
{
"path": ".gitignore",
"chars": 19,
"preview": "node_modules\n*.log\n"
},
{
"path": ".travis.yml",
"chars": 642,
"preview": "language: node_js\nnode_js:\n - \"9\"\nmatrix:\n fast_finish: true\ncache:\n directories:\n - ~/.npm\n - node_modules\n - pac"
},
{
"path": "README.MD",
"chars": 1665,
"preview": "# Vudash\n\nAn open-source, configurable, extensible dashboard for monitoring, marketing, and more.\n\nNote that this projec"
},
{
"path": "docs/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "docs/README.md",
"chars": 11263,
"preview": "# Vudash\n\n[](ht"
},
{
"path": "docs/_coverpage.md",
"chars": 9763,
"preview": "<svg class=\"logo\" viewBox=\"0 0 176.08649 106.1699\">\n <g transform=\"translate(-24.377661,-51.731245)\">\n <g>\n <pa"
},
{
"path": "docs/api/README.md",
"chars": 2791,
"preview": "# API\n\nVudash exposes a very simple RESTful HTTP versioned api which can be used to perform a number of operations on a "
},
{
"path": "docs/datasources/README.md",
"chars": 8536,
"preview": "# Datasources\n\n## What is a Datasource\n\nA datasource provides the mechanism for widgets to recieve the information they "
},
{
"path": "docs/developers/README.md",
"chars": 10797,
"preview": "# Developing\n\nCreating widgets and datasources is designed to be quick and painless. They are delivered as simple npm mo"
},
{
"path": "docs/index.html",
"chars": 1211,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Vudash</title>\n <meta name=\"description\" cont"
},
{
"path": "docs/transformers/README.md",
"chars": 2996,
"preview": "# Transformers\n\nTransformers allow you to retrieve information from a data source or widget, and modify it before it is "
},
{
"path": "docs/widgets/README.md",
"chars": 10308,
"preview": "# Predefined widgets\n\nVudash has a number of widgets which are available on npm, these are in the `packages/` directory "
},
{
"path": "lerna.json",
"chars": 91,
"preview": "{\n \"lerna\": \"2.0.0-beta.36\",\n \"packages\": [\n \"packages/*\"\n ],\n \"version\": \"9.9.0\"\n}\n"
},
{
"path": "package.json",
"chars": 2123,
"preview": "{\n \"scripts\": {\n \"lint\": \"lerna run lint\",\n \"start\": \"lerna run start --scope vudash\",\n \"heroku-postbuild\": \"."
},
{
"path": "packages/core/README.md",
"chars": 1258,
"preview": "[](https://gitt"
},
{
"path": "packages/core/app.js",
"chars": 262,
"preview": "'use strict'\n\nconst { register, start, stop } = require('./src/server')\n\nlet server\n\nregister()\n .then(registered => {\n"
},
{
"path": "packages/core/bin/vudash.js",
"chars": 324,
"preview": "#!/usr/bin/env node\n\nconst create = require('../src/cli/create')\nconst help = require('../src/cli/help')\nconst logo = re"
},
{
"path": "packages/core/dashboards/simple.json",
"chars": 399,
"preview": "{\n \"name\": \"simple-dashboard\",\n \"layout\": {\n \"columns\": 5,\n \"rows\": 4\n },\n \"datasources\": {\n \"ds-rnd\": {\n "
},
{
"path": "packages/core/dashboards/template.json",
"chars": 190,
"preview": "{\n \"name\": \"simple-dashboard\",\n \"layout\": {\n \"columns\": 5,\n \"rows\": 4\n },\n \"widgets\": [\n { \"position\": { \"x"
},
{
"path": "packages/core/package.json",
"chars": 2252,
"preview": "{\n \"name\": \"vudash\",\n \"version\": \"9.9.0\",\n \"keywords\": [\n \"vudash\",\n \"dashboard\",\n \"dashing\",\n \"analytics"
},
{
"path": "packages/core/src/cli/create.js",
"chars": 1706,
"preview": "'use strict'\n\nconst npm = require('npm-programmatic')\nconst ora = require('ora')\nconst Path = require('path')\nconst fs ="
},
{
"path": "packages/core/src/cli/help.js",
"chars": 306,
"preview": "'use strict'\n\nconst { bold } = require('chalk')\n\nexports.run = function () {\n console.log('Usage: vudash [action]')\n c"
},
{
"path": "packages/core/src/cli/logo.js",
"chars": 308,
"preview": "'use strict'\n\nconst { textSync } = require('figlet')\nconst { yellow, blue } = require('chalk')\nconst { version } = requi"
},
{
"path": "packages/core/src/config-validator/config-validator.spec.js",
"chars": 1066,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst { validate } = require('.')\nconst Joi = require('joi')\n\ndescribe("
},
{
"path": "packages/core/src/config-validator/index.js",
"chars": 479,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst { ConfigurationError } = require('../errors')\n\nexports.validate = functio"
},
{
"path": "packages/core/src/dashboard/bundler/index.js",
"chars": 1641,
"preview": "'use strict'\n\nconst base = `\n import iziToast from 'izitoast'\n import 'izitoast/dist/css/iziToast.css'\n\n const VUDASH"
},
{
"path": "packages/core/src/dashboard/compiler/compiler.spec.js",
"chars": 625,
"preview": "'use strict'\n\nconst compiler = require('.')\nconst { stub } = require('sinon')\nconst rollup = require('rollup')\nconst { e"
},
{
"path": "packages/core/src/dashboard/compiler/configuration-builder/configuration-builder.js",
"chars": 874,
"preview": "'use strict'\n\nconst svelte = require('rollup-plugin-svelte')\nconst resolve = require('rollup-plugin-node-resolve')\nconst"
},
{
"path": "packages/core/src/dashboard/compiler/configuration-builder/configuration-builder.spec.js",
"chars": 461,
"preview": "'use strict'\n\nconst builder = require('.')\nconst { reach } = require('hoek')\nconst { expect } = require('code')\n\ndescrib"
},
{
"path": "packages/core/src/dashboard/compiler/configuration-builder/index.js",
"chars": 66,
"preview": "'use strict'\n\nmodule.exports = require('./configuration-builder')\n"
},
{
"path": "packages/core/src/dashboard/compiler/index.js",
"chars": 329,
"preview": "'use strict'\n\nconst rollup = require('rollup')\nconst { build } = require('./configuration-builder')\n\nexports.compile = f"
},
{
"path": "packages/core/src/dashboard/dashboard.js",
"chars": 2628,
"preview": "'use strict'\n\nconst { reach } = require('hoek')\nconst Emitter = require('./emitter')\nconst id = require('../id-gen')\ncon"
},
{
"path": "packages/core/src/dashboard/dashboard.spec.js",
"chars": 10630,
"preview": "'use strict'\n\nconst Emitter = require('./emitter')\nconst widgetBinder = require('../widget-binder')\nconst renderer = req"
},
{
"path": "packages/core/src/dashboard/emitter/emitter.js",
"chars": 872,
"preview": "'use strict'\n\nconst chalk = require('chalk')\n\nclass Emitter {\n constructor (socketio, room) {\n this.io = socketio\n "
},
{
"path": "packages/core/src/dashboard/emitter/emitter.spec.js",
"chars": 1755,
"preview": "'use strict'\n\nconst Emitter = require('.')\nconst { expect } = require('code')\nconst { stub, spy } = require('sinon')\n\nde"
},
{
"path": "packages/core/src/dashboard/emitter/index.js",
"chars": 52,
"preview": "'use strict'\n\nmodule.exports = require('./emitter')\n"
},
{
"path": "packages/core/src/dashboard/index.js",
"chars": 54,
"preview": "'use strict'\n\nmodule.exports = require('./dashboard')\n"
},
{
"path": "packages/core/src/dashboard/loader/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./loader')\n"
},
{
"path": "packages/core/src/dashboard/loader/loader.js",
"chars": 828,
"preview": "'use strict'\n\nconst { NotFoundError } = require('../../errors')\nconst Dashboard = require('..')\nconst fs = require('fs')"
},
{
"path": "packages/core/src/dashboard/loader/loader.spec.js",
"chars": 1087,
"preview": "'use strict'\n\nconst { NotFoundError } = require('../../errors')\nconst loader = require('.')\nconst { expect } = require('"
},
{
"path": "packages/core/src/dashboard/parser/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./parser')\n"
},
{
"path": "packages/core/src/dashboard/parser/parser.js",
"chars": 602,
"preview": "'use strict'\n\nconst { ConfigurationError } = require('../../errors')\n\nfunction get (directive) {\n if (process.env.hasOw"
},
{
"path": "packages/core/src/dashboard/parser/parser.spec.js",
"chars": 1462,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst { parse } = require('.')\nconst { ConfigurationError } = require('"
},
{
"path": "packages/core/src/dashboard/renderer/index.js",
"chars": 53,
"preview": "'use strict'\n\nmodule.exports = require('./renderer')\n"
},
{
"path": "packages/core/src/dashboard/renderer/renderer.js",
"chars": 650,
"preview": "'use strict'\n\nconst bundler = require('../bundler')\nconst compiler = require('../compiler')\nconst Css = require('json-to"
},
{
"path": "packages/core/src/dashboard/renderer/renderer.spec.js",
"chars": 57,
"preview": "'use strict'\n\ndescribe('dashboard/renderer', () => {\n\n})\n"
},
{
"path": "packages/core/src/dashboard/schema/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./schema')\n"
},
{
"path": "packages/core/src/dashboard/schema/schema.js",
"chars": 1837,
"preview": "'use strict'\n\nconst Joi = require('joi')\n\nconst layoutSchema = Joi.object({\n columns: Joi.number().required().descripti"
},
{
"path": "packages/core/src/dashboard/schema/schema.spec.js",
"chars": 3189,
"preview": "'use strict'\n\nconst { schema } = require('.')\nconst fs = require('fs')\nconst { join } = require('path')\nconst { expect }"
},
{
"path": "packages/core/src/dashboard-event/dashboard-event.js",
"chars": 119,
"preview": "'use strict'\n\nexports.build = function (data) {\n return {\n meta: {\n updated: new Date()\n },\n data\n }\n}\n"
},
{
"path": "packages/core/src/dashboard-event/index.js",
"chars": 60,
"preview": "'use strict'\n\nmodule.exports = require('./dashboard-event')\n"
},
{
"path": "packages/core/src/datasource/datasource.spec.js",
"chars": 1888,
"preview": "'use strict'\n\nconst loader = require('.')\nconst locator = require('./locator')\nconst DatasourceBuilder = require('util/d"
},
{
"path": "packages/core/src/datasource/dummy-datasource/dummy-datasource.spec.js",
"chars": 522,
"preview": "'use strict'\n\nconst DummyDatasource = require('.')\nconst { expect } = require('code')\n\ndescribe('datasource.dummy-dataso"
},
{
"path": "packages/core/src/datasource/dummy-datasource/index.js",
"chars": 311,
"preview": "'use strict'\n\nclass DummyDatasource {\n constructor (options) {\n this.config = options\n }\n\n fetch () {\n throw ne"
},
{
"path": "packages/core/src/datasource/index.js",
"chars": 1040,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst { applyToDefaults } = require('hoek')\nconst DummyDatasource = require('./"
},
{
"path": "packages/core/src/datasource/locator/index.js",
"chars": 318,
"preview": "'use strict'\n\nconst { WidgetRegistrationError } = require('../../errors')\n\nexports.locate = function (datasources, datas"
},
{
"path": "packages/core/src/datasource/locator/locator.spec.js",
"chars": 573,
"preview": "'use strict'\n\nconst { WidgetRegistrationError } = require('../../errors')\nconst locator = require('.')\nconst { expect } "
},
{
"path": "packages/core/src/datasource/validator/index.js",
"chars": 254,
"preview": "'use strict'\n\nconst configValidator = require('../../config-validator')\n\nexports.validate = function (widgetName, valida"
},
{
"path": "packages/core/src/datasource/validator/validator.spec.js",
"chars": 1075,
"preview": "'use strict'\n\nconst validator = require('.')\nconst configValidator = require('../../config-validator')\nconst { stub } = "
},
{
"path": "packages/core/src/datasource-binder/datasource-binder.js",
"chars": 619,
"preview": "'use strict'\n\nconst { createEmitter } = require('./datasource-emitter')\nconst chalk = require('chalk')\n\nexports.bind = f"
},
{
"path": "packages/core/src/datasource-binder/datasource-binder.spec.js",
"chars": 2422,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst binder = require('.')\nconst { stub, useFakeTimers } = require('si"
},
{
"path": "packages/core/src/datasource-binder/datasource-emitter.js",
"chars": 176,
"preview": "'use strict'\n\nconst EventEmitter = require('events')\n\nclass DatasourceEmitter extends EventEmitter {}\n\nexports.createEmi"
},
{
"path": "packages/core/src/datasource-binder/index.js",
"chars": 62,
"preview": "'use strict'\n\nmodule.exports = require('./datasource-binder')\n"
},
{
"path": "packages/core/src/datasource-loader/datasource-loader.js",
"chars": 1452,
"preview": "'use strict'\n\nconst resolver = require('../resolver')\nconst configValidator = require('../config-validator')\nconst datas"
},
{
"path": "packages/core/src/datasource-loader/datasource-loader.spec.js",
"chars": 3357,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst { stub } = require('sinon')\nconst datasourceLoader = require('.')"
},
{
"path": "packages/core/src/datasource-loader/index.js",
"chars": 62,
"preview": "'use strict'\n\nmodule.exports = require('./datasource-loader')\n"
},
{
"path": "packages/core/src/errors/configuration.error.js",
"chars": 75,
"preview": "'use strict'\n\nmodule.exports = class ConfigurationError extends Error {\n\n}\n"
},
{
"path": "packages/core/src/errors/index.js",
"chars": 209,
"preview": "'use strict'\n\nconst { upperCamel } = require('../upper-camel')\n\nconst requireDirectory = require('require-directory')\nco"
},
{
"path": "packages/core/src/errors/not-found.error.js",
"chars": 70,
"preview": "'use strict'\n\nmodule.exports = class NotFoundError extends Error {\n\n}\n"
},
{
"path": "packages/core/src/errors/plugin-registration.error.js",
"chars": 80,
"preview": "'use strict'\n\nmodule.exports = class PluginRegistrationError extends Error {\n\n}\n"
},
{
"path": "packages/core/src/errors/widget-registration.error.js",
"chars": 73,
"preview": "'use strict'\n\nmodule.exports = class WidgetRegistrati extends Error {\n\n}\n"
},
{
"path": "packages/core/src/id-gen/id-gen.js",
"chars": 161,
"preview": "'use strict'\n\nmodule.exports = () => {\n const random = Math.random() * 0xFFFFFFFFFFFF << 0\n const positive = Math.abs("
},
{
"path": "packages/core/src/id-gen/id-gen.spec.js",
"chars": 210,
"preview": "'use strict'\n\nconst id = require('.')\nconst { expect } = require('code')\n\ndescribe('id-gen', () => {\n it('generates a s"
},
{
"path": "packages/core/src/id-gen/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./id-gen')\n"
},
{
"path": "packages/core/src/plugins/api/api.js",
"chars": 1131,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst viewCurrentHandlers = require('./handlers/view/current')\nconst dashboardH"
},
{
"path": "packages/core/src/plugins/api/handlers/dashboards/index.js",
"chars": 45,
"preview": "'use strict'\n\nexports.put = require('./put')\n"
},
{
"path": "packages/core/src/plugins/api/handlers/dashboards/put.js",
"chars": 425,
"preview": "'use strict'\n\nconst loader = require('../../../../dashboard/loader')\n\nmodule.exports = function (request, reply) {\n con"
},
{
"path": "packages/core/src/plugins/api/handlers/dashboards/put.spec.js",
"chars": 1125,
"preview": "'use strict'\n\nconst { stub } = require('sinon')\nconst { put } = require('.')\nconst { expect } = require('code')\n\ndescrib"
},
{
"path": "packages/core/src/plugins/api/handlers/view/current/index.js",
"chars": 45,
"preview": "'use strict'\n\nexports.put = require('./put')\n"
},
{
"path": "packages/core/src/plugins/api/handlers/view/current/put.js",
"chars": 199,
"preview": "'use strict'\n\nmodule.exports = function (request, reply) {\n const { io } = request.server.plugins.socket\n const { dash"
},
{
"path": "packages/core/src/plugins/api/handlers/view/current/put.spec.js",
"chars": 1236,
"preview": "'use strict'\n\nconst { stub } = require('sinon')\nconst { put } = require('.')\nconst { expect } = require('code')\n\ndescrib"
},
{
"path": "packages/core/src/plugins/api/index.js",
"chars": 48,
"preview": "'use strict'\n\nmodule.exports = require('./api')\n"
},
{
"path": "packages/core/src/plugins/socket/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./socket')\n"
},
{
"path": "packages/core/src/plugins/socket/socket.js",
"chars": 299,
"preview": "'use strict'\n\nconst socketio = require('socket.io')\n\nconst SocketPlugin = {\n register: function (server, options, next)"
},
{
"path": "packages/core/src/plugins/static/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./static')\n"
},
{
"path": "packages/core/src/plugins/static/static.js",
"chars": 443,
"preview": "'use strict'\n\nconst { join } = require('path')\n\nconst AssetsPlugin = {\n register: function (server, options, next) {\n "
},
{
"path": "packages/core/src/plugins/ui/handlers/dashboard/index.js",
"chars": 978,
"preview": "'use strict'\n\nconst dashboardLoader = require('../../../../dashboard/loader')\nconst { NotFoundError } = require('../../."
},
{
"path": "packages/core/src/plugins/ui/handlers/index/index.js",
"chars": 1093,
"preview": "'use strict'\n\nconst Path = require('path')\nconst { readdirSync } = require('fs')\nconst { internal } = require('boom')\nco"
},
{
"path": "packages/core/src/plugins/ui/handlers/index/index.spec.js",
"chars": 557,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst { handler } = require('.')\n\ndescribe('plugins/ui/handlers/index',"
},
{
"path": "packages/core/src/plugins/ui/index.js",
"chars": 47,
"preview": "'use strict'\n\nmodule.exports = require('./ui')\n"
},
{
"path": "packages/core/src/plugins/ui/ui.js",
"chars": 991,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst { handler: indexHandler } = require('./handlers/index')\nconst { handler: "
},
{
"path": "packages/core/src/plugins/ui/ui.spec.js",
"chars": 669,
"preview": "'use strict'\n\nconst server = require('server')\nconst { expect } = require('code')\n\ndescribe('core/plugins/ui', function "
},
{
"path": "packages/core/src/public/css/listing.css",
"chars": 435,
"preview": "body {\n font-family: 'Ubuntu', sans-serif;\n color: #fff;\n background-color: #191919;\n text-align: center;\n}\n\n.logo {"
},
{
"path": "packages/core/src/public/css/style.css",
"chars": 770,
"preview": "@font-face {\n font-family: 'Ubuntu';\n font-style: normal;\n font-weight: 400;\n src: local('Ubuntu Regular'), local('U"
},
{
"path": "packages/core/src/public/js/audio.js",
"chars": 287,
"preview": "'use strict'\n\nvar VUDASH = window.VUDASH\n\nvar Player = function () {\n this.audio = new window.Audio()\n}\n\nPlayer.prototy"
},
{
"path": "packages/core/src/public/js/object-assign.polyfill.js",
"chars": 608,
"preview": "'use strict'\n\nif (typeof Object.assign !== 'function') {\n Object.assign = function (target, varArgs) {\n 'use strict'"
},
{
"path": "packages/core/src/resolver/index.js",
"chars": 53,
"preview": "'use strict'\n\nmodule.exports = require('./resolver')\n"
},
{
"path": "packages/core/src/resolver/resolver.js",
"chars": 776,
"preview": "'use strict'\n\nconst fs = require('fs')\nconst { join } = require('path')\n\nfunction discoverNpmModule (moduleName) {\n try"
},
{
"path": "packages/core/src/resolver/resolver.spec.js",
"chars": 629,
"preview": "'use strict'\n\nconst { discover } = require('.')\nconst { expect } = require('code')\nconst { resolve } = require('path')\n\n"
},
{
"path": "packages/core/src/server.js",
"chars": 2604,
"preview": "'use strict'\n\nconst requirePaths = require('app-module-path')\nrequirePaths.addPath(process.cwd())\nrequirePaths.addPath(`"
},
{
"path": "packages/core/src/transform-loader/index.js",
"chars": 61,
"preview": "'use strict'\n\nmodule.exports = require('./transform-loader')\n"
},
{
"path": "packages/core/src/transform-loader/transform-loader.js",
"chars": 771,
"preview": "'use strict'\n\nconst resolver = require('../resolver')\nconst configValidator = require('../config-validator')\nconst Joi ="
},
{
"path": "packages/core/src/transform-loader/transform-loader.spec.js",
"chars": 2105,
"preview": "'use strict'\n\nconst { load } = require('.')\nconst { expect } = require('code')\nconst { stub } = require('sinon')\nconst r"
},
{
"path": "packages/core/src/upper-camel/index.js",
"chars": 183,
"preview": "'use strict'\n\nconst { flow, camelCase, upperFirst } = require('lodash')\n\nexports.upperCamel = function (name) {\n const "
},
{
"path": "packages/core/src/upper-camel/upper-camel.spec.js",
"chars": 574,
"preview": "'use strict'\n\nconst { upperCamel } = require('.')\nconst { expect } = require('code')\n\ndescribe('upper-camel', () => {\n "
},
{
"path": "packages/core/src/views/dashboard.html",
"chars": 715,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\""
},
{
"path": "packages/core/src/views/listing.html",
"chars": 10329,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\""
},
{
"path": "packages/core/src/widget/history/history.js",
"chars": 333,
"preview": "'use strict'\n\nclass History {\n constructor (size = 10) {\n this.size = size\n this.items = []\n }\n\n insert (entry)"
},
{
"path": "packages/core/src/widget/history/history.spec.js",
"chars": 1439,
"preview": "'use strict'\n\nconst { create } = require('.')\nconst { times } = require('lodash')\nconst { expect } = require('code')\n\nde"
},
{
"path": "packages/core/src/widget/history/index.js",
"chars": 52,
"preview": "'use strict'\n\nmodule.exports = require('./history')\n"
},
{
"path": "packages/core/src/widget/index.js",
"chars": 1489,
"preview": "'use strict'\n\nconst id = require('../id-gen')\nconst WidgetPosition = require('./widget-position')\nconst loader = require"
},
{
"path": "packages/core/src/widget/loader/index.js",
"chars": 1836,
"preview": "'use strict'\n\nconst { reach } = require('hoek')\nconst { join } = require('path')\nconst resolver = require('../../resolve"
},
{
"path": "packages/core/src/widget/loader/loader.spec.js",
"chars": 1345,
"preview": "'use strict'\n\nconst loader = require('.')\nconst { ComponentCompilationError } = require('errors')\nconst { expect } = req"
},
{
"path": "packages/core/src/widget/renderer/index.js",
"chars": 927,
"preview": "'use strict'\n\nexports.renderScript = function (id, name, config) {\n return `\n const widget_${id} = new ${name}({ \n "
},
{
"path": "packages/core/src/widget/renderer/renderer.spec.js",
"chars": 2638,
"preview": "'use strict'\n\nconst renderer = require('.')\nconst WidgetPosition = require('../widget-position')\nconst Cheerio = require"
},
{
"path": "packages/core/src/widget/validator/index.js",
"chars": 54,
"preview": "'use strict'\n\nmodule.exports = require('./validator')\n"
},
{
"path": "packages/core/src/widget/validator/validator.js",
"chars": 406,
"preview": "'use strict'\n\nconst configValidator = require('../../config-validator')\nconst Joi = require('joi')\n\nconst defaultSchema "
},
{
"path": "packages/core/src/widget/validator/validator.spec.js",
"chars": 276,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst { validate } = require('.')\n\ndescribe('widget/validator', () => {"
},
{
"path": "packages/core/src/widget/widget-position/index.js",
"chars": 592,
"preview": "class WidgetPosition {\n constructor (dashboardLayout, position) {\n this.columns = dashboardLayout.columns\n this.r"
},
{
"path": "packages/core/src/widget/widget-position/widget-position.spec.js",
"chars": 889,
"preview": "'use strict'\n\nconst WidgetPosition = require('.')\nconst { expect } = require('code')\n\ndescribe('css-builder/widget-posit"
},
{
"path": "packages/core/src/widget/widget.spec.js",
"chars": 4848,
"preview": "'use strict'\n\nconst { create } = require('.')\nconst { stub } = require('sinon')\nconst loader = require('./loader')\nconst"
},
{
"path": "packages/core/src/widget-binder/index.js",
"chars": 58,
"preview": "'use strict'\n\nmodule.exports = require('./widget-binder')\n"
},
{
"path": "packages/core/src/widget-binder/widget-binder.js",
"chars": 1212,
"preview": "'use strict'\n\nconst Widget = require('../widget')\nconst EventEmitter = require('events')\nconst transformLoader = require"
},
{
"path": "packages/core/src/widget-binder/widget-binder.spec.js",
"chars": 4301,
"preview": "'use strict'\n\nconst { load } = require('.')\nconst { expect } = require('code')\nconst { stub } = require('sinon')\nconst W"
},
{
"path": "packages/core/src/widget-datasource-binding/index.js",
"chars": 70,
"preview": "'use strict'\n\nmodule.exports = require('./widget-datasource-binding')\n"
},
{
"path": "packages/core/src/widget-datasource-binding/widget-datasource-binding.js",
"chars": 657,
"preview": "'use strict'\n\nconst dashboardEvent = require('../dashboard-event')\n\nfunction transform (data, transformers) {\n return t"
},
{
"path": "packages/core/src/widget-datasource-binding/widget-datasource-binding.spec.js",
"chars": 2408,
"preview": "'use strict'\n\nconst widgetDatasourceBinder = require('.')\nconst { stub } = require('sinon')\nconst { expect } = require('"
},
{
"path": "packages/core/test/resources/widgets/broken/package.json",
"chars": 60,
"preview": "{\n \"name\": \"vudash-widget-broken\",\n \"main\": \"widget.js\"\n}\n"
},
{
"path": "packages/core/test/resources/widgets/broken/widget.js",
"chars": 55,
"preview": "'use strict'\n\nexports.register = () => {\n return {}\n}\n"
},
{
"path": "packages/core/test/resources/widgets/configurable/component.html",
"chars": 11,
"preview": "<h1>hi</h1>"
},
{
"path": "packages/core/test/resources/widgets/configurable/package.json",
"chars": 121,
"preview": "{\n \"name\": \"vudash-widget-configurable\",\n \"main\": \"widget.js\",\n \"vudash\": {\n \"component\": \"./component.html\"\n }\n}"
},
{
"path": "packages/core/test/resources/widgets/configurable/widget.js",
"chars": 354,
"preview": "'use strict'\n\nconst { Promise } = require('bluebird')\nconst defaults = {\n foo: 'bar',\n working: false\n}\n\nclass Example"
},
{
"path": "packages/core/test/resources/widgets/example/markup.html",
"chars": 112,
"preview": "<h1>Hello</h1>\n\n<script>\n console.log('hello');\n</script>\n\n<style>\n body { color: rgb(255,255,255); }\n</style>"
},
{
"path": "packages/core/test/resources/widgets/example/package.json",
"chars": 113,
"preview": "{\n \"name\": \"vudash-widget-example\",\n \"main\": \"widget.js\",\n \"vudash\": {\n \"component\": \"./markup.html\"\n }\n}\n"
},
{
"path": "packages/core/test/resources/widgets/example/widget.js",
"chars": 197,
"preview": "'use strict'\n\nconst { Promise } = require('bluebird')\n\nclass ExampleWidget {\n update (data) {\n return Promise.resolv"
},
{
"path": "packages/core/test/resources/widgets/missing/package.json",
"chars": 61,
"preview": "{\n \"name\": \"vudash-widget-missing\",\n \"main\": \"widget.js\"\n}\n"
},
{
"path": "packages/core/test/resources/widgets/missing/widget.js",
"chars": 55,
"preview": "'use strict'\n\nexports.register = () => {\n return {}\n}\n"
},
{
"path": "packages/core/test/util/dashboard.builder.js",
"chars": 784,
"preview": "'use strict'\n\nconst WidgetBuilder = require('./widget.builder')\n\nclass DashboardBuilder {\n constructor () {\n this.ov"
},
{
"path": "packages/core/test/util/datasource.builder.js",
"chars": 321,
"preview": "'use strict'\n\nclass Datasource {\n constructor (options) {\n this.options = options\n }\n\n fetch () {\n return this."
},
{
"path": "packages/core/test/util/widget.builder.js",
"chars": 1063,
"preview": "'use strict'\n\nconst { Promise } = require('bluebird')\n\nclass WidgetBuilder {\n constructor () {\n this.widget = this._"
},
{
"path": "packages/datasource-google-sheets/datasource.js",
"chars": 262,
"preview": "'use strict'\n\nconst GoogleSheetsTransport = require('./src/google-sheets-transport')\nconst { validation } = require('./s"
},
{
"path": "packages/datasource-google-sheets/datasource.spec.js",
"chars": 345,
"preview": "'use strict'\n\nconst datasource = require('./datasource')\nconst { expect } = require('code')\n\ndescribe('datasource-google"
},
{
"path": "packages/datasource-google-sheets/package.json",
"chars": 808,
"preview": "{\n \"name\": \"@vudash/datasource-google-sheets\",\n \"version\": \"9.9.0\",\n \"description\": \"Google Sheets datasource for Vud"
},
{
"path": "packages/datasource-google-sheets/src/config-validator/config-validator.spec.js",
"chars": 1084,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst configUtil = require('../../test/config.util')\nconst sinon = require('sin"
},
{
"path": "packages/datasource-google-sheets/src/config-validator/index.js",
"chars": 2309,
"preview": "'use strict'\n\nconst Joi = require('joi')\n\nclass ConfigValidator {\n get inlineCredentialsValidation () {\n return Joi."
},
{
"path": "packages/datasource-google-sheets/src/google-sheets-transport/google-sheets-transport.spec.js",
"chars": 4066,
"preview": "'use strict'\n\nconst GoogleSheetsTransport = require('.')\nconst configUtil = require('../../test/config.util')\nconst sino"
},
{
"path": "packages/datasource-google-sheets/src/google-sheets-transport/index.js",
"chars": 2371,
"preview": "'use strict'\n\nconst path = require('path')\nconst fs = require('fs')\nconst { Promise } = require('bluebird')\nconst spread"
},
{
"path": "packages/datasource-google-sheets/test/config.util.js",
"chars": 1033,
"preview": "'use strict'\n\nfunction getConfig (credentialsOverride) {\n const uri = 'http://a.b'\n\n const credentials = credentialsOv"
},
{
"path": "packages/datasource-google-sheets/test/example.credentials.test.json",
"chars": 252,
"preview": "{\"type\":\"service_account\",\"project_id\":\"d\",\"private_key_id\":\"a\",\"private_key\":\"b\",\"client_email\":\"p@x.y\",\"client_id\":\"12"
},
{
"path": "packages/datasource-google-sheets/test/example.invalid-credentials.test.json",
"chars": 3,
"preview": "{}\n"
},
{
"path": "packages/datasource-random/datasource.js",
"chars": 248,
"preview": "'use strict'\n\nconst RandomTransport = require('./src/random-transport')\nconst { validation } = require('./src/datasource"
},
{
"path": "packages/datasource-random/datasource.spec.js",
"chars": 2094,
"preview": "'use strict'\n\nconst RandomTransport = require('./src/random-transport')\nconst { expect } = require('code')\n\ndescribe('pl"
},
{
"path": "packages/datasource-random/package.json",
"chars": 870,
"preview": "{\n \"name\": \"@vudash/datasource-random\",\n \"version\": \"9.9.0\",\n \"description\": \"Random Datasource for Vudash\",\n \"main\""
},
{
"path": "packages/datasource-random/src/datasource-validation/index.js",
"chars": 310,
"preview": "'use strict'\n\nconst Joi = require('joi')\n\nmodule.exports.validation = {\n method: Joi.string().optional().description('C"
},
{
"path": "packages/datasource-random/src/random-transport/index.js",
"chars": 1215,
"preview": "'use strict'\n\nconst Chance = require('chance')\nconst Promise = require('bluebird').Promise\n\nclass RandomTransport {\n co"
},
{
"path": "packages/datasource-random/src/random-transport/index.spec.js",
"chars": 369,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst RandomTransport = require('.')\n\ndescribe('random-transport', () ="
},
{
"path": "packages/datasource-rest/datasource.js",
"chars": 242,
"preview": "'use strict'\n\nconst RestTransport = require('./src/rest-transport')\nconst { validation } = require('./src/datasource-val"
},
{
"path": "packages/datasource-rest/package.json",
"chars": 660,
"preview": "{\n \"name\": \"@vudash/datasource-rest\",\n \"version\": \"9.9.0\",\n \"description\": \"REST Datasource for Vudash\",\n \"main\": \"d"
},
{
"path": "packages/datasource-rest/src/datasource-validation/datasource-validation.js",
"chars": 477,
"preview": "'use strict'\n\nconst Joi = require('joi')\n\nexports.validation = {\n url: Joi.string().description('Url to call'),\n metho"
},
{
"path": "packages/datasource-rest/src/datasource-validation/datasource-validation.spec.js",
"chars": 310,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst { validation } = require('.')\nconst { expect } = require('code')\n\ndescrib"
},
{
"path": "packages/datasource-rest/src/datasource-validation/index.js",
"chars": 66,
"preview": "'use strict'\n\nmodule.exports = require('./datasource-validation')\n"
},
{
"path": "packages/datasource-rest/src/rest-transport/index.js",
"chars": 784,
"preview": "'use strict'\n\nconst got = require('got')\nconst { applyToDefaults } = require('hoek')\nconst pkg = require('../../package."
},
{
"path": "packages/datasource-rest/src/rest-transport/rest-transport.spec.js",
"chars": 1739,
"preview": "'use strict'\n\nconst nock = require('nock')\nconst RestTransport = require('.')\nconst { expect } = require('code')\n\ndescri"
},
{
"path": "packages/datasource-value/datasource.js",
"chars": 245,
"preview": "'use strict'\n\nconst ValueTransport = require('./src/value-transport')\nconst { validation } = require('./src/datasource-v"
},
{
"path": "packages/datasource-value/package.json",
"chars": 744,
"preview": "{\n \"name\": \"@vudash/datasource-value\",\n \"version\": \"9.9.0\",\n \"description\": \"Value datasource for Vudash dashboards\","
},
{
"path": "packages/datasource-value/src/datasource-validation/datasource-validation.spec.js",
"chars": 655,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst { validation } = require('.')\nconst { expect } = require('code')\n\ndescrib"
},
{
"path": "packages/datasource-value/src/datasource-validation/index.js",
"chars": 135,
"preview": "'use strict'\n\nconst Joi = require('joi')\n\nmodule.exports.validation = {\n value: Joi.any().required().description('Value"
},
{
"path": "packages/datasource-value/src/value-transport/index.js",
"chars": 237,
"preview": "'use strict'\n\nconst Promise = require('bluebird').Promise\n\nclass ValueTransport {\n constructor (options) {\n this.con"
},
{
"path": "packages/datasource-value/src/value-transport/index.spec.js",
"chars": 342,
"preview": "'use strict'\n\nconst ValueTransport = require('.')\nconst { expect } = require('code')\n\ndescribe('value', () => {\n it('Re"
},
{
"path": "packages/transformer-jq/index.js",
"chars": 60,
"preview": "'use strict'\n\nmodule.exports = require('./lib/transformer')\n"
},
{
"path": "packages/transformer-jq/lib/transformer.js",
"chars": 549,
"preview": "'use strict'\n\nconst { jq } = require('jq.node')\nconst { Promise } = require('bluebird')\n\nclass JqTransformer {\n constru"
},
{
"path": "packages/transformer-jq/lib/transformer.spec.js",
"chars": 870,
"preview": "'use strict'\n\nconst JqTransformer = require('./transformer')\nconst { expect } = require('code')\n\ndescribe('transformer',"
},
{
"path": "packages/transformer-jq/package.json",
"chars": 491,
"preview": "{\n \"name\": \"@vudash/transformer-jq\",\n \"version\": \"9.9.0\",\n \"description\": \"Vudash data transformer which uses jq to m"
},
{
"path": "packages/transformer-map/package.json",
"chars": 389,
"preview": "{\n \"name\": \"@vudash/transformer-map\",\n \"version\": \"9.9.0\",\n \"description\": \"Vudash data transformer which maps from o"
},
{
"path": "packages/transformer-map/transformer.js",
"chars": 263,
"preview": "'use strict'\n\nconst { transform } = require('reorient')\n\nclass MapTransformer {\n constructor (schema) {\n this.schema"
},
{
"path": "packages/widget-chart/package.json",
"chars": 883,
"preview": "{\n \"name\": \"@vudash/widget-chart\",\n \"version\": \"9.9.0\",\n \"description\": \"Chart widget for Vudash\",\n \"main\": \"./src/s"
},
{
"path": "packages/widget-chart/src/client/chart-options.js",
"chars": 458,
"preview": "'use strict'\n\nconst ChartTypes = {\n 'line': {\n constructorName: 'Line',\n options: {\n chartPadding: {\n "
},
{
"path": "packages/widget-chart/src/client/markup.html",
"chars": 1342,
"preview": "<div class=\"vudash-chart\">\n <div ref:chart class=\"value\">\n </div>\n <div class=\"label\">\n {{ config.description }}\n "
},
{
"path": "packages/widget-chart/src/server/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./widget')\n"
},
{
"path": "packages/widget-chart/src/server/widget.js",
"chars": 421,
"preview": "'use strict'\n\nconst defaults = {\n description: '',\n labels: [],\n type: 'line'\n}\n\nclass ChartWidget {\n constructor (o"
},
{
"path": "packages/widget-chart/src/server/widget.spec.js",
"chars": 1475,
"preview": "'use strict'\n\nconst { register } = require('.')\nconst { expect } = require('code')\n\ndescribe('widget', () => {\n context"
},
{
"path": "packages/widget-ci/.gitignore",
"chars": 19,
"preview": "node_modules\n*.log\n"
},
{
"path": "packages/widget-ci/README.MD",
"chars": 228,
"preview": "# Vudash CI Widget\n\nShows the status of CI builds on a [Vudash](https://npmjs.org/vudash) Dashboard\n\nIndicates project b"
},
{
"path": "packages/widget-ci/package.json",
"chars": 972,
"preview": "{\n \"name\": \"vudash-widget-ci\",\n \"version\": \"9.9.0\",\n \"description\": \"A CI Widget for Vudash\",\n \"main\": \"./src/server"
},
{
"path": "packages/widget-ci/src/build-status.enum.js",
"chars": 305,
"preview": "class BuildStatus {\n static get passed () {\n return 'passed'\n }\n\n static get failed () {\n return 'failed'\n }\n\n"
},
{
"path": "packages/widget-ci/src/client/markup.html",
"chars": 3888,
"preview": "<div class=\"ci-widget {{ icon }}\">\n <div class=\"value\">\n {{#if icon === 'running'}}\n <svg width=\"54%\" version=\"1."
},
{
"path": "packages/widget-ci/src/engines/circleci/circleci.spec.js",
"chars": 1397,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst CircleCI = require('circleci')\nconst BuildStatus = require('../.."
},
{
"path": "packages/widget-ci/src/engines/circleci/index.js",
"chars": 1108,
"preview": "'use strict'\n\nconst CircleCI = require('circleci')\nconst BuildStatus = require('../../build-status.enum')\n\nclass CircleC"
},
{
"path": "packages/widget-ci/src/engines/factory.js",
"chars": 352,
"preview": "const Travis = require('./travis')\nconst CircleCI = require('./circleci')\n\nclass Factory {\n constructor () {\n this.e"
},
{
"path": "packages/widget-ci/src/engines/travis/index.js",
"chars": 971,
"preview": "'use strict'\n\nconst Travis = require('travis-ci')\nconst Promise = require('bluebird').Promise\nconst BuildStatus = requir"
},
{
"path": "packages/widget-ci/src/engines/travis/travis.spec.js",
"chars": 617,
"preview": "'use strict'\n\nconst { expect } = require('code')\nconst sinon = require('sinon')\n\nconst BuildStatus = require('../../buil"
},
{
"path": "packages/widget-ci/src/server/index.js",
"chars": 51,
"preview": "'use strict'\n\nmodule.exports = require('./widget')\n"
},
{
"path": "packages/widget-ci/src/server/validation.js",
"chars": 1150,
"preview": "'use strict'\n\nconst engineFactory = require('../engines/factory')\nconst Joi = require('joi')\n\nmodule.exports = {\n repo:"
},
{
"path": "packages/widget-ci/src/server/widget.js",
"chars": 1235,
"preview": "'use strict'\n\nconst Joi = require('joi')\nconst Hoek = require('hoek')\nconst engineFactory = require('../engines/factory'"
},
{
"path": "packages/widget-ci/src/server/widget.spec.js",
"chars": 3736,
"preview": "'use strict'\n\nconst { register } = require('.')\nconst Travis = require('../engines/travis')\nconst BuildStatus = require("
},
{
"path": "packages/widget-gauge/.gitignore",
"chars": 19,
"preview": "node_modules\n*.log\n"
},
{
"path": "packages/widget-gauge/README.MD",
"chars": 177,
"preview": "# Vudash Gauge Widget\n\nDisplays a gauge, like a vu-meter, on a [Vudash](https://npmjs.org/vudash) Dashboard\n\nDocumentati"
},
{
"path": "packages/widget-gauge/package.json",
"chars": 579,
"preview": "{\n \"name\": \"vudash-widget-gauge\",\n \"version\": \"9.9.0\",\n \"main\": \"./src/server\",\n \"scripts\": {\n \"test\": \"NODE_PATH"
},
{
"path": "packages/widget-gauge/src/client/markup.html",
"chars": 2507,
"preview": "<div class=\"widget-gauge\">\n <div class=\"wrapper\">\n <svg width=\"54%\" viewBox=\"0 0 {{dimension}} {{dimension}}\">\n "
}
]
// ... and 44 more files (download for full content)
About this extraction
This page contains the full source code of the vudash/vudash GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 244 files (252.0 KB), approximately 79.5k tokens, and a symbol index with 185 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.