[
  {
    "path": ".codeclimate.json",
    "content": "{\n  \"version\": \"2\",\n  \"exclude_patterns\": [\n    \"**/**/**.spec.js\"\n  ]\n}"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n*.log\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"9\"\nmatrix:\n  fast_finish: true\ncache:\n  directories:\n  - ~/.npm\n  - node_modules\n  - packages/**/node_modules\nenv:\n  matrix:\n  - PACKAGE=vudash\n  - PACKAGE=@vudash/datasource-rest\n  - PACKAGE=@vudash/datasource-random\n  - PACKAGE=@vudash/datasource-value\n  - PACKAGE=@vudash/datasource-google-sheets\n  - PACKAGE=vudash-widget-ci\n  - PACKAGE=vudash-widget-gauge\n  - PACKAGE=vudash-widget-progress\n  - PACKAGE=vudash-widget-statistic\n  - PACKAGE=vudash-widget-time\n  - PACKAGE=vudash-widget-status\n  - PACKAGE=@vudash/widget-chart\nscript:\n  - lerna run lint --scope $TEST_DIR\n  - lerna run test --scope $TEST_DIR"
  },
  {
    "path": "README.MD",
    "content": "# Vudash\n\nAn open-source, configurable, extensible dashboard for monitoring, marketing, and more.\n\nNote that this project is a lerna `monorepo`, individual packages in the vudash family are under `/packages`\n\n\n[![Join the chat at https://gitter.im/vudash/vudash-core](https://badges.gitter.im/vudash/vudash-core.svg)](https://gitter.im/vudash/vudash-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/vudash/vudash.svg?branch=master)](https://travis-ci.org/vudash/vudash) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/475d7d8cff824b11bee7680de7134d94)](https://www.codacy.com/app/ant/vudash?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=vudash/vudash&amp;utm_campaign=Badge_Grade)\n[![Maintainability](https://api.codeclimate.com/v1/badges/8e7cf36d54ce0210c0ba/maintainability)](https://codeclimate.com/github/vudash/vudash/maintainability)\n[![CodeFactor](https://www.codefactor.io/repository/github/vudash/vudash/badge)](https://www.codefactor.io/repository/github/vudash/vudash)\n\nSee this project on NPM: [Vudash](https://npmjs.org/vudash)\n\n# Screenshots\n\n![dashboard](https://user-images.githubusercontent.com/218949/38768859-ca2a2ee4-3ff1-11e8-9d8c-3bf1138b563d.gif)\n![crypto](https://user-images.githubusercontent.com/218949/38768861-cf9f70b4-3ff1-11e8-91b5-ea8a27d06fb6.png)\n\n\n# Product Demo\n\n* Removed due to domain squatters.\n\nGot a dashboard you want to showcase? Let us know!\n\n# Quick Start\n\n```\nnpm -g install vudash\nvudash create\nvudash\n```\n"
  },
  {
    "path": "docs/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/README.md",
    "content": "# Vudash\n\n[![Join the chat at https://gitter.im/vudash/vudash-core](https://badges.gitter.im/vudash/vudash-core.svg)](https://gitter.im/vudash/vudash-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/vudash/vudash.svg?branch=master)](https://travis-ci.org/vudash/vudash) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/475d7d8cff824b11bee7680de7134d94)](https://www.codacy.com/app/ant/vudash?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=vudash/vudash&amp;utm_campaign=Badge_Grade)\n[![Maintainability](https://api.codeclimate.com/v1/badges/8e7cf36d54ce0210c0ba/maintainability)](https://codeclimate.com/github/vudash/vudash/maintainability)\n[![CodeFactor](https://www.codefactor.io/repository/github/vudash/vudash/badge)](https://www.codefactor.io/repository/github/vudash/vudash)\n\nA dashboard, like dashing, but written in NodeJS.\n\nVudash open source component\nWriten using hapijs, lab, material ui, socket.io, lerna, and svelte\n\n# Quick start\nIn so few lines:\n```bash\n  npm install -g vudash\n  vudash create\n  vudash\n```\n\n# Usage\nInstall as a global module `npm install -g vudash` and use `vudash create` to create an example dashboard.\nAdd new widgets under `/widgets` and add them to your dashboard under `/dashboards`.\n\nYou 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.\n\nVisiting 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.\n\n# Screenshots\n\n![dashboard](https://user-images.githubusercontent.com/218949/38768859-ca2a2ee4-3ff1-11e8-9d8c-3bf1138b563d.gif)\n![crypto](https://user-images.githubusercontent.com/218949/38768861-cf9f70b4-3ff1-11e8-91b5-ea8a27d06fb6.png)\n\n# Demo\n\n - [Demo Dashboard](http://vudash.herokuapp.com/demo.dashboard)\n - [Crypto Dashboard](http://vudash.herokuapp.com/crypto.dashboard)\n\nIf, 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.\n\n# Dashboards\nA dashboard is a collection of widgets separated into rows and columns.\n\n## Creating Dashboards\n\nDashboards are in JSON format and take the form:\n```javascript\n{\n  \"name\": \"Happy\",\n  \"layout\": {\n    \"columns\": 5,\n    \"rows\": 4\n  },\n  \"datasources\": {\n    \"datasource-exchange-rates\": { \n      \"module\": \"@vudash/datasource-rest\",\n      \"schedule\": 30000,\n      \"options\": {\n        \"url\": \"http://exchangerat.es/api/v1/rates\",\n        \"method\": \"get\",\n        \"graph\": \"rates.GBP\" \n      }\n    }\n  },\n  \"widgets\": [\n    { \"position\": {\"x\": 0, \"y\": 0, \"w\": 1, \"h\": 1}, \"widget\": \"./widgets/random\" },\n    { \"position\": {\"x\": 3, \"y\": 0, \"w\": 2, \"h\": 1}, \"widget\": \"vudash-widget-time\" },\n    { \"position\": {\"x\": 4, \"y\": 1, \"w\": 1, \"h\": 1}, \"widget\": \"./widgets/github\" },\n    { \"position\": {\"x\": 0, \"y\": 1, \"w\": 2, \"h\": 1},\n      \"widget\": \"vudash-widget-statistic\",\n      \"datasource\": \"datasource-exchange-rates\",\n      \"history\": 100,\n      \"options\": {\n        \"description\": \"EUR -> GBP\",\n      }\n    },\n    { \n      \"position\": {\"x\": 4, \"y\": 2, \"w\": 1, \"h\": 1},\n      \"widget\": \"@vudash/widget-ci\",\n      \"options\": {\n        \"schedule\": 60000,\n        \"user\": \"vudash\",\n        \"repo\": \"vudash-widget-ci\"\n      }\n    }\n  ]\n}\n\n```\nWhere '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.\n\nThe values for `position.w` and `position.h` are the number of grid units the widget occupies in width and height, respectively.\n\nThe `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.\n\nWidgets 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.\n\n### Environment variables\n\nYou can use environment variables in your dashboard or widget configuration:\n\n```javascript\n  { \n    \"position\": { ... },\n    \"widget\": \"@vudash/widget-ci\",\n    \"options\": {\n      \"user\": \"vudash\",\n      \"repo\": \"vudash-widget-ci\",\n      \"auth\": {\n        \"$env\": \"ENVIRONMENT_VARIABLE_NAME\" \n      }\n    }\n  }\n```\n\nWhere the value of `auth` in the configuration will be replaced with the contents of the environment variable `ENVIRONMENT_VARIABLE_NAME`.\n\n## Custom CSS\n\nYou can add to (or override) the CSS for a dashboard, using the `css` attribute in your dashboard's json configuration.\n\nBecause 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.\n\nAs a (rather ugly) example, lets change the dashboard's background colour to red.\n\n```javascript\n{\n  \"name\": \"dashboard-with-custom-css\",\n  \"layout\": { ... },\n  \"css\": {\n    \"body\": {\n      \"background-color\": \"red\"\n    }\n  },\n  \"datasources\": { ... },\n  \"widgets\": [ ... ]\n}\n```\n\nAs 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.\n\n# Widgets\n\nWidgets are configured as an array in the `dashboard.json` file, in the format:\n\n```javascript\n\"widgets\": [\n  {\n    \"widget\": \"./widgets/pluck\", // widget file path, node module name, or class definition\n    \"datasource\": \"datasource-xyz\", // name of a datasource listed in `datasources`\n    \"position\": {\n      \"x\": 1, // x position (row number) of widget\n      \"y\": 1, // y position (column number) of widget\n      \"w\": 1, // widget width in columns\n      \"h\": 1  // widget height in columns\n    },\n    \"options\": { // widget specific config\n      \"your\" : \"config\"\n    }\n  }\n]\n```\n\nWidgets have some optional properties:\n\n| property name | description                          | example |\n| ------------- | ------------------------------------ | ------- |\n| background    | css for \"background\" style attribute | #ffffff |\n\nFor a list of built in widgets, see [Widgets](widgets/).\nFor developing widgets see [Developing Widgets](developers/?id=developing-widgets).\n\n# Datasources\n\nUnless a widget specifies its own data fetching method, data is fetched by a datasource.\n\nDatasources are specified as a hash in the `dashboard.json` as follows:\n\n```javascript\n{\n  \"datasources\": {\n    \"datasource-id\": { // can be anything as long as it is unique\n      \"module\": \"../datasource-random\", // as with widgets, a node module name or directory\n      \"schedule\": 1000, // how often (in ms) the datasource should be refreshed\n      \"options\": { // options for the datasource\n        \"method\": \"string\"\n      }\n    }\n  }\n}\n```\n\nEach refresh, the datasource will fetch new data, and tell all widgets that listen to it about the new data.\n\nFor a list of built in datasources, see [Datasources](datasources/).\nFor developing datasources see [Developing Datasources](developers/?id=developing-datasources).\n\n# Configuration\n\nWhen running the server, a number of environment variables are available:\n\n| environment variable      | description                                                            | default value |\n| ------------------------- | ---------------------------------------------------------------------- | ------------- |\n| DEFAULT_DASHBOARD         | specify default dashboard to mount at /                                | none               |\n| DISCONNECT_RELOAD_TIMEOUT | default number of milliseconds to wait to reload if server disconnects | 30000         |\n| API_KEY                   | api key used to access the vudash api                                  | (random)      |\n| SERVER_URL                | external server url (for when node can't resolve it by itself)         | (inferred)    |\n\n# Tips and tricks\n\n## Securing your dashboard with basic auth\n\nWant to protect your dashboard from the public eye? You can secure it with basic auth in a few steps:\n\n1. Install basic-auth and http-proxy modules:\n\n```javascript\nnpm i -S basic-auth http-proxy\n```\n\n2. Change the start script in package.json\n\n```json\n{\n  \"scripts\": {\n    \"start\": \"node ./proxy\"\n  }\n}\n```\n\n3. Create a simple proxy server called `proxy.js` in your project's root directory\n\n```javascript\n'use strict'\n\nconst http = require('http')\nconst httpProxy = require('http-proxy')\nconst auth = require('basic-auth')\n\nconst proxy = httpProxy.createProxyServer()\n\nfunction verify (credentials) {\n  const user = process.env.BASIC_AUTH_NAME\n  const pass = process.env.BASIC_AUTH_PASS\n  return credentials && credentials.name === user && credentials.pass === pass\n}\n\nhttp.createServer((req, res) => {\n  const credentials = auth(req)\n \n  if (verify (credentials)) {\n    return proxy.web(req, res, {\n      target: 'http://localhost:3300'\n    })\n  }\n\n  res.statusCode = 401\n  res.setHeader('WWW-Authenticate', 'Basic realm=\"example\"')\n  res.end('Access denied')\n}).listen(process.env.PORT)\n\nprocess.env.PORT = 3300\nrequire('vudash')\n```\n\n4. When you run the project, don't forget your credentials:\n\n```bash\nBASIC_AUTH_NAME=username BASIC_AUTH_PASS=password npm run start\n```\n\n# Troubleshooting\n\n* Q. The console shows that the websocket is failing to connect, and my widgets aren't updating.\n* 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/`\n\n# Contributing\n\n## Running Tests\n\nVudash > 5 is a monorepo! This makes it easier to contribute, and keep track of all the native plugins.\n\nClone the project and run:\n\n```\nlerna bootstrap\nlerna run test\n```\n\n# Why create Vudash?\n\n* I'll get to the point. I like dashing, but I don't like ruby.\n* Both Dashing and Dashing-js are stellar efforts, but abandoned.\n* Jade is an abomination.\n* Coffeescript is an uneccessary abstraction.\n* dashing-js has a lot of bugs\n\n# Features\n\n* will happily run on heroku, now.sh, or any other hosting you fancy.\n* es6\n* all cross-origin requests done server side\n* websockets rather than polling\n* websocket fallback to long-poll when sockets aren't available\n* Custom widgets\n* Custom dashboards\n* Simple row-by-row layout\n* Super simple widget structure\n\n# Roadmap\n\n - now.sh 5-second howto\n - You, sending Pull Requests.\n - Plugins\n\n# Credits\n\n - Concept and foundation by Antony Jones / Desirable Objects Ltd\n - Contributions from github committers\n - Contains svg imagery from flaticons, by [Gregor Cresnar](http://www.flaticon.com/authors/gregor-cresnar), [Vectors Market](http://www.flaticon.com/authors/vectors-market)\n - Various fixes and improvements by [Alex Voigt](https://github.com/alex-voigt)\n"
  },
  {
    "path": "docs/_coverpage.md",
    "content": "<svg class=\"logo\" viewBox=\"0 0 176.08649 106.1699\">\n  <g transform=\"translate(-24.377661,-51.731245)\">\n    <g>\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n    </g>\n    <g transform=\"matrix(0.69851314,0,0,0.7944649,208.52468,40.27109)\">\n      <g>\n        <rect y=\"14.424998\" x=\"-177.00626\" height=\"18.785416\" width=\"37.570831\" />\n        <rect y=\"36.782299\" x=\"-177.27081\" height=\"43.65625\" width=\"37.570835\" />\n      </g>\n      <g transform=\"matrix(1,0,0,-1,41.539557,94.863551)\">\n        <rect y=\"14.424998\" x=\"-177.00626\" height=\"18.785416\" width=\"37.570831\" />\n        <rect y=\"36.782299\" x=\"-177.27081\" height=\"43.65625\" width=\"37.570835\" />\n      </g>\n    </g>\n    <g>\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <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\" />\n      <path d=\"m 149.79531,151.0302 h 0.9142 v 6.73622 h -0.9142 z\" />\n      <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\" />\n      <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\" />\n    </g>\n  </g>\n</svg>\n\n<style>\n  .logo {\n    width: 33vw;\n  }\n\n  path, rect {\n    fill: #000;\n  }\n</style>\n\n- Uses websockets for realtime updates\n- Integrates with a huge number of services\n- Familiar JSON configuration\n- Extensible datasource system\n\n[GitHub](https://github.com/vudash/vudash)\n[Get Started](#quick-start)"
  },
  {
    "path": "docs/api/README.md",
    "content": "# API\n\nVudash exposes a very simple RESTful HTTP versioned api which can be used to perform a number of operations on a running dashboard.\n\n## Versioning\n\nAPI endpoints are versioned in their url. Current versions are:\n\n`/api/v1`\n\n## Authentication\n\n### Authenticating Requests\n\nAuthentication to the api is performed by passing an `api-key` parameter as either a `header` or a `request parameter`, i.e:\n\n```bash\ncurl -X GET 'http://your.dashboard.url:3300/api/v1/something/to-do?api-key=abcde12345'\n```\n\nor\n\n```bash\ncurl -X GET --header 'api-key: abcde12345' 'http://your.dashboard.url:3300/api/v1/something/to-do'\n```\n\n### API Keys\n\nBy 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:\n\n```bash\n$ API_KEY=abcde12345 vudash\n```\n\n## Endpoints\n\nBelow is a list of operations which can be peformed via the API. This list will grow over time\n\n### PUT /api/v1/view/current\n\nChange which dashboard viewers are seeing. This affects *all viewers* of any dashboard, so use it wisely.\n\nPossible 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.\n\nAnother 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.\n\n### PUT /api/v1/dashboards/{name}\n\nDynamically add a new dashboard to vudash (or replace an existing one of the same `{name}`).\n\nThis way, you can add dashboards to a running instance of vudash without redeploying.\n\nYou 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)\n\nAn example payload might be:\n\n```json\n{\n\t\"descriptor\": {\n\t\t\"name\": \"my-dynamic-dashboard\",\n\t\t\"layout\": {\n\t\t\t\"columns\": 2,\n\t\t\t\"rows\": 2\n\t\t},\n\t\t\"datasources\": {\n\t\t\t\"ds-rnd\": {\n\t\t\t\t\"module\": \"../datasource-random\",\n\t\t\t\t\"schedule\": 1000,\n\t\t\t\t\"options\": {\n\t\t\t\t\t\"method\": \"natural\",\n\t\t\t\t\t\"options\": {\n\t\t\t\t\t\t\"max\": 100000\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"widgets\": [{\n\t\t\t\"position\": {\n\t\t\t\t\"x\": 0,\n\t\t\t\t\"y\": 0,\n\t\t\t\t\"w\": 2,\n\t\t\t\t\"h\": 2\n\t\t\t},\n\t\t\t\"widget\": \"../widget-statistic\",\n\t\t\t\"datasource\": \"ds-rnd\"\n\t\t}]\n\t}\n}\n```\n\nYou can then visit the new dashboard as you normally would at `http://<vudash-url>/<name>.dashboard`.\n\nNote that dynamically added dashboards are *in memory* and do not survive server restarts.\n\nIf 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"
  },
  {
    "path": "docs/datasources/README.md",
    "content": "# Datasources\n\n## What is a Datasource\n\nA datasource provides the mechanism for widgets to recieve the information they show.\n\n## How to add a datasource to the dashboard\n\nIn this guide, we'll install the `value` datasource and use it in a widget.\n\n1. Firstly, install the module required:\n  ```\n  npm install --save @vudash/datasource-value\n  ```\n2. 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.\n  ```\n  { \n    \"datasources\": {\n      \"my-datasource-id\": {\n        \"module\": \"@vudash/datasource-value\",\n        \"schedule\": 30000,\n        \"options\": {\n          \"value\": \"12345\"\n        }\n      }\n    }\n  }\n  ```\n3. 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:\n```\n  \"widgets\": [\n    ...,\n    { \n      \"position\": ...,\n      \"widget\": \"vudash-widget-statistic\",\n      \"datasource\": \"my-datasource-id\",\n      \"options\": {\n        \"description\": \"Some Description\"\n      }\n    }\n  ]\n```\n4. You're good to go! Your widget will now use the `value-datasource` to fetch its data.\n\n## Shared datasource configuration\n\nWhen 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:\n\n ```javascript\n  { \n    \"datasources\": {\n      \"datasource-id\": {\n        \"module\": \"some-datasource-npm-package-name\",\n        \"options\": {\n          \"number\": 1,\n          \"foo\": \"bar\"\n        }\n      }\n    }\n  }\n```\n\n## Provided Datasources\n\n### Benefits\n\nVudash Datasources are referenced using the `datasource` attribute of a widget.\n\nThis saves time for a widget developer, and means that any widget can easily fetch data from a number of different sources.\n\n### Supported sources\n\n| Datasource name  | Source of data                                                       | Documentation                                        |\n|------------------|----------------------------------------------------------------------|------------------------------------------------------|\n|  value           | config ```{ value: <value> }```                                      | [Value Datasource](#value-datasource)\n|  random          | [chance.natural({ min: 0, max: 999})](http://chancejs.org)           | [Random Datasource](#random-datasource)\n|  rest            | http(s) using [request](http://requestjs.org)                        | [REST Datasource](#REST-datasource)\n|  google-sheets   | [Google Sheets](http://drive.google.com)                             | [Google Sheets Datasource](#google-sheets-datasource)\n\n### Usage in widgets\n\nWhen 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.\n\nIn `dashboard.json`\n\n1. Add the datasource under the `datasource` section. The `options` will contain the configuration for the datasource:\n```javascript\n\"datasource\": {\n  \"datasource-id\": { \n    \"module\": \"datasource-package-name\",\n    \"options\": {\n      \"url\": \"http://example.com/some/api\",\n      \"method\": \"get\"\n    }\n  }\n}\n```\n2. Tell the widget to use the datasource, by modifying your widget entry under `widgets`:\n```javascript\n  {\n    \"position\": { ... },\n    \"widget\": \"some-widget\",\n    \"datasource\": \"datasource-id\",\n    \"options\": {\n      ...\n    }\n  }\n```\n\nConfiguration is validated when the datasources are registered by the dashboard, if a datasource supports it.\n\n### Value datasource\n\nThe Vudash Value Datasource returns hardcoded values.\n\n#### Basic Config\n\nSimply specify the value you want returned.\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-value\",\n  \"options\": {\n    \"value\": 2\n  }\n}\n```\n\nReturns the number 2.\n\n#### Arrays and Objects\n\nValue Datasource can return anything you can provide in JSON\n\n\nSimply specify the value you want returned.\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-value\",\n  \"options\": {\n    \"value\": { \"x\": [{ \"y\": [1,2,3,4,5] }, { \"z\": false }] }\n  }\n}\n```\n\nWill return the object specified by 'value'.\n\n### REST datasource\n\nThe Vudash REST Datasource allows fetching data from external APIs.\n\n#### Basic Config\n\nThe default method is GET. Simply specify an URL.\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-rest\",\n  \"options\": {\n    \"url\": \"http://example.com/some/api\"\n  }\n}\n```\n\n#### POSTing data\n\nSay you wanted to POST to the endpoint `/v1/api` at `https://example.org` on port 3333.\nFurthermore, you want to send JSON request data as specified in \"body\" below.\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-rest\",\n  \"options\": {\n    \"method\": \"post\",\n    \"url\": \"https://example.org:3333/v1/api\",\n    \"body\": {\n      \"foo\": \"bar\",\n      \"one\": 2,\n      \"three\": false\n    }\n  }\n}\n```\n\n#### Query Parameters\n\nSay you wanted to POST to the endpoint `/v1/api` at `https://example.org` on port 3333.\nFurthermore, you want to send JSON request data as specified in \"query\" below.\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-rest\",\n  \"options\": {\n    \"method\": \"get\", // optional\n    \"url\": \"https://example.net/v1/api\",\n    \"query\": {\n      \"param1\":\"foo\",\n      \"param2\":\"bar\"\n    }\n  }\n}\n```\n\n#### Parsing data\n\nVudash 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.\n\n## Troubleshooting SSL\n\nYou might encounter an error when trying to fetch data from SSL protected servers, such as:\n\n```bash\nError in widget datasource-rest (461305c2) { RequestError\n    at ClientRequest.req.once.err (/home/aj/Projects/vudash-core/packages/core/node_modules/got/index.js:73:21)\n    at Object.onceWrapper (events.js:291:19)\n    at emitOne (events.js:96:13)\n    at ClientRequest.emit (events.js:189:7)\n    at TLSSocket.socketErrorListener (_http_client.js:358:9)\n    at emitOne (events.js:96:13)\n    at TLSSocket.emit (events.js:189:7)\n    at emitErrorNT (net.js:1280:8)\n    at _combinedTickCallback (internal/process/next_tick.js:74:11)\n    at process._tickDomainCallback (internal/process/next_tick.js:122:9)\n  code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',\n  message: 'unable to verify the first certificate',\n  host: 'ssl.example.com',\n  hostname: 'ssl.example.com',\n  method: 'GET',\n  path: '/health' }\n\n```\n\nThis means that you don't have the correct root CA certificates for node to connect to the endpoint.\n\nThis is easily fixed, if you are using node > 7.3 however:\n\n1. Find out who the Root CA for the domain you are trying to connect to, using [an ssl analysis tool](https://sslanalyzer.comodoca.com/)\n1. Download the root CA's pem file and drop it in your project folder.\n1. 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`\n\nMore details on this [can be found here](https://git.daplie.com/Daplie/node-ssl-root-cas)\n\n### Random datasource\n\nThe Vudash Random Datasource returns hardcoded values.\n\n#### Basic Config\n\nVudash Random Datasource uses ChanceJS underneath. That means you can it bare, to generate a random integer:\n\n```javascript\n{\n  \"source\": \"random\"\n}\n```\n\n#### Custom Chance Methods\n\nor you can define the method used to generate your random data:\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-random\",\n  \"options\": {\n    \"method\": \"string\"\n  }\n}\n```\n\n#### Chance Methods with Custom Parameters\n\nor 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.\n\nYou 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?\n\nThe code used below is the equivalent of calling [chance.n(chance.integer, 12, {min: 15, max: 32})](http://chancejs.com/#n).\n\n```javascript\n{\n  \"module\": \"@vudash/datasource-random\",\n  \"options\": {\n    \"method\": \"n\",\n    \"options\": [\n      \"integer\",\n      12,\n      [{\n        min: 15,\n        max: 32\n      }]\n    ]\n  }\n}\n```\n\nGenerates an array of 12 integers between 15 and 32."
  },
  {
    "path": "docs/developers/README.md",
    "content": "# Developing\n\nCreating 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.\n\n## Developing Widgets\n\nA 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.\n\nA widget is packaged as a node module, but a node module can simply be a folder with a `package.json` file.\n\nA widget is simply a node module, and really only needs a couple of files.\n\n### package.json\n```javascript\n  { \n    \"name\": \"vudash-widget-example\", \n    \"main\": \"widget.js\",\n    \"vudash\": {\n      \"component\": \"somefile.html\"\n    }\n  }\n```\nThe `main` js file above should reference your main module class, in this example we call it `widget.js`\n\nThe `vudash.component` is a single file [SvelteJS](http://svelte.technology) component that is an all-in-one (html, css, js),\nview-component with an immutable-data-tree based state model.\n\n### Writing the server-side component\n```javascript\n'use strict'\n\nconst moment = require('moment')\n\nclass TimeWidget {\n\n  constructor (options, emitter) {\n    this.options = options // options is the configuration passed under \"options\" in your dashboard.json\n    this.emitter = emitter // emitter is only useful if you want to emit events yourself (see below)\n  }\n\n  /**\n   * This method is called by the datasource when it gets new data.\n   **/\n  update (data) {\n    const now = moment()\n    return {\n      time: now.format('HH:mm:ss'),\n      date: now.format('MMMM Do YYYY')\n    } // just return the data you want to display on the dashboard\n  }\n}\n\nexports.register = function (options, emitter) {\n  return new TimeWidget(options, emitter)\n}\n```\n* The first parameter to register is the widget configuration given in the `dashboard.json` file\n* 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.\n\n### Writing the client side component\n\nClient side components are defined using [svelte](https://svelte.technology/) which allows you to build framework-independent client side components with ease.\n\nCreate 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.\n\nIn 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`.\n\n#### Example of a component\n\npackage.json\n```javascript\n{\n  \"name\": \"vudash-widget-health\",\n  \"main\": \"widget.js\",\n  \"vudash\": {\n    \"component\": \"./component.html\"\n  }\n}\n```\n\ncomponent.html\n```html\n<h1 class=\"vudash-hello\">{{ greeting }}</h1>\n<style>\n  .vudash-hello {\n    text-align: right;\n  }\n</style>\n<script>\n  export default {\n    data () {\n      return {\n        greeting: 'hello'\n      }\n    },\n\n    methods: {\n      /**\n      * This is the really important bit.\n      * This update method is called whenever the widget emits data.\n      *\n      * data: the actual update data given by the datasource\n      * meta: metadata about the update. This currently contains 'updated' which is the data fetch time.\n      * history: the last x events, where `x` is defined by the 'history' option in the widget's config.\n      **/\n      update ({ data, meta, history }) {\n        this.set(data)\n      }\n    }\n  }\n</script>\n```\n\nSee the [Svelte Documentation](https://svelte.technology/guide) for information on how to build svelte components.\n\n#### Third party dependencies\n\nAll components and their dependencies are processed by `rollup` and bundled into a browser-friendly script.\n\nYou 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:\n\n```bash\nnpm install thing-maker\n```\n\nThen, import it to your component:\n\n```html\n<span>Hello {{ thing }}</span>\n<script>\n  import { world } from 'thing-maker'\n\n  export default {\n    data () {\n      return {\n        thing: world()\n      }\n    }\n  }\n</script>\n```\n\nIt 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.\n\n#### Images\n\nYou 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:\n\n<img alt=\"logo\" src=\"{{ logo }}\" />\n<script>\n  import { logo } from './logo.svg'\n\n  export default {\n    data () {\n      return {\n        logo\n      }\n    }\n  }\n</script>\n\nYou can [read more about Rollup.js](https://rollupjs.org/) in order to better understand how to optimise your component's client side code.\n\n### Using datasources\n\nDatasources 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.\n\n#### Benefits\n\n  * Consumer chooses where your widget gets its data\n  * Don't need to implement any data fetching code yourself\n  * Focus your widget on displaying data, not fetching it\n  * Shared configuration for consumers, datasources are configured globally, and/or on a widget level.\n\n#### How to\n\n1. 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.\n\n```javascript\n  update(value) {\n    // do something with the value, like add an emoji!\n    return `🌟 ${value}`\n  }\n```\n\n1. 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.\n\n```javascript\n    update ({ data, meta, history }) {\n      this.set({ someValue: data.myValue })\n    }\n```\n1. Then simply use it in your markup\n```html\n<h1>{{ someValue }}</h1>\n```\n\n### Writing a component without a datasource\n\nYour 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:\n\n```javascript\n'use strict'\n\nclass HealthWidget {\n  constructor (options, emitter) {\n    this.emitter = emitter\n    this.on = false\n\n    this.timer = setInterval(function () {\n      this.run()\n    }.bind(this), options.schedule || 1000)\n    this.run()\n  }\n\n  run () {\n    this.on = !this.on\n    this.emitter.emit('update', { on: this.on })\n  }\n\n  destroy () {\n    clearInterval(this.timer)\n  }\n}\n\nexports.register = function (options, emitter) {\n  return new HealthWidget(options, emitter)\n}\n\n// Validation is optional\nexports.validation = Joi.object({\n  'some-option': Joi.string().required()\n})\n```\n\nImportant things to note here are:\n\n* 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.\n\n* We have to call our `run()` method somehow to update the data. This is done using a `setInterval`\n\n* In case the user doesn't configure a schedule for the widget in its `options`, we default to 1000ms.\n\n* 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.\n\n* 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.\n\n## Developing Datasources\n\nTBC - for now, have a look at the source code in `vudash/packages/datasource-*`\n\n## Developing Transformers\n\nData 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.\n\nThe format of a transformer is relatively simple. Here's a very simple transformer:\n\n```javascript\n'use strict'\n\nclass AddingTransformer {\n  constructor (numberToAdd) {\n    this.numberToAdd = numberToAdd\n  }\n\n  transform (data) {\n    return data + this.numberToAdd\n  }\n}\n\nmodule.exports = AddingTransformer\n```\n\nA transformer takes the configuration provided by the dashboard (//widgets[]/transformations) as its only constructor argument.\n\nWhen 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.\n\nYou might configure the transformer as part of the following dashboard:\n\n```javascript\n  { \n    \"datasources\": {\n      \"my-datasource-hundred\": {\n        \"module\": \"@vudash/datasource-value\",\n        \"options\": {\n          \"value\": 100\n        }\n      }\n    }\n  },\n  {\n    \"datasource\": \"my-datasource-hundred\",\n    \"transformations\": [\n      {\n        \"transformer\": \"./adding-transformer.js\",\n        \"options\": 100\n      }\n    ],\n    \"options\": {\n      \"description\": \"Shows two-hundred\"\n    },\n    \"widget\": \"vudash-widget-statistic\"\n  }\n```\n\nThe resulting dashboard widget would show the value `200` as the value `100` is transformed by adding `100` to it.\n\n## Dashboard Events\n\nEvents can be emitted using the event emitter which is passed into the register method. These events will cause dashboard-wide actions to happen.\n\n```\nemit('plugin', 'audio:play', {data: data})\n```\n\nThe current list of events that can be triggered are:\n\n| Event         | Data             | Description                                                         |\n| ------------- |------------------| --------------------------------------------------------------------|\n| audio:play    | `{ data: data }` | Plays an audio clip (once). `data` is a data-uri of the audio file. |\n\n## Working on the Vudash project\n\nVudash uses\n* ES6\n* Svelte\n* StandardJS\n\nContributions are always welcome, please open a PR."
  },
  {
    "path": "docs/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Vudash</title>\n  <meta name=\"description\" content=\"Description\">\n  <meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n  <link rel=\"stylesheet\" href=\"//unpkg.com/docsify/themes/vue.css\">\n  <style>\n    @import url('https://fonts.googleapis.com/css?family=Gruppo');\n  </style>\n</head>\n<body>\n  <nav>\n    <a href=\"#/\">Vudash</a>\n    <a href=\"#/widgets/\">Widgets</a>\n    <a href=\"#/datasources/\">Datasources</a>\n    <a href=\"#/transformers/\">Transformers</a>\n    <a href=\"#/developers/\">Developers</a>\n    <a href=\"#/api/\">API</a>\n  </nav>\n  <div id=\"app\">\n  </div>\n</body>\n<script>\n    window.$docsify = {\n      coverpage: true,\n      ga: 'UA-117504415-1',\n      name: 'vudash',\n      search: {\n        noData: {\n          '/': 'No results!'\n        },\n        paths: 'auto',\n        placeholder: {\n          '/': 'Search'\n        }\n      }\n    }\n  </script>\n<script src=\"//unpkg.com/docsify/lib/docsify.min.js\"></script>\n<script src=\"//unpkg.com/docsify/lib/plugins/search.min.js\"></script>\n<script src=\"//unpkg.com/docsify/lib/plugins/ga.min.js\"></script>\n</html>"
  },
  {
    "path": "docs/transformers/README.md",
    "content": "# Transformers\n\nTransformers allow you to retrieve information from a data source or widget, and modify it before it is sent to the dashboard.\n\n## How to install a transformer\n\nTransformers, like widgets and datasources, are just node modules. Install the module required:\n\n```bash\nnpm install --save @vudash/transformer-map\n```\n\n## Provided Transformers\n\nWe provide a selection of transformers, or you can [write your own](/#/developers), very simply.\n\n### Map Transformer (@vudash/transformer-map)\n\nMaps json data from one structure to another.\n\nSelection 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.\n\nYou should consult the [reorient](https://www.npmjs.com/package/reorient) documentation for advanced mapping features such as default values, but in a pinch:\n\nSay that your desired API returns the following payload in JSON:\n\n```javascript\n{ \n  \"one\": {\n    \"two\": {\n      \"three\": \"abcde\"\n    }\n  }\n}\n```\n\nLets say we wanted the value of \"three\" buried down in the middle there. It's easy:\n\n```javascript\n{\n  \"position\": {\n    ...\n  },\n  \"datasource\": \"ds-rest\",\n  \"transformations\": [\n    {\n      \"transformer\": \"@vudash/transformer-map\",\n      \"options\": {\n        \"value\": \"one.two.three\"\n      }\n    }\n  ],\n  \"options\": {\n    \"description\": \"Value of three\"\n  },\n  \"widget\": \"vudash-widget-statistic\"\n}\n```\n\nAnd if you actually want the contents of two? (As an object of course):\n\n```javascript\n{\n  \"position\": {\n    ...\n  },\n  \"datasource\": \"ds-rest\",\n  \"transformations\": [\n    {\n      \"transformer\": \"@vudash/transformer-map\",\n      \"options\": {\n        \"value\": \"one.two\"\n      }\n    }\n  ],\n  \"options\": {\n    \"description\": \"Value of two\"\n  },\n  \"widget\": \"vudash-widget-statistic\"\n}\n```\n\n### JQ Transformer (@vudash/transformer-jq)\n\nThe 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.\n\nUsage is a matter of compiling a selector to modify the data as you please\n\nSupposing your data looks like this:\n\n```json\n{\n  \"a\": {\n    \"big\": {\n      \"json\": {\n        \"jeff\": {\n          \"email\": \"jeff@example.net\"\n        },\n        \"joe\": {\n          \"phone\": \"+447721981546\"\n        },\n        \"emma\": {\n          \"email\": \"emma@example.com\"\n        },\n        \"grayson\": {\n          \"email\": \"wailo@example.net\"\n        }\n      }\n    } \n  }\n}\n```\n\nYou could use the following configuration to group the above users by email domain.\n\n```javascript\n{\n  \"position\": {\n    ...\n  },\n  \"datasource\": \"ds-rest\",\n  \"transformations\": [\n    {\n      \"transformer\": \"@vudash/transformer-jq\",\n      \"options\":  {\n        \"value\": \"filter(has('email')) | groupBy(flow(get('email'), split('@'), get(1)))\"\n      }\n    }\n  ],\n  \"options\": {\n    \"description\": \"People grouped by email domain\"\n  },\n  \"widget\": \"vudash-widget-statistic\"\n}\n```"
  },
  {
    "path": "docs/widgets/README.md",
    "content": "# Predefined widgets\n\nVudash has a number of widgets which are available on npm, these are in the `packages/` directory of the monorepo, and also available on npm.\n\nYou 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.\n\n## Chart Widget\n\nShows Bar, Line, Chart, and Donut graphs for data series.\n\n### Screenshot\n\nLine Chart\n\n![line-chart](https://cloud.githubusercontent.com/assets/218949/25781884/57c8d264-3337-11e7-8e46-ae6737d20f50.png)\n\n### Configuration\n\nThe chart widget has a number of configuration options:\n\n| Option | Default | Allowed Values | Description |\n| --- | --- |\n| `description` | `<empty string>` | any string | Widget description, shown at the bottom of the widget\n| `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. |\n| `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. |\n\n#### Configuration example\n\nYou can configure any status page which uses [Atlassian StatusPage](https://www.atlassian.com/software/statuspage) easily:\n\n```javascript\n{ \n  \"position\": { ... }, \n  \"widget\": \"@vudash/widget-chart\",\n  \"datasource\": { ... },\n  \"options\": {\n    \"type\": \"pie\",\n    \"description\": \"My Pie Chart\",\n    \"labels\": [\"Apples\", \"Pears\", \"Peaches\", \"Lemons\", \"Oranges\"]\n  }\n}\n```\n\n## CI Widget\n\nConnects to CI Providers and displays build results.\n\nCurrently supports [CircleCI](http://www.circleci.com) and [TravisCI](http://www.travis-ci.org)\n\n### Screenshot\n\nBuilding:\n\n![ci-widget 1](https://cloud.githubusercontent.com/assets/218949/25973853/c90fe034-369d-11e7-80ef-639126d7e1dd.gif)\n\nFailing, and Passing:\n\n![ci-widget](https://cloud.githubusercontent.com/assets/218949/25781907/d0242da8-3337-11e7-904d-9c6f00b7ea27.gif)\n\n### Configuration\nAdd to a [Vudash](https://www.npmjs.com/package/vudash) dashboard with the following configuration:\n\n#### Simple Configuration\n\nThe simplest configuration is very straightforward\n\n```javascript\n  { \"position\": { ... },\n    \"widget\": \"vudash-widget-ci\",\n    \"datasource\": \"some-data-source-id\",\n    \"options\": {\n      \"provider\": \"circleci\",\n      \"repo\": \"some-repo\",\n      \"user\": \"some-user\"\n    }\n  }\n```\n\n#### Display\n\nYou can turn the display of the repository owner on and off, which might be useful if all your repositories belong to a single organisation:\n\n```javascript\n  { \"position\": { ... },\n    \"widget\": \"vudash-widget-ci\",\n    \"datasource\": \"some-data-source-id\",\n    \"options\": {\n      \"hideOwner\": true\n      ...\n    }\n  }\n```\n\n#### Build Noises\n\n```javascript\n      {\n        \"widget\": \"vudash-widget-ci\",\n        \"options\": {\n          \"provider\": \"travis\", // CI Provider (travis or circleci)\n          \"user\": \"your-user\", // username, mandatory\n          \"repo\": \"your-repo\", // repository name, mandatory\n          \"branch\": \"your-branch\" // branch to monitor, optional.\n          \"schedule\": 60000 // Update frequency in MS (optional),\n          \"sounds\": { // Sound to play on build state changes (optional)\n            \"passed\": \"/some/local/path/sound.ogg\",\n            \"failed\": \"data:audio/ogg;base64, ...\",\n            \"unknown\": \"data:audio/ogg;base64, ...\"\n          },\n          \"options\": {\n            \"auth\": \"xxx\" // circleci auth token, only required for circleci\n          }\n        }\n      }\n```\n\nWhere `your-user` is your github organisation or user name, and `your-repo` is your build/repository name.\n\n* The travis plugin currently only deals with public repositories (i.e. travis-ci.org, not .com)\n\n## Gauge Widget\n\nShows a VU-Meter like Gauge which represents numerical figures like percentages\n\n### Screenshot\n\n![gauge-widget](https://cloud.githubusercontent.com/assets/218949/25781923/339cd844-3338-11e7-8e12-0ff197e3876f.gif)\n\n### Configuration\nSimply include in your dashboard, and configure as required:\n\n```javascript\n  {\n    \"widget\": \"vudash-widget-statistic\",\n    \"datasource\": \"some-data-source-id\",\n    \"options\": {\n      \"description\": \"Gauge\", // Optional. Description shown below statistic,\n      \"maximum\": 3983 // Required, the maximum value the gauge can ever reach\n    }\n  }\n```\n\n## Progress Widget\n\nSimilar to VU Meter, but with a linear progress bar\n\n### Screenshot\n\n![progress](https://cloud.githubusercontent.com/assets/218949/25974011/81164970-369e-11e7-83b3-c42e87febabb.png)\n\n### Configuration\n\nThe only configuration for the progress widget is the description.\n\n```javascript\n  {\n    \"widget\": \"vudash-widget-progress\"\n    \"datasource\": \"some-data-source-id\",\n    \"options\": {\n      \"description\": \"Stuff\", // Optional. Default \"Progress\" Description shown below statistic\n    }\n  }\n```\n\nThe datasource connected to the progress widget should ideally return numbers between 1 and 100, anything over 100 will be represented as 100% anyway.\n\n## Statistics Widget\n\nShows a statistic, which can be a number, a word, or anything else representable on screen.\n\nOptionally can draw a graph of the previous results behind the main one.\n\n### Screenshot\n![stats widget](https://cloud.githubusercontent.com/assets/218949/20489789/adb964ca-b003-11e6-917b-c07218625bd3.png)\n\n### Configuration\nSimply include in your dashboard, and configure as required:\n\n```javascript\n  {\n    \"widget\": \"vudash-widget-statistic\",\n    \"datasource\": \"some-data-source-id\",\n    \"options\": {\n      \"description\": \"Visitor Count\", // Optional. Default \"Statistics\" Description shown below statistic,\n      \"format\": \"%s\", // Optional. Default %s. Format the incoming data (using sprintf-js),\n      \"font-ratio\": 4 // Optional. Default 4. Scaling ratio for main statistic (for longer text, increase this number),\n      \"colour\": \"#86797d\", // Optional. Defaults to a random colour from a pre-selected \"pretty\" list. Colour for line / fill-area of graph, if shown.\n      \"historyView\": \"chart\" // Optional, defaults to \"chart\". How historical figures are represented. \n    }\n  }\n```\n\nNote that `datasource` tells the widget how to get data, and is using a datasource, which is documented in the [Datasources documentation](/#/datasources)\n\n#### History Views\n\nThere are two ways to represent previous values that the widget has received:\n\n* `chart`: Displays a line-graph of the previous historical values which floats behind the widget's content.\n* `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.\n\n#### Graphs\nThis widget will graph data which is passed in as an array.\n\nThis means that if your data-source resolves an array of numbers as data, the last number in the array \nwill be shown as the statistic value, and a line graph will be drawn behind the widget using the remaining numbers.\n\nFor example\n\n`[1,2,3,4,5,6,7]` will result in a widget value of 7, and a graph of 1-6 behind it.\n\n## Status Widget\n\nShows the status of an external service like github, or any API which uses Atlassian StatusPage\n\n### Screenshot\n\n![status-widget](https://cloud.githubusercontent.com/assets/218949/25781933/62f3e344-3338-11e7-9cf8-dc7b29aa1a98.png)\n\n### Configuration\n\nCurrently this widget has two integrations.\n\n#### Atlassian Statuspage\n\nYou can configure any status page which uses [Atlassian StatusPage](https://www.atlassian.com/software/statuspage) easily:\n\n```javascript\n{ \n  \"position\": { ... }, \n  \"widget\": \"vudash-widget-status\",\n  \"datasource\": \"some-data-source-id\",\n  \"options\": {\n    \"schedule\": 300000,\n    \"type\": \"statuspageio\",\n    \"config\": {\n      \"url\": \"https://status.newrelic.com/\", // URL to the status page\n      \"components\": [ // List the names of components you want to monitor the status of\n        \"APM\",\n        \"Data Collection\",\n        \"Alerts\"\n      ]\n    }\n  }\n}\n```\n\n#### Github\n\nGithub status page monitoring is no-configuration. It will tell you when it is up, down, or otherwise.\n\n```javascript\n{ \n  \"position\": { ... }, \n  \"datasource\": \"some-data-source-id\",\n  \"widget\": \"vudash-widget-status\",\n  \"options\": {\n    \"schedule\": 300000,\n    \"type\": \"github\"\n  }\n}\n```\n\n## Time Widget\n\nSimply shows the time, and has optional audiable alarams\n\n### Screenshot\n![time-widget](https://cloud.githubusercontent.com/assets/218949/25781881/50fffa66-3337-11e7-89dc-12871a2350b8.png)\n\n### Configuration\nSimply include in your dashboard:\n\n```javascript\n  {\n    \"widget\": \"vudash-widget-time\",\n    \"options\": { ... }  \n  }\n```\n\n#### Timezone support\nThe timezone can be set via configuration. The list of allowed timezones is that of the `moment-timezone` library.\n\n```javascript\n  \"options\": {\n    \"timezone\": \"Europe/London\"\n  }\n```\n\n#### Alarms\nThis widget can play sounds! Simply pass 'alarms' into your configuration:\n\n```javascript\n  \"options\": {\n    \"alarms\": [\n      {\n        \"expression\": \"5 * * * * *\",\n        \"actions\": [\n          {\n            \"action\": \"sound\",\n            \"options\": {\n              \"data\": \"data:audio/ogg;base64, ...\"\n            }\n          }\n        ]\n      }\n    ]\n  }\n```\n\n`expression` is a cron expression which determines when the sound will be played.\n`actions` is the action to perform when the alarm is triggered. Supported actions are listed below:\n\n#### Actions\nAction: `sound`\nOptions: `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.\n\n## Health Widget\n\nA simple widget with a beating heart, to let you know that the dashboard is alive.\n\n### Screenshot\n\n![health-widget](https://cloud.githubusercontent.com/assets/218949/25781948/a0314bfc-3338-11e7-99a9-3d81065b0518.png)\n\n### Configuration\n\nThere is no configuration for this widget. It runs every second, and the heart will change shape.\n\nIf the heart stops... your vudash client's websocket has become disconnected from the backend, and your data is out of date.\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"lerna\": \"2.0.0-beta.36\",\n  \"packages\": [\n    \"packages/*\"\n  ],\n  \"version\": \"9.9.0\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"scripts\": {\n    \"lint\": \"lerna run lint\",\n    \"start\": \"lerna run start --scope vudash\",\n    \"heroku-postbuild\": \"./node_modules/.bin/lerna bootstrap\",\n    \"docs:preview\": \"docsify serve docs\",\n    \"postinstall\": \"lerna bootstrap --hoist\"\n  },\n  \"devDependencies\": {\n    \"chance\": \"^1.0.4\",\n    \"cheerio\": \"^0.22.0\",\n    \"code\": \"^4.0.0\",\n    \"docsify\": \"^4.6.10\",\n    \"docsify-cli\": \"^4.2.1\",\n    \"glob\": \"^7.0.6\",\n    \"lerna\": \"2.0.0-beta.36\",\n    \"marked\": \"^0.3.19\",\n    \"mocha\": \"^4.0.1\",\n    \"nock\": \"^9.0.2\",\n    \"nodemon\": \"^1.9.2\",\n    \"prismjs\": \"^1.14.0\",\n    \"sinon\": \"^1.17.4\",\n    \"sinon-as-promised\": \"^4.0.2\",\n    \"standard\": \"^11.0.1\"\n  },\n  \"engines\": {\n    \"node\": \">=9.x\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  },\n  \"dependencies\": {\n    \"app-module-path\": \"^2.2.0\",\n    \"bluebird\": \"^3.5.2\",\n    \"boom\": \"^6.0.0\",\n    \"browser-sync\": \"^2.24.7\",\n    \"buble\": \"^0.19.3\",\n    \"catbox-memory\": \"^3.1.2\",\n    \"chartist\": \"^0.11.0\",\n    \"circleci\": \"^0.3.3\",\n    \"cron\": \"^1.4.1\",\n    \"export-dir\": \"^0.1.2\",\n    \"fit-text\": \"^2.0.1\",\n    \"fs-extra\": \"^0.30.0\",\n    \"handlebars\": \"^4.0.12\",\n    \"hapi\": \"^16.6.3\",\n    \"hapi-api-secret-key\": \"^1.1.0\",\n    \"inert\": \"^4.2.1\",\n    \"izitoast\": \"^1.4.0\",\n    \"joi\": \"^13.7.0\",\n    \"jq.node\": \"^2.1.1\",\n    \"json-to-css\": \"^0.1.0\",\n    \"moment\": \"^2.22.2\",\n    \"moment-timezone\": \"^0.5.21\",\n    \"npm\": \"^5.10.0\",\n    \"npm-programmatic\": \"0.0.8\",\n    \"ora\": \"^1.4.0\",\n    \"reorient\": \"^2.1.0\",\n    \"rollup\": \"^0.43.1\",\n    \"rollup-plugin-buble\": \"^0.19.2\",\n    \"rollup-plugin-commonjs\": \"^8.4.1\",\n    \"rollup-plugin-node-resolve\": \"^3.4.0\",\n    \"rollup-plugin-postcss\": \"^0.4.3\",\n    \"rollup-plugin-svelte\": \"^4.3.2\",\n    \"rollup-plugin-svg\": \"^1.0.1\",\n    \"rollup-plugin-uglify-es\": \"0.0.1\",\n    \"rollup-plugin-virtual\": \"^1.0.1\",\n    \"slash\": \"^1.0.0\",\n    \"socket.io\": \"^2.1.1\",\n    \"spreadsheet-to-json\": \"^1.3.1\",\n    \"svelte\": \"^1.64.1\",\n    \"travis-ci\": \"^2.2.0\",\n    \"unhandled-rejection\": \"^1.0.0\",\n    \"vision\": \"^4.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "[![Join the chat at https://gitter.im/vudash/vudash-core](https://badges.gitter.im/vudash/vudash-core.svg)](https://gitter.im/vudash/vudash-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/vudash/vudash.svg?branch=master)](https://travis-ci.org/vudash/vudash) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)\n\n# Vudash\nA dashboard, like dashing, but written in NodeJS.\n\n## Documentation\n\nDocumentation has moved [here](http://vudash.github.io/vudash/)\n\n## What does it look like?\n\n![dashboard](https://cloud.githubusercontent.com/assets/218949/18632967/05d72ba6-7e72-11e6-964d-6de1f38135ac.png)\n![graph](https://cloud.githubusercontent.com/assets/218949/18608448/68c9bf90-7ce1-11e6-95a9-15c864722271.png)\n\n## Demo\n\nhttp://vudash.herokuapp.com/demo.dashboard\n\n## Features\n* will happily run on a free heroku instance\n* es6\n* all cross-origin requests done server side\n* websockets rather than polling\n* websocket fallback to long-poll when sockets aren't available\n* Custom widgets\n* Custom dashboards\n* Simple row-by-row layout\n* Dashboard arrangement is simply the config order (see below)\n* Super simple widget structure"
  },
  {
    "path": "packages/core/app.js",
    "content": "'use strict'\n\nconst { register, start, stop } = require('./src/server')\n\nlet server\n\nregister()\n  .then(registered => {\n    server = registered\n    start(server)\n  })\n\nprocess.on('SIGUSR2', () => {\n  stop(server)\n    .then(() => {\n      process.exit()\n    })\n})\n"
  },
  {
    "path": "packages/core/bin/vudash.js",
    "content": "#!/usr/bin/env node\n\nconst create = require('../src/cli/create')\nconst help = require('../src/cli/help')\nconst logo = require('../src/cli/logo')\n\nconst [ , , arg ] = process.argv\n\nlogo.run()\n\nif (!arg) {\n  require('../app')\n}\n\nif (arg === 'create') {\n  create.run()\n}\n\nif (['help', '--help'].includes(arg)) {\n  help.run()\n}\n"
  },
  {
    "path": "packages/core/dashboards/simple.json",
    "content": "{\n  \"name\": \"simple-dashboard\",\n  \"layout\": {\n    \"columns\": 5,\n    \"rows\": 4\n  },\n  \"datasources\": {\n    \"ds-rnd\": {\n      \"module\": \"../datasource-random\",\n      \"schedule\": 1000,\n      \"options\": {\n        \"method\": \"string\"\n      }\n    }\n  },\n  \"widgets\": [\n    {\n      \"position\": {\"x\": 3, \"y\": 0, \"w\": 2, \"h\": 1},\n      \"widget\": \"../widget-statistic\",\n      \"datasource\": \"ds-rnd\"\n    }\n  ]\n}"
  },
  {
    "path": "packages/core/dashboards/template.json",
    "content": "{\n  \"name\": \"simple-dashboard\",\n  \"layout\": {\n    \"columns\": 5,\n    \"rows\": 4\n  },\n  \"widgets\": [\n    { \"position\": { \"x\": 1, \"y\": 0, \"w\": 3, \"h\":1 }, \"widget\": \"vudash-widget-time\" }\n  ]\n}\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"vudash\",\n  \"version\": \"9.9.0\",\n  \"keywords\": [\n    \"vudash\",\n    \"dashboard\",\n    \"dashing\",\n    \"analytics\",\n    \"monitoring\",\n    \"websockets\",\n    \"geckoboard\",\n    \"widget\",\n    \"stats\",\n    \"dash\",\n    \"dashing-js\",\n    \"statistics\",\n    \"big screen\",\n    \"display\",\n    \"home automation\",\n    \"automation\",\n    \"ha\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"description\": \"Easy to use, flexible dashboard software for monitoring, analytics, and more.\",\n  \"main\": \"app.js\",\n  \"scripts\": {\n    \"lint\": \"../../node_modules/.bin/standard\",\n    \"watch\": \"BROWSER_SYNC=true ../../node_modules/.bin/nodemon -e html,js .\",\n    \"start\": \"./bin/vudash.js\",\n    \"test\": \"PORT=3418 NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\"\n  },\n  \"bin\": {\n    \"vudash\": \"bin/vudash.js\"\n  },\n  \"engines\": {\n    \"node\": \">=9.x\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"app-module-path\": \"^2.2.0\",\n    \"bluebird\": \"^3.5.0\",\n    \"boom\": \"^6.0.0\",\n    \"browser-sync\": \"^2.12.9\",\n    \"buble\": \"^0.19.3\",\n    \"catbox-memory\": \"^3.0.0\",\n    \"chalk\": \"^1.1.3\",\n    \"figlet\": \"^1.2.0\",\n    \"find-root\": \"^1.1.0\",\n    \"fs-extra\": \"^0.30.0\",\n    \"handlebars\": \"^4.0.10\",\n    \"hapi\": \"^16.6.2\",\n    \"hapi-api-secret-key\": \"^1.1.0\",\n    \"hoek\": \"^4.1.1\",\n    \"inert\": \"^4.2.0\",\n    \"izitoast\": \"^1.2.0\",\n    \"joi\": \"^10.5.2\",\n    \"json-to-css\": \"^0.1.0\",\n    \"lodash\": \"^4.16.4\",\n    \"npm\": \"^5.0.4\",\n    \"npm-programmatic\": \"0.0.8\",\n    \"ora\": \"^1.3.0\",\n    \"require-directory\": \"^2.1.1\",\n    \"rollup\": \"^0.43.0\",\n    \"rollup-plugin-buble\": \"^0.19.2\",\n    \"rollup-plugin-commonjs\": \"^8.0.2\",\n    \"rollup-plugin-node-resolve\": \"^3.3.0\",\n    \"rollup-plugin-postcss\": \"^0.4.3\",\n    \"rollup-plugin-svelte\": \"^4.1.0\",\n    \"rollup-plugin-svg\": \"^1.0.1\",\n    \"rollup-plugin-uglify-es\": \"0.0.1\",\n    \"rollup-plugin-virtual\": \"^1.0.1\",\n    \"slash\": \"^1.0.0\",\n    \"socket.io\": \"^2.0.1\",\n    \"svelte\": \"^1.60.2\",\n    \"unhandled-rejection\": \"^1.0.0\",\n    \"vision\": \"^4.1.1\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/cli/create.js",
    "content": "'use strict'\n\nconst npm = require('npm-programmatic')\nconst ora = require('ora')\nconst Path = require('path')\nconst fs = require('fs-extra')\n\nconst { green, yellow } = require('chalk')\n\nconst dockerFileContents = `\nFROM node:10-alpine\n\nCOPY . /app\n\nWORKDIR /app\nRUN npm install\n\nEXPOSE 3300\n\nENV SERVER_URL http://localhost:3300\n\nCMD npm start\n`\n\nexports.run = function () {\n  const dashboard = require('../../dashboards/template.json')\n  const cwd = process.cwd()\n  const spinner = ora().start('Creating dashboard layout')\n  const configFile = Path.join(cwd, 'dashboards', 'default.json')\n  const dockerFile = Path.join(cwd, 'Dockerfile')\n  const packageJson = Path.join(cwd, 'package.json')\n  const dashboardsDir = Path.join(cwd, 'dashboards')\n\n  fs.ensureDirSync(dashboardsDir)\n  fs.writeJsonSync(packageJson, {\n    name: 'my-vudash-dashboard',\n    main: 'vudash',\n    scripts: { start: 'vudash' },\n    'engines': {\n      'node': '>=9.x'\n    }\n  })\n  fs.writeJsonSync(configFile, dashboard)\n  fs.outputFileSync(dockerFile, dockerFileContents)\n  spinner.succeed('Created dashboard layout')\n  spinner.start('Installing dependencies. This could take a minute or two...')\n\n  npm.install([\n    'vudash',\n    'vudash-widget-time'\n  ], {\n    cwd,\n    save: true\n  })\n    .then(() => {\n      spinner.succeed('Installed dependencies.')\n      console.log(\n        green(\n          'Created sample dashboard. Run \"vudash\" or \"npm start\" to view'\n        )\n      )\n      console.log(\n        yellow(\n          'Dockerfile written. Use `docker build -t my-dashboard-name .` to build'\n        )\n      )\n    })\n    .catch(e => {\n      spinner.fail('Failed to install some dependencies.')\n      console.log(e)\n    })\n}\n"
  },
  {
    "path": "packages/core/src/cli/help.js",
    "content": "'use strict'\n\nconst { bold } = require('chalk')\n\nexports.run = function () {\n  console.log('Usage: vudash [action]')\n  console.log('with no action, runs the dashboard configured in the current working directory.')\n  console.log('\\nactions:')\n  console.log('\\n', bold('create'), 'Create a new dashboard')\n}\n"
  },
  {
    "path": "packages/core/src/cli/logo.js",
    "content": "'use strict'\n\nconst { textSync } = require('figlet')\nconst { yellow, blue } = require('chalk')\nconst { version } = require('../../package.json')\n\nexports.run = function () {\n  console.log(\n    yellow(\n      textSync('vudash', {\n        font: 'Slant'\n      })\n    ),\n    blue(\n      `v${version}`\n    )\n  )\n}\n"
  },
  {
    "path": "packages/core/src/config-validator/config-validator.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst { validate } = require('.')\nconst Joi = require('joi')\n\ndescribe('config-validator', () => {\n  it('returns validated values', () => {\n    const result = validate('some-name', Joi.string().required(), 'hello')\n    expect(result).to.equal('hello')\n  })\n\n  it('throws validation errors', () => {\n    expect(() => {\n      validate('some-name', Joi.number().required(), 'hello')\n    }).to.throw()\n  })\n\n  it('with no json', () => {\n    const result = validate('some-name', {}, undefined)\n    expect(result).to.equal({})\n  })\n\n  it('with no rules', () => {\n    const result = validate('some-name', null, 'hello')\n    expect(result).to.equal('hello')\n  })\n\n  it('with empty rules', () => {\n    const result = validate('some-name', {}, 'hello')\n    expect(result).to.equal('hello')\n  })\n\n  it('with options', () => {\n    const result = validate(\n      'some-name',\n      Joi.object({ a: Joi.string() }),\n      { a: 'x', b: 'y' },\n      { allowUnknown: true }\n    )\n    expect(result).to.equal({a: 'x', b: 'y'})\n  })\n})\n"
  },
  {
    "path": "packages/core/src/config-validator/index.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst { ConfigurationError } = require('../errors')\n\nexports.validate = function (name, rules = {}, json = {}, options = {}) {\n  if (!rules || (typeof rules === 'object' && !Object.keys(rules).length)) {\n    return json\n  }\n\n  const { error, value } = Joi.validate(json, rules, options)\n  if (error) {\n    throw new ConfigurationError(\n      `Could not register ${name} due to invalid configuration: ${error}`\n    )\n  }\n\n  return value\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/bundler/index.js",
    "content": "'use strict'\n\nconst base = `\n  import iziToast from 'izitoast'\n  import 'izitoast/dist/css/iziToast.css'\n\n  const VUDASH = window.VUDASH\n  const socket = io(VUDASH.config.serverUrl)\n\n  socket.on('error', function (e) {\n    iziToast.show({\n      title: 'Socket Error',\n      theme: 'dark',\n      color: 'red',\n      message: e.message,\n      timeout: 5000,\n      onOpen: function () {\n        console.error(e)\n      }\n    })\n  })\n\n  socket.on('disconnect', function () {\n    iziToast.show({\n      id: 'disconnect',\n      title: 'Socket Disconnected',\n      theme: 'light',\n      color: 'red',\n      message: 'Will reload soon to restore connection...',\n      timeout: ${process.env.DISCONNECT_RELOAD_TIMEOUT} || 30000,\n      onClosed: function () {\n        window.location.reload()\n      }\n    })\n  })\n\n  socket.on('audio:play', function (data) {\n    VUDASH.player.play(data.data)\n  })\n\n  socket.on('view:current', function (data) {\n    window.location.pathname = '/' + data.dashboard + '.dashboard'\n  })\n`\n\nexports.build = function (widgets) {\n  const model = widgets.reduce((curr, { name, markup, css, js, componentPath }) => {\n    curr.imports.push(`import ${name} from '${componentPath}'`)\n    curr.containers.push(markup)\n    curr.css.push(css)\n    curr.events.push(js)\n    return curr\n  }, { imports: [], containers: [], events: [], css: [] })\n\n  const imports = [ ...new Set(model.imports) ]\n\n  const js = `\n    'use strict'\n    ${imports.join('\\n')}\n    ${base}\n    ${model.events.join('\\n')}\n  `\n\n  const html = `\n    <style>\n      ${model.css.join('\\n')}\n    </style>\n\n    ${model.containers.join('\\n')}\n  `\n  return { js, html }\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/compiler/compiler.spec.js",
    "content": "'use strict'\n\nconst compiler = require('.')\nconst { stub } = require('sinon')\nconst rollup = require('rollup')\nconst { expect } = require('code')\n\ndescribe('dashboard/compiler', () => {\n  context('Bundle', () => {\n    const compiled = 'abc123'\n    let js\n\n    before(async () => {\n      stub(rollup, 'rollup')\n\n      const bundleStub = { generate: stub().returns(compiled) }\n      rollup.rollup.resolves(bundleStub)\n      js = await compiler.compile('zzz')\n    })\n\n    after(() => {\n      rollup.rollup.restore()\n    })\n\n    it('Returns compiled js', () => {\n      expect(js).to.exist().and.to.equal(compiled)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/compiler/configuration-builder/configuration-builder.js",
    "content": "'use strict'\n\nconst svelte = require('rollup-plugin-svelte')\nconst resolve = require('rollup-plugin-node-resolve')\nconst commonjs = require('rollup-plugin-commonjs')\nconst virtual = require('rollup-plugin-virtual')\nconst css = require('rollup-plugin-postcss')\nconst svg = require('rollup-plugin-svg')\nconst buble = require('rollup-plugin-buble')\nconst uglify = require('rollup-plugin-uglify-es')\n\nexports.build = function (source) {\n  const inputConfig = {\n    entry: '__input__',\n    plugins: [\n      svg(),\n      virtual({\n        '__input__': source\n      }),\n      commonjs(),\n      resolve({\n        customResolveOptions: {\n          moduleDirectory: 'node_modules'\n        }\n      }),\n      css(),\n      svelte(),\n      buble(),\n      uglify()\n    ]\n  }\n\n  const outputConfig = {\n    format: 'iife',\n    file: 'bundle.js'\n  }\n\n  return { inputConfig, outputConfig }\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/compiler/configuration-builder/configuration-builder.spec.js",
    "content": "'use strict'\n\nconst builder = require('.')\nconst { reach } = require('hoek')\nconst { expect } = require('code')\n\ndescribe('dashboard/compiler/configuration-builder', () => {\n  context('Dynamic Contents', () => {\n    it('Contents correctly set', () => {\n      const expected = '__input__'\n      const { inputConfig } = builder.build(expected)\n      const actual = reach(inputConfig, 'entry')\n      expect(actual).to.exist().and.to.equal(expected)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/compiler/configuration-builder/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./configuration-builder')\n"
  },
  {
    "path": "packages/core/src/dashboard/compiler/index.js",
    "content": "'use strict'\n\nconst rollup = require('rollup')\nconst { build } = require('./configuration-builder')\n\nexports.compile = function (source) {\n  const { inputConfig, outputConfig } = build(source)\n\n  return rollup\n    .rollup(inputConfig)\n    .then(bundle => {\n      const js = bundle.generate(outputConfig)\n      return js\n    })\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/dashboard.js",
    "content": "'use strict'\n\nconst { reach } = require('hoek')\nconst Emitter = require('./emitter')\nconst id = require('../id-gen')\nconst { schema } = require('./schema')\nconst configValidator = require('../config-validator')\nconst parser = require('./parser')\nconst datasourceLoader = require('../datasource-loader')\nconst widgetBinder = require('../widget-binder')\nconst renderer = require('./renderer')\n\nfunction isWidgetEvent (eventId) {\n  return eventId.endsWith(':update')\n}\n\nclass Dashboard {\n  constructor (json, io) {\n    const preprocessed = parser.parse(json)\n    const descriptor = configValidator.validate(preprocessed.name, schema, preprocessed, { allowUnknown: true })\n    const { name, layout, css } = descriptor\n\n    this.id = id()\n    this.name = name\n    this.additionalCss = css || {}\n    this.emitter = new Emitter(io, this.id)\n    this.layout = layout\n\n    this.descriptor = descriptor\n  }\n\n  emit (eventId, data, historical) {\n    if (!isWidgetEvent(eventId)) {\n      return this.emitter.emit(eventId, data, historical)\n    }\n\n    const widgetId = eventId.split(':')[0]\n    const widget = this.widgets[widgetId]\n\n    if (!historical && widget) {\n      widget.history.insert(data)\n    }\n\n    const history = widget ? widget.history.fetch() : {}\n    const update = Object.assign({ history }, data)\n    this.emitter.emit(eventId, update, historical)\n  }\n\n  loadDatasources () {\n    const datasources = reach(this, 'descriptor.datasources', { default: {} })\n    const hasDatasources = Object.keys(datasources).length\n    this.datasources = hasDatasources ? datasourceLoader.load(datasources) : {}\n  }\n\n  loadWidgets () {\n    const widgets = reach(this, 'descriptor.widgets', { default: [] })\n    const hasWidgets = Object.keys(widgets).length\n    this.widgets = hasWidgets ? widgetBinder.load(this, widgets, this.datasources) : {}\n  }\n\n  destroy () {\n    const datasources = Object.values(this.datasources)\n    console.log(`Dashboard ${this.id} cleaning up ${datasources.length} datasources.`)\n    datasources.forEach(datasource => {\n      clearInterval(datasource.timer)\n    })\n\n    const widgets = Object.values(this.widgets)\n    console.log(`Dashboard ${this.id} attempting cleanup of ${widgets.length} widgets.`)\n    widgets.forEach(widget => {\n      widget.hasOwnProperty('destroy') && widget.destroy()\n    })\n  }\n\n  async toRenderModel () {\n    const model = await renderer.buildRenderModel(\n      this.name, this.widgets, this.layout\n    )\n\n    model.css = renderer.compileAdditionalCss(this.additionalCss)\n    return model\n  }\n}\n\nexports.create = function (descriptor, io) {\n  return new Dashboard(descriptor, io)\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/dashboard.spec.js",
    "content": "'use strict'\n\nconst Emitter = require('./emitter')\nconst widgetBinder = require('../widget-binder')\nconst renderer = require('./renderer')\nconst datasourceLoader = require('../datasource-loader')\nconst parser = require('./parser')\nconst configValidator = require('../config-validator')\nconst { stub, useFakeTimers } = require('sinon')\nconst { expect } = require('code')\nconst { create } = require('.')\n\ndescribe('dashboard', () => {\n  describe('constructor', () => {\n    let dashboard\n    const descriptor = { name: 'bar', layout: { columns: 4, rows: 6 } }\n\n    beforeEach(() => {\n      stub(parser, 'parse').returns(descriptor)\n      stub(configValidator, 'validate').returns(descriptor)\n      dashboard = create({}, {\n        on: stub()\n      })\n    })\n\n    afterEach(() => {\n      parser.parse.restore()\n      configValidator.validate.restore()\n    })\n\n    it('generates a dashboard id', () => {\n      expect(dashboard.id).to.exist()\n    })\n\n    it('assigns dashboard name', () => {\n      expect(dashboard.name).to.equal(descriptor.name)\n    })\n\n    it('assigns dashboard layout', () => {\n      expect(dashboard.layout).to.equal(descriptor.layout)\n    })\n\n    it('assigns descriptor for future use', () => {\n      expect(dashboard.descriptor).to.equal(descriptor)\n    })\n\n    it('creates emitter', () => {\n      expect(dashboard.emitter).to.be.an.instanceOf(Emitter)\n    })\n  })\n\n  describe('#loadDatasources()', () => {\n    let dashboard\n\n    const emitter = { on: stub() }\n\n    beforeEach(() => {\n      stub(parser, 'parse')\n      stub(configValidator, 'validate')\n    })\n\n    afterEach(() => {\n      parser.parse.restore()\n      configValidator.validate.restore()\n    })\n\n    context('empty datasource stanza', () => {\n      it('empty datasources when none are specified', () => {\n        parser.parse.returns({})\n        configValidator.validate.returns({})\n        dashboard = create({}, emitter)\n        dashboard.loadDatasources()\n        expect(dashboard.datasources).to.equal({})\n      })\n    })\n\n    context('list of datasources', () => {\n      beforeEach(() => {\n        const descriptor = {\n          datasources: {\n            foo: { foo: 'bar' }\n          }\n        }\n        parser.parse.returns(descriptor)\n        configValidator.validate.returns(descriptor)\n        stub(datasourceLoader, 'load').returns('bar')\n\n        dashboard = create({}, emitter)\n        dashboard.loadDatasources()\n      })\n\n      afterEach(() => {\n        datasourceLoader.load.restore()\n      })\n\n      it('calls loader to load datasources', () => {\n        expect(datasourceLoader.load.callCount).to.equal(1)\n      })\n\n      it('calls loader to load datasources', () => {\n        expect(dashboard.datasources).to.equal('bar')\n      })\n    })\n  })\n\n  describe('#loadWidgets()', () => {\n    let dashboard\n\n    const emitter = { on: stub() }\n\n    beforeEach(() => {\n      stub(parser, 'parse')\n      stub(configValidator, 'validate')\n    })\n\n    afterEach(() => {\n      parser.parse.restore()\n      configValidator.validate.restore()\n    })\n\n    context('empty widget stanza', () => {\n      it('empty widgets when none are specified', () => {\n        parser.parse.returns({})\n        configValidator.validate.returns({})\n        dashboard = create({}, emitter)\n        dashboard.loadWidgets()\n        expect(dashboard.widgets).to.equal({})\n      })\n    })\n\n    context('list of widgets', () => {\n      beforeEach(() => {\n        const descriptor = {\n          widgets: [\n            { foo: 'bar' }\n          ]\n        }\n        parser.parse.returns(descriptor)\n        configValidator.validate.returns(descriptor)\n        stub(widgetBinder, 'load').returns('bar')\n\n        dashboard = create({}, emitter)\n        dashboard.loadWidgets()\n      })\n\n      afterEach(() => {\n        widgetBinder.load.restore()\n      })\n\n      it('calls loader to load widgets', () => {\n        expect(widgetBinder.load.callCount).to.equal(1)\n      })\n\n      it('calls loader to load widgets', () => {\n        expect(dashboard.widgets).to.equal('bar')\n      })\n    })\n  })\n\n  describe('#destroy()', () => {\n    let dashboard\n    let clock\n\n    beforeEach(() => {\n      clock = useFakeTimers()\n      stub(parser, 'parse').returns({})\n      stub(configValidator, 'validate').returns({})\n      dashboard = create({}, {\n        on: stub()\n      })\n    })\n\n    afterEach(() => {\n      parser.parse.restore()\n      configValidator.validate.restore()\n      clock.restore()\n    })\n\n    context('with list of datasources', () => {\n      let stub1 = stub()\n      let stub2 = stub()\n\n      beforeEach(() => {\n        const timer1 = setInterval(stub1, 1)\n        const timer2 = setInterval(stub2, 1)\n        dashboard.datasources = {\n          foo: { timer: timer1 },\n          bar: { timer: timer2 }\n        }\n        dashboard.widgets = {}\n        clock.tick(1)\n      })\n\n      it('clears all timers', () => {\n        dashboard.destroy()\n        clock.tick(1)\n        expect(stub1.callCount).to.equal(1)\n        expect(stub1.callCount).to.equal(1)\n      })\n    })\n\n    context('when no datasources exist', () => {\n      it('succeeds silently', () => {\n        dashboard.datasources = {}\n        dashboard.widgets = {}\n        expect(() => {\n          dashboard.destroy()\n        }).not.to.throw()\n      })\n    })\n\n    context('with list of widgets', () => {\n      const widgets = {\n        abc: { destroy: stub() },\n        def: { }\n      }\n\n      beforeEach(() => {\n        dashboard.widgets = widgets\n        dashboard.datasources = {}\n      })\n\n      it('calls destroy on widgets which support it', () => {\n        dashboard.destroy()\n        expect(widgets.abc.destroy.callCount).to.equal(1)\n      })\n    })\n\n    context('when no widgets exist', () => {\n      it('succeeds silently', () => {\n        dashboard.datasources = {}\n        dashboard.widgets = {}\n        expect(() => {\n          dashboard.destroy()\n        }).not.to.throw()\n      })\n    })\n  })\n\n  describe('#toRenderModel()', () => {\n    context('no additiona css', () => {\n      let dashboard\n\n      const descriptor = {\n        name: 'some-name',\n        layout: 'some-layout'\n      }\n\n      beforeEach(() => {\n        stub(parser, 'parse').returns(descriptor)\n        stub(configValidator, 'validate').returns(descriptor)\n        stub(renderer, 'buildRenderModel').returns({})\n        dashboard = create({}, {\n          on: stub()\n        })\n        dashboard.widgets = { abc: { foo: 'bar' } }\n        dashboard.toRenderModel()\n      })\n\n      afterEach(() => {\n        parser.parse.restore()\n        configValidator.validate.restore()\n        renderer.buildRenderModel.restore()\n      })\n\n      it('calls renderer with name', () => {\n        expect(renderer.buildRenderModel.firstCall.args[0]).to.equal(dashboard.name)\n      })\n\n      it('calls renderer with widgets', () => {\n        expect(renderer.buildRenderModel.firstCall.args[1]).to.equal(dashboard.widgets)\n      })\n\n      it('calls renderer with layout', () => {\n        expect(renderer.buildRenderModel.firstCall.args[2]).to.equal(dashboard.layout)\n      })\n    })\n\n    context('additional css', () => {\n      let dashboard\n      let renderModel\n\n      const descriptor = {\n        name: 'some-name',\n        layout: 'some-layout'\n      }\n\n      beforeEach(async () => {\n        stub(parser, 'parse').returns(descriptor)\n        stub(configValidator, 'validate').returns({})\n        stub(renderer, 'buildRenderModel').returns({})\n        stub(renderer, 'compileAdditionalCss').returns('some: css')\n        dashboard = create({}, {\n          on: stub()\n        })\n        dashboard.additionalCss = { some: 'css' }\n        dashboard.widgets = {}\n        renderModel = await dashboard.toRenderModel()\n      })\n\n      afterEach(() => {\n        renderer.compileAdditionalCss.restore()\n        parser.parse.restore()\n        configValidator.validate.restore()\n        renderer.buildRenderModel.restore()\n      })\n\n      it('calls css transpiler', () => {\n        expect(renderer.compileAdditionalCss.callCount).to.equal(1)\n      })\n\n      it('css is compiled', () => {\n        expect(renderer.compileAdditionalCss.firstCall.args[0]).to.equal({ some: 'css' })\n      })\n\n      it('dashboard css contains additional css', () => {\n        expect(renderModel.css).to.equal('some: css')\n      })\n    })\n  })\n\n  describe('#emit()', () => {\n    const exampleEvent = { some: 'data' }\n    let dashboard\n\n    const socketEmitter = {\n      on: stub()\n    }\n\n    const dashboardEmitter = {\n      emit: stub()\n    }\n\n    beforeEach(() => {\n      stub(parser, 'parse').returns({})\n      stub(configValidator, 'validate').returns({})\n      dashboard = create({}, socketEmitter)\n      dashboard.emitter = dashboardEmitter\n      dashboard.widgets = {\n        xyz: {\n          history: {\n            insert: stub(),\n            fetch: stub().returns([{ foo: 'bar' }])\n          }\n        }\n      }\n    })\n\n    afterEach(() => {\n      parser.parse.restore()\n      configValidator.validate.restore()\n      dashboardEmitter.emit.reset()\n    })\n\n    context('a widget event', () => {\n      it('emits event', () => {\n        dashboard.emit('xyz:update', exampleEvent)\n        expect(dashboardEmitter.emit.callCount).to.equal(1)\n      })\n\n      it('calls widget event history', () => {\n        dashboard.emit('xyz:update', exampleEvent)\n        expect(dashboard.widgets.xyz.history.insert.callCount).to.equal(1)\n      })\n\n      it('returns existing history', () => {\n        dashboard.emit('xyz:update', exampleEvent)\n        expect(\n          dashboardEmitter.emit.firstCall.args[1].history\n        ).to.exist()\n          .and.to.equal([{ foo: 'bar' }])\n      })\n\n      it('widget does not exist', () => {\n        expect(() => {\n          dashboard.emit('abc:update', exampleEvent)\n        }).not.to.throw()\n      })\n\n      it('stores non-historical event', () => {\n        dashboard.emit('xyz:update', exampleEvent)\n        expect(\n          dashboard.widgets.xyz.history.insert.firstCall.args[0]\n        ).to.equal(exampleEvent)\n      })\n\n      it('does not store non-historical event', () => {\n        dashboard.emit('xyz:update', exampleEvent, true)\n        expect(dashboard.widgets.xyz.history.insert.callCount).to.equal(0)\n      })\n    })\n\n    context('a non widget event', () => {\n      it('emits event', () => {\n        dashboard.emit('xyz:abc', exampleEvent)\n        expect(dashboardEmitter.emit.callCount).to.equal(1)\n      })\n\n      it('does not add event to history', () => {\n        dashboard.emit('xyz:update', exampleEvent, true)\n        expect(\n          dashboard.widgets.xyz.history.insert.callCount\n        ).to.equal(0)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/emitter/emitter.js",
    "content": "'use strict'\n\nconst chalk = require('chalk')\n\nclass Emitter {\n  constructor (socketio, room) {\n    this.io = socketio\n    this.room = room\n    this.recentEvents = {}\n    this.io.on('connection', (socket) => {\n      this.clientJoinHandler(socket)\n    })\n  }\n\n  clientJoinHandler (socket) {\n    socket.join(this.room)\n    const historicalEvents = this.recentEvents\n    const eventIds = Object.keys(historicalEvents)\n    console.log(`Client ${chalk.bold.green(socket.id)} \n      connected to ${chalk.bold.red(this.room)}. \n      Receives ${chalk.bold.yellow(eventIds.length)} historical events.`)\n    eventIds.map(eventId => {\n      this.emit(eventId, historicalEvents[eventId], true)\n    })\n  }\n\n  emit (event, data, historical) {\n    this.io.to(this.room).emit(event, data)\n    if (!historical) {\n      this.recentEvents[event] = data\n    }\n  }\n}\n\nmodule.exports = Emitter\n"
  },
  {
    "path": "packages/core/src/dashboard/emitter/emitter.spec.js",
    "content": "'use strict'\n\nconst Emitter = require('.')\nconst { expect } = require('code')\nconst { stub, spy } = require('sinon')\n\ndescribe('dashboard/emitter', () => {\n  const room = 'my-room'\n  const broadcastStub = {\n    emit: spy()\n  }\n  const socketSpy = {\n    on: stub(),\n    to: stub().withArgs(room).returns(broadcastStub)\n  }\n  const emitter = new Emitter(socketSpy, room)\n\n  it('Instantiation of emitter binds handler', () => {\n    expect(socketSpy.on.callCount).to.equal(1)\n    expect(socketSpy.on.firstCall.args[0]).to.equal('connection')\n  })\n\n  it('Emitted events are saved', () => {\n    emitter.emit('abc', {id: 'abc'})\n    emitter.emit('def', {id: 'def'})\n    emitter.emit('ghi', {id: 'ghi'})\n    const recentEvents = emitter.recentEvents\n    expect(Object.keys(recentEvents)).to.have.length(3)\n  })\n\n  it('Repeated event updates previous event', () => {\n    emitter.emit('def', {id: 'pqr'})\n    const recentEvents = emitter.recentEvents\n    expect(Object.keys(recentEvents)).to.have.length(3)\n    expect(recentEvents.def).to.equal({id: 'pqr'})\n  })\n\n  it('On connect, socket is joined to a room', () => {\n    const mockSocket = { id: 'xyz', join: spy() }\n    emitter.clientJoinHandler(mockSocket)\n    expect(mockSocket.join.callCount).to.equal(1)\n    expect(mockSocket.join.firstCall.args[0]).to.equal(room)\n  })\n\n  it('On connect, socket is joined to a room', () => {\n    const emit = broadcastStub.emit\n    emit.reset()\n    const mockSocket = { id: 'xyz', join: spy() }\n    emitter.clientJoinHandler(mockSocket)\n    expect(emit.callCount).to.equal(3)\n    expect(emit.firstCall.args).to.equal(['abc', {id: 'abc'}])\n    expect(emit.secondCall.args).to.equal(['def', {id: 'pqr'}])\n    expect(emit.thirdCall.args).to.equal(['ghi', {id: 'ghi'}])\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/emitter/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./emitter')\n"
  },
  {
    "path": "packages/core/src/dashboard/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./dashboard')\n"
  },
  {
    "path": "packages/core/src/dashboard/loader/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./loader')\n"
  },
  {
    "path": "packages/core/src/dashboard/loader/loader.js",
    "content": "'use strict'\n\nconst { NotFoundError } = require('../../errors')\nconst Dashboard = require('..')\nconst fs = require('fs')\nconst { join } = require('path')\n\nfunction load (cache, name, io) {\n  const path = join(process.cwd(), 'dashboards', `${name}.json`)\n\n  if (!fs.existsSync(path)) {\n    throw new NotFoundError(`Dashboard ${name} does not exist.`)\n  }\n\n  const descriptor = require(path)\n  return add(cache, name, io, descriptor)\n}\n\nfunction add (cache, name, io, descriptor) {\n  const dashboard = Dashboard.create(descriptor, io)\n  dashboard.loadDatasources()\n  dashboard.loadWidgets()\n\n  cache[name] = dashboard\n  return cache[name]\n}\n\nfunction find (cache, name, io) {\n  return cache[name] || load(cache, name, io)\n}\n\nfunction has (cache, name) {\n  return !!cache[name]\n}\n\nmodule.exports = {\n  find,\n  has,\n  add,\n  load\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/loader/loader.spec.js",
    "content": "'use strict'\n\nconst { NotFoundError } = require('../../errors')\nconst loader = require('.')\nconst { expect } = require('code')\n\ndescribe('dashboard/loader', () => {\n  it('Dashboard is not found', () => {\n    expect(() => {\n      return loader.load({}, 'xyz')\n    }).to.throw(NotFoundError, 'Dashboard xyz does not exist.')\n  })\n\n  context('#add()', () => {\n    const cache = {}\n    const emitter = { on: () => { } }\n    const descriptor = { layout: { columns: 0, rows: 0 }, widgets: [] }\n\n    beforeEach(() => {\n      loader.add(cache, 'xyz', emitter, descriptor)\n    })\n\n    it('Add dashboard to cache', () => {\n      expect(cache.xyz.descriptor).to.equal(descriptor)\n    })\n\n    it('Find dashboard from cache', () => {\n      expect(loader.find(cache, 'xyz', emitter).descriptor).to.equal(descriptor)\n    })\n  })\n\n  context('#has()', () => {\n    const cache = { 'abc': {} }\n    it('is contained in cache', () => {\n      expect(loader.has(cache, 'abc')).to.be.true()\n    })\n\n    it('is not contained in cache', () => {\n      expect(loader.has(cache, 'xyz')).to.be.false()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/parser/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./parser')\n"
  },
  {
    "path": "packages/core/src/dashboard/parser/parser.js",
    "content": "'use strict'\n\nconst { ConfigurationError } = require('../../errors')\n\nfunction get (directive) {\n  if (process.env.hasOwnProperty(directive)) {\n    return process.env[directive]\n  }\n\n  throw new ConfigurationError(`Environment variable ${directive} does not exist`)\n}\n\nfunction parse (json) {\n  const root = Object.keys(json)\n\n  root.forEach(key => {\n    const child = json[key]\n    if (typeof child !== 'object') { return }\n    const directive = child['$env']\n    if (directive) {\n      json[key] = get(directive)\n    } else {\n      parse(json[key])\n    }\n  })\n\n  return json\n}\n\nexports.parse = parse\n"
  },
  {
    "path": "packages/core/src/dashboard/parser/parser.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst { parse } = require('.')\nconst { ConfigurationError } = require('../../errors')\n\ndescribe('dashboard/parser', () => {\n  beforeEach(() => {\n    process.env.SOME_KEY = 'abcde'\n  })\n\n  afterEach(() => {\n    process.env.SOME_KEY = undefined\n  })\n\n  it('replaces config element with environmental variable', () => {\n    const config = {\n      some: {\n        value: { $env: 'SOME_KEY' }\n      }\n    }\n\n    expect(parse(config)).to.equal({\n      some: {\n        value: 'abcde'\n      }\n    })\n  })\n\n  it('replaces multiple elements', () => {\n    const config = {\n      some: {\n        value: { $env: 'SOME_KEY' },\n        other: {\n          value: { $env: 'SOME_KEY' }\n        }\n      }\n    }\n\n    expect(parse(config)).to.equal({\n      some: {\n        value: 'abcde',\n        other: {\n          value: 'abcde'\n        }\n      }\n    })\n  })\n\n  it('replaces entire element', () => {\n    const config = {\n      some: {\n        value: {\n          $env: 'SOME_KEY',\n          invalid: 'entry'\n        }\n      }\n    }\n\n    expect(parse(config)).to.equal({\n      some: {\n        value: 'abcde'\n      }\n    })\n  })\n\n  it('throws error if variable does not exist', () => {\n    const config = {\n      some: {\n        value: { $env: 'NONEXISTENT_KEY' }\n      }\n    }\n\n    expect(() => {\n      parse(config)\n    }).to.throw(\n      ConfigurationError,\n      'Environment variable NONEXISTENT_KEY does not exist'\n    )\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/renderer/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./renderer')\n"
  },
  {
    "path": "packages/core/src/dashboard/renderer/renderer.js",
    "content": "'use strict'\n\nconst bundler = require('../bundler')\nconst compiler = require('../compiler')\nconst Css = require('json-to-css')\n\nfunction renderWidgets (widgets, layout) {\n  const widgetModel = Object.values(widgets)\n  return widgetModel.map(widget => {\n    return widget.toRenderModel(layout)\n  })\n}\n\nexports.buildRenderModel = async function (name, widgets, layout) {\n  const renderedWidgets = renderWidgets(widgets, layout)\n  const { js, html } = bundler.build(renderedWidgets)\n\n  const script = await compiler.compile(js)\n\n  return {\n    name,\n    html,\n    js: script\n  }\n}\n\nexports.compileAdditionalCss = function (css) {\n  return Css.of(css)\n}\n"
  },
  {
    "path": "packages/core/src/dashboard/renderer/renderer.spec.js",
    "content": "'use strict'\n\ndescribe('dashboard/renderer', () => {\n\n})\n"
  },
  {
    "path": "packages/core/src/dashboard/schema/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./schema')\n"
  },
  {
    "path": "packages/core/src/dashboard/schema/schema.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\n\nconst layoutSchema = Joi.object({\n  columns: Joi.number().required().description('Number of columns'),\n  rows: Joi.number().required().description('Number of rows')\n}).required().description('Layout')\n\nconst widgetPositionSchema = Joi.object({\n  x: Joi.number().required().description('Column for widget in dashboard, zero indexed'),\n  y: Joi.number().required().description('Row for widget in dashboard, zero indexed'),\n  w: Joi.number().required().description('Widget width in dashboard columns'),\n  h: Joi.number().required().description('Widget height in dashboard rows')\n}).required().description('Widget position data')\n\nconst widgetSchema = Joi.object({\n  position: widgetPositionSchema,\n  widget: Joi.any().required().description('Path to widget, Node package name, or Class'),\n  datasource: Joi.string().optional().description('Datasource name'),\n  options: Joi.object().optional().description('Widget configuration'),\n  background: Joi.string().optional().description('Optional background styling, used as css background')\n}).description('Widget Configuration')\n\nconst widgetsSchema = Joi.array().required().items(widgetSchema).description('List of widgets')\n\nconst datasourcesSchema = Joi.object().pattern(/.*/, Joi.object({\n  module: Joi.string().required().description('Datasource module name or directory path'),\n  schedule: Joi.number().required().description('Update frequency, milliseconds'),\n  options: Joi.object().optional().description('Datasource specific options')\n})).optional().description('Hash of datasources')\n\nconst dashboardSchema = Joi.object({\n  name: Joi.string().optional().description('Dashboard name'),\n  layout: layoutSchema,\n  datasources: datasourcesSchema,\n  widgets: widgetsSchema\n}).description('Dashboard Descriptor')\n\nexports.schema = dashboardSchema\n"
  },
  {
    "path": "packages/core/src/dashboard/schema/schema.spec.js",
    "content": "'use strict'\n\nconst { schema } = require('.')\nconst fs = require('fs')\nconst { join } = require('path')\nconst { expect } = require('code')\nconst Joi = require('joi')\n\ndescribe('dashboard/schema', () => {\n  context('Parse', () => {\n    it('Throws on invalid schema', () => {\n      const { error } = Joi.validate({}, schema)\n      expect(error.message).to.include('\"layout\" is required')\n    })\n\n    const dashboardsDir = join(__dirname, '..', '..', '..', 'dashboards')\n    const boards = fs.readdirSync(dashboardsDir)\n\n    boards.forEach(board => {\n      it(`Parses valid schema ${board}`, () => {\n        const json = require(join(dashboardsDir, board))\n        const { error } = Joi.validate(json, schema)\n        expect(error).not.to.exist()\n      })\n    })\n  })\n\n  context('validation', () => {\n    context('datasources', () => {\n      it('are optional', () => {\n        const descriptor = {\n          layout: {\n            columns: 1,\n            rows: 1\n          },\n          widgets: []\n        }\n        const { value } = Joi.validate(descriptor, schema)\n        expect(value).to.equal(descriptor)\n      })\n\n      it('must be datasources', () => {\n        const descriptor = {\n          layout: {\n            columns: 1,\n            rows: 1\n          },\n          widgets: [],\n          datasources: {\n            'some-datasource': {}\n          }\n        }\n\n        const { error } = Joi.validate(descriptor, schema)\n        expect(error.message).to.include('fails because [child \"module\" fails')\n      })\n    })\n\n    it('datasource options is optional', () => {\n      const descriptor = {\n        layout: {\n          columns: 1,\n          rows: 1\n        },\n        widgets: [],\n        datasources: {\n          'some-datasource': {\n            module: 'a',\n            schedule: 30000\n          }\n        }\n      }\n      const { value } = Joi.validate(descriptor, schema)\n      expect(value).to.equal(descriptor)\n    })\n\n    it('must include update schedule', () => {\n      const descriptor = {\n        layout: {\n          columns: 1,\n          rows: 1\n        },\n        widgets: [],\n        datasources: {\n          'some-datasource': {\n            module: 'a'\n          }\n        }\n      }\n\n      const { error } = Joi.validate(descriptor, schema)\n      expect(error.message).to.include('\"schedule\" is required')\n    })\n\n    it('update schedule must be in milliseconds', () => {\n      const descriptor = {\n        layout: {\n          columns: 1,\n          rows: 1\n        },\n        widgets: [],\n        datasources: {\n          'some-datasource': {\n            module: 'a',\n            schedule: 'aaa'\n          }\n        }\n      }\n      const { error } = Joi.validate(descriptor, schema)\n      expect(error.message).to.include('\"schedule\" must be a number')\n    })\n\n    it('must include module name', () => {\n      const descriptor = {\n        layout: {\n          columns: 1,\n          rows: 1\n        },\n        widgets: [],\n        datasources: {\n          'some-datasource': {\n            schedule: 30000\n          }\n        }\n      }\n      const { error } = Joi.validate(descriptor, schema)\n      expect(error.message).to.include('\"module\" is required')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/dashboard-event/dashboard-event.js",
    "content": "'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",
    "content": "'use strict'\n\nmodule.exports = require('./dashboard-event')\n"
  },
  {
    "path": "packages/core/src/datasource/datasource.spec.js",
    "content": "'use strict'\n\nconst loader = require('.')\nconst locator = require('./locator')\nconst DatasourceBuilder = require('util/datasource.builder')\nconst validator = require('./validator')\nconst { expect } = require('code')\nconst { stub } = require('sinon')\n\ncontext('datasource.validator', () => {\n  const widget = DatasourceBuilder.create().build()\n\n  context('Datasource specified', () => {\n    beforeEach(() => {\n      stub(locator, 'locate')\n      stub(validator, 'validate').returns({})\n    })\n\n    afterEach(() => {\n      locator.locate.restore()\n      validator.validate.restore()\n    })\n\n    it('Registers widget data source', () => {\n      locator.locate.returns({ Constructor: widget, options: {} })\n      loader.load('some-widget', {}, 'a-datasource')\n      expect(locator.locate.callCount).to.equal(1)\n      expect(locator.locate.firstCall.args[1]).to.equal('a-datasource')\n    })\n\n    it('calls for widget validation on load', () => {\n      const options = { foo: 'bar' }\n      locator.locate.returns({\n        Constructor: widget,\n        options,\n        validation: {}\n      })\n      loader.load('some-widget', {}, 'a-datasource')\n      expect(validator.validate.callCount).to.equal(1)\n      expect(validator.validate.firstCall.args[2]).to.equal(options)\n    })\n\n    it('no validation specified', () => {\n      locator.locate.returns({\n        Constructor: widget\n      })\n      loader.load('some-widget', {}, 'a-datasource')\n      expect(validator.validate.callCount).to.equal(0)\n    })\n  })\n\n  context('no datasource specified', () => {\n    it('will polyfill a fetch method which throws', () => {\n      const loaded = loader.load('some-widget', {}, undefined)\n      function fn () { return loaded.fetch() }\n      expect(fn).to.throw('Widget some-widget requested data, but no datasource was configured. Check the widget configuration in your dashboard config.')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/datasource/dummy-datasource/dummy-datasource.spec.js",
    "content": "'use strict'\n\nconst DummyDatasource = require('.')\nconst { expect } = require('code')\n\ndescribe('datasource.dummy-datasource', () => {\n  it('can be constructed', () => {\n    expect(DummyDatasource).to.be.a.function()\n  })\n\n  it('Does not fetch', () => {\n    const widgetName = 'abczys'\n    const ds = new DummyDatasource({ widgetName })\n    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.`)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/datasource/dummy-datasource/index.js",
    "content": "'use strict'\n\nclass DummyDatasource {\n  constructor (options) {\n    this.config = options\n  }\n\n  fetch () {\n    throw new Error(`Widget ${this.config.widgetName} requested data, but no datasource was configured. Check the widget configuration in your dashboard config.`)\n  }\n}\n\nmodule.exports = DummyDatasource\n"
  },
  {
    "path": "packages/core/src/datasource/index.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst { applyToDefaults } = require('hoek')\nconst DummyDatasource = require('./dummy-datasource')\nconst validator = require('./validator')\nconst locator = require('./locator')\n\nconst datasourceValidation = {\n  schedule: Joi.number().min(0).description('Datasource refresh schedule')\n}\n\nfunction extendValidation (validation) {\n  return validation\n    ? applyToDefaults(datasourceValidation, validation)\n    : datasourceValidation\n}\n\nfunction loadValidOptions (widgetName, validation, options) {\n  return options\n    ? validator.validate(widgetName, validation, options)\n    : {}\n}\n\nexports.load = function (widgetName, dashboard, datasourceName) {\n  if (!datasourceName) {\n    return new DummyDatasource({ widgetName })\n  }\n\n  const { Constructor, validation, options } = locator.locate(dashboard.datasources, datasourceName)\n  const datasourceValidation = extendValidation(validation)\n  const config = loadValidOptions(widgetName, datasourceValidation, options)\n\n  return new Constructor(config)\n}\n"
  },
  {
    "path": "packages/core/src/datasource/locator/index.js",
    "content": "'use strict'\n\nconst { WidgetRegistrationError } = require('../../errors')\n\nexports.locate = function (datasources, datasource) {\n  const resolved = datasources[datasource]\n\n  if (!resolved) {\n    throw new WidgetRegistrationError(`Unable to use datasource ${datasource} as it does not exist`)\n  }\n\n  return resolved\n}\n"
  },
  {
    "path": "packages/core/src/datasource/locator/locator.spec.js",
    "content": "'use strict'\n\nconst { WidgetRegistrationError } = require('../../errors')\nconst locator = require('.')\nconst { expect } = require('code')\n\ndescribe('datasource.locator', () => {\n  it('loads datasource', () => {\n    const datasources = { abcde: { foo: 'bar' } }\n    expect(\n      locator.locate(datasources, 'abcde')\n    ).to.equal(datasources.abcde)\n  })\n\n  it('Cannot load datasource', () => {\n    expect(() => {\n      return locator.locate({}, 'non-existent')\n    }).to.throw(WidgetRegistrationError, 'Unable to use datasource non-existent as it does not exist')\n  })\n})\n"
  },
  {
    "path": "packages/core/src/datasource/validator/index.js",
    "content": "'use strict'\n\nconst configValidator = require('../../config-validator')\n\nexports.validate = function (widgetName, validation, options = {}) {\n  return validation\n    ? configValidator.validate(`widget:${widgetName}`, validation, options)\n    : options\n}\n"
  },
  {
    "path": "packages/core/src/datasource/validator/validator.spec.js",
    "content": "'use strict'\n\nconst validator = require('.')\nconst configValidator = require('../../config-validator')\nconst { stub } = require('sinon')\nconst { expect } = require('code')\n\ndescribe('datasource.validator', () => {\n  context('No validation specified', () => {\n    let options\n    beforeEach(() => {\n      stub(configValidator, 'validate')\n      options = validator.validate('a', null, { a: 'b' })\n    })\n\n    afterEach(() => {\n      configValidator.validate.restore()\n    })\n\n    it('returns datasource options', () => {\n      expect(\n        options\n      ).to.equal({ a: 'b' })\n    })\n\n    it('validation is not called as it does not exist', () => {\n      expect(configValidator.validate.callCount).to.equal(0)\n    })\n  })\n\n  context('Validation specified', () => {\n    beforeEach(() => {\n      stub(configValidator, 'validate')\n    })\n\n    afterEach(() => {\n      configValidator.validate.restore()\n    })\n\n    it('validation is called if available', () => {\n      validator.validate('a', {}, {})\n      expect(configValidator.validate.callCount).to.equal(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/datasource-binder/datasource-binder.js",
    "content": "'use strict'\n\nconst { createEmitter } = require('./datasource-emitter')\nconst chalk = require('chalk')\n\nexports.bind = function (name, datasource, schedule = 30000) {\n  const emitter = createEmitter()\n\n  function fetchFunction () {\n    datasource\n      .fetch()\n      .then(data => {\n        emitter.emit('update', data)\n      })\n      .catch(e => {\n        console.error(chalk.red.bold(`Error updating datasource ${name}`), chalk.yellow(e.message))\n        console.error(chalk.red(e.stack))\n      })\n  }\n\n  const timer = setInterval(fetchFunction, schedule)\n\n  fetchFunction()\n\n  return {\n    timer,\n    emitter\n  }\n}\n"
  },
  {
    "path": "packages/core/src/datasource-binder/datasource-binder.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst binder = require('.')\nconst { stub, useFakeTimers } = require('sinon')\nconst datasourceEmitter = require('./datasource-emitter')\n\ndescribe('datasource-binder', () => {\n  describe('timers', () => {\n    let clock\n    let timer\n\n    const emitter = {\n      emit: stub()\n    }\n\n    const datasource = {\n      fetch: stub().resolves({ foo: 'bar' })\n    }\n\n    beforeEach(() => {\n      stub(datasourceEmitter, 'createEmitter').returns(emitter)\n      clock = useFakeTimers()\n      const result = binder.bind('xyz', datasource, 500)\n      timer = result.timer\n    })\n\n    afterEach(() => {\n      datasourceEmitter.createEmitter.restore()\n      clock.restore()\n      datasource.fetch.reset()\n      clearInterval(timer)\n    })\n\n    it('fetch is called immediately', () => {\n      expect(datasource.fetch.callCount).to.equal(1)\n    })\n\n    it('fetch when interval is reached', () => {\n      clock.tick(500)\n      expect(datasource.fetch.callCount).to.equal(2)\n    })\n\n    it('on each subsequent interval', () => {\n      clock.tick(1000)\n      expect(datasource.fetch.callCount).to.equal(3)\n    })\n  })\n\n  context('missing schedule', () => {\n    let clock\n    let timer\n\n    const emitter = {\n      emit: stub()\n    }\n\n    const datasource = {\n      fetch: stub().resolves({ foo: 'bar' })\n    }\n\n    beforeEach(() => {\n      stub(datasourceEmitter, 'createEmitter').returns(emitter)\n      clock = useFakeTimers()\n      const result = binder.bind('xyz', datasource)\n      timer = result.timer\n    })\n\n    afterEach(() => {\n      datasourceEmitter.createEmitter.restore()\n      clock.restore()\n      datasource.fetch.reset()\n      clearInterval(timer)\n    })\n\n    it('default interval is set to 30 seconds', () => {\n      clock.tick(30000)\n      expect(datasource.fetch.callCount).to.equal(2)\n    })\n  })\n\n  describe('event emitters', () => {\n    let timer\n    let emitter\n\n    const expectedData = { foo: 'bar' }\n\n    const datasource = {\n      fetch: stub().resolves(expectedData)\n    }\n\n    beforeEach(() => {\n      const result = binder.bind('xyz', datasource, 500)\n      timer = result.timer\n      emitter = result.emitter\n    })\n\n    afterEach(() => {\n      datasource.fetch.reset()\n      clearInterval(timer)\n    })\n\n    it('fetch emits data', done => {\n      emitter.on('update', data => {\n        expect(data).to.equal(expectedData)\n        done()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/datasource-binder/datasource-emitter.js",
    "content": "'use strict'\n\nconst EventEmitter = require('events')\n\nclass DatasourceEmitter extends EventEmitter {}\n\nexports.createEmitter = function () {\n  return new DatasourceEmitter()\n}\n"
  },
  {
    "path": "packages/core/src/datasource-binder/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./datasource-binder')\n"
  },
  {
    "path": "packages/core/src/datasource-loader/datasource-loader.js",
    "content": "'use strict'\n\nconst resolver = require('../resolver')\nconst configValidator = require('../config-validator')\nconst datasourceBinder = require('../datasource-binder')\n\nconst loadError = 'Cannot load datasource someDatasource because it does not look like a datasource'\n\nfunction resolveDatasource (path) {\n  return resolver.resolve(path)\n}\n\nfunction parseConfiguration (datasourceName, validation, options) {\n  return configValidator.validate(datasourceName, validation, options)\n}\n\nfunction register (registrationFn, configuration) {\n  if (typeof registrationFn !== 'function') {\n    throw new Error(`${loadError} (no registration function)`)\n  }\n\n  return registrationFn(configuration)\n}\n\nfunction initialise (name, registrationFn, configuration = {}, schedule) {\n  const datasource = register(registrationFn, configuration)\n\n  if (typeof datasource.fetch !== 'function') {\n    throw new Error(`${loadError} (no fetch function)`)\n  }\n\n  return datasourceBinder.bind(name, datasource, schedule)\n}\n\nexports.load = function (descriptor) {\n  const datasourceNames = Object.keys(descriptor)\n  return datasourceNames.reduce((datasources, name) => {\n    const { module: path, options, schedule } = descriptor[name]\n    const { validation, register } = resolveDatasource(path)\n    const configuration = parseConfiguration(name, validation, options)\n    datasources[name] = initialise(name, register, configuration, schedule)\n    return datasources\n  }, {})\n}\n"
  },
  {
    "path": "packages/core/src/datasource-loader/datasource-loader.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst { stub } = require('sinon')\nconst datasourceLoader = require('.')\nconst resolver = require('../resolver')\nconst binder = require('../datasource-binder')\nconst validator = require('../config-validator')\n\ndescribe('datasource-loader', () => {\n  describe('datasource prototype', () => {\n    const descriptor = {\n      someDatasource: {\n        module: '../some/path',\n        schedule: 1000,\n        options: {\n          foo: 'bar'\n        }\n      }\n    }\n\n    context('missing fetch function', () => {\n      const someDatasource = {\n        register: options => {\n          return {}\n        }\n      }\n\n      beforeEach(() => {\n        stub(resolver, 'resolve').returns(someDatasource)\n      })\n\n      it('fails to load', () => {\n        expect(() => {\n          datasourceLoader.load(descriptor)\n        }).to.throw(\n          Error,\n          'Cannot load datasource someDatasource because it does not look like a datasource (no fetch function)'\n        )\n      })\n\n      afterEach(() => {\n        resolver.resolve.restore()\n      })\n    })\n\n    context('missing registration function', () => {\n      const someDatasource = {}\n\n      beforeEach(() => {\n        stub(resolver, 'resolve').returns(someDatasource)\n      })\n\n      it('fails to load', () => {\n        expect(() => {\n          datasourceLoader.load(descriptor)\n        }).to.throw(\n          Error,\n          'Cannot load datasource someDatasource because it does not look like a datasource (no registration function)'\n        )\n      })\n\n      afterEach(() => {\n        resolver.resolve.restore()\n      })\n    })\n  })\n\n  context('no datasource options specified', () => {\n    const descriptor = {\n      someDatasource: {\n        module: '../some/path',\n        schedule: 1000\n      }\n    }\n\n    const someDatasource = {\n      register: stub().returns({ fetch: stub() })\n    }\n\n    beforeEach(() => {\n      stub(resolver, 'resolve').returns(someDatasource)\n      stub(binder, 'bind')\n      datasourceLoader.load(descriptor)\n    })\n\n    it('should call register', () => {\n      expect(someDatasource.register.callCount).to.equal(1)\n    })\n\n    it('should pass empty options', () => {\n      expect(someDatasource.register.firstCall.args[0]).to.equal({})\n    })\n\n    afterEach(() => {\n      resolver.resolve.restore()\n      binder.bind.restore()\n    })\n  })\n\n  context('multiple datasources', () => {\n    let datasources\n\n    const descriptor = {\n      'plugin-a': {\n        module: '../some/path'\n      },\n      'plugin-b': {\n        module: '../some/path'\n      }\n    }\n\n    const bound = { some: 'value' }\n\n    const datasource = {\n      register: () => {\n        return {\n          fetch: () => {}\n        }\n      }\n    }\n\n    beforeEach(() => {\n      stub(resolver, 'resolve').returns(datasource)\n      stub(validator, 'validate').returns({})\n      stub(binder, 'bind').returns(bound)\n      datasources = datasourceLoader.load(descriptor)\n    })\n\n    afterEach(() => {\n      resolver.resolve.restore()\n      validator.validate.restore()\n      binder.bind.restore()\n    })\n\n    it('contains two datasources', () => {\n      const datasourceNames = Object.keys(datasources)\n      expect(datasourceNames.length).to.equal(2)\n    })\n\n    it('datasources are exposed', () => {\n      expect(datasources['plugin-a']).to.equal(bound)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/datasource-loader/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./datasource-loader')\n"
  },
  {
    "path": "packages/core/src/errors/configuration.error.js",
    "content": "'use strict'\n\nmodule.exports = class ConfigurationError extends Error {\n\n}\n"
  },
  {
    "path": "packages/core/src/errors/index.js",
    "content": "'use strict'\n\nconst { upperCamel } = require('../upper-camel')\n\nconst requireDirectory = require('require-directory')\nconst errors = requireDirectory(module, {\n  rename: upperCamel\n})\n\nmodule.exports = errors\n"
  },
  {
    "path": "packages/core/src/errors/not-found.error.js",
    "content": "'use strict'\n\nmodule.exports = class NotFoundError extends Error {\n\n}\n"
  },
  {
    "path": "packages/core/src/errors/plugin-registration.error.js",
    "content": "'use strict'\n\nmodule.exports = class PluginRegistrationError extends Error {\n\n}\n"
  },
  {
    "path": "packages/core/src/errors/widget-registration.error.js",
    "content": "'use strict'\n\nmodule.exports = class WidgetRegistrati extends Error {\n\n}\n"
  },
  {
    "path": "packages/core/src/id-gen/id-gen.js",
    "content": "'use strict'\n\nmodule.exports = () => {\n  const random = Math.random() * 0xFFFFFFFFFFFF << 0\n  const positive = Math.abs(random)\n  return positive.toString(16)\n}\n"
  },
  {
    "path": "packages/core/src/id-gen/id-gen.spec.js",
    "content": "'use strict'\n\nconst id = require('.')\nconst { expect } = require('code')\n\ndescribe('id-gen', () => {\n  it('generates a string id', () => {\n    expect(id())\n      .to.exist()\n      .and.to.be.a.string()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/id-gen/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./id-gen')\n"
  },
  {
    "path": "packages/core/src/plugins/api/api.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst viewCurrentHandlers = require('./handlers/view/current')\nconst dashboardHandlers = require('./handlers/dashboards')\n\nconst ApiPlugin = {\n  register: function (server, options, next) {\n    server.route({\n      method: 'PUT',\n      path: '/api/v1/view/current',\n      config: {\n        tags: ['api'],\n        validate: {\n          payload: {\n            dashboard: Joi.string().required().description('Dashboard to switch to')\n          }\n        }\n      },\n      handler: viewCurrentHandlers.put\n    })\n\n    server.route({\n      method: 'PUT',\n      path: '/api/v1/dashboards/{name}',\n      config: {\n        tags: ['api'],\n        validate: {\n          params: {\n            name: Joi.string().required().description('Dashboard id')\n          },\n          payload: {\n            descriptor: Joi.object().required().description('Dashboard descriptor')\n          }\n        }\n      },\n      handler: dashboardHandlers.put\n    })\n\n    next()\n  }\n}\n\nApiPlugin.register.attributes = {\n  name: 'api',\n  version: '1.0.0',\n  dependencies: ['hapi-api-secret-key']\n}\n\nmodule.exports = ApiPlugin\n"
  },
  {
    "path": "packages/core/src/plugins/api/handlers/dashboards/index.js",
    "content": "'use strict'\n\nexports.put = require('./put')\n"
  },
  {
    "path": "packages/core/src/plugins/api/handlers/dashboards/put.js",
    "content": "'use strict'\n\nconst loader = require('../../../../dashboard/loader')\n\nmodule.exports = function (request, reply) {\n  const { dashboards } = request.server.plugins.ui\n  const { io } = request.server.plugins.socket\n  const { name } = request.params\n  const { descriptor } = request.payload\n\n  const isUpdate = loader.has(dashboards, name)\n\n  loader.add(dashboards, name, io, descriptor)\n\n  reply().code(isUpdate ? 200 : 201)\n}\n"
  },
  {
    "path": "packages/core/src/plugins/api/handlers/dashboards/put.spec.js",
    "content": "'use strict'\n\nconst { stub } = require('sinon')\nconst { put } = require('.')\nconst { expect } = require('code')\n\ndescribe('core/plugins/api', () => {\n  describe('v1', () => {\n    describe('dashboards', () => {\n      const name = 'xyz'\n      const descriptor = { layout: { columns: 0, rows: 0 }, widgets: [] }\n      const dashboards = {}\n      const server = {\n        plugins: {\n          ui: {\n            dashboards\n          },\n          socket: {\n            io: {\n              on: stub()\n            }\n          }\n        }\n      }\n\n      const reply = stub()\n        .returns({\n          code: stub()\n        })\n\n      beforeEach(() => {\n        const request = {\n          server,\n          params: {\n            name\n          },\n          payload: {\n            descriptor\n          }\n        }\n\n        put(request, reply)\n      })\n\n      afterEach(() => {\n        reply.reset()\n      })\n\n      it('reply is called', () => {\n        expect(reply.callCount).to.equal(1)\n      })\n\n      it('dashboards contains new dashboard', () => {\n        expect(Object.keys(dashboards)).to.include(name)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/plugins/api/handlers/view/current/index.js",
    "content": "'use strict'\n\nexports.put = require('./put')\n"
  },
  {
    "path": "packages/core/src/plugins/api/handlers/view/current/put.js",
    "content": "'use strict'\n\nmodule.exports = function (request, reply) {\n  const { io } = request.server.plugins.socket\n  const { dashboard } = request.payload\n  io.emit('view:current', { dashboard })\n  reply()\n}\n"
  },
  {
    "path": "packages/core/src/plugins/api/handlers/view/current/put.spec.js",
    "content": "'use strict'\n\nconst { stub } = require('sinon')\nconst { put } = require('.')\nconst { expect } = require('code')\n\ndescribe('core/plugins/api', () => {\n  describe('v1', () => {\n    describe('view/current', () => {\n      const dashboard = 'xyz'\n      const server = {\n        plugins: {\n          socket: {\n            io: {\n              emit: stub()\n            }\n          }\n        }\n      }\n\n      const reply = stub()\n\n      beforeEach(() => {\n        const request = {\n          server,\n          payload: {\n            dashboard\n          }\n        }\n\n        put(request, reply)\n      })\n\n      afterEach(() => {\n        server.plugins.socket.io.emit.reset()\n        reply.reset()\n      })\n\n      it('reply is called', () => {\n        expect(reply.callCount).to.equal(1)\n      })\n\n      it('emits a broadcast event', () => {\n        expect(server.plugins.socket.io.emit.callCount).to.equal(1)\n      })\n\n      it('broadcast event has dashboard switch event', () => {\n        expect(server.plugins.socket.io.emit.firstCall.args[0]).to.equal('view:current')\n      })\n\n      it('broadcast event has payload data', () => {\n        expect(server.plugins.socket.io.emit.firstCall.args[1]).to.equal({ dashboard })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/plugins/api/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./api')\n"
  },
  {
    "path": "packages/core/src/plugins/socket/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./socket')\n"
  },
  {
    "path": "packages/core/src/plugins/socket/socket.js",
    "content": "'use strict'\n\nconst socketio = require('socket.io')\n\nconst SocketPlugin = {\n  register: function (server, options, next) {\n    server.expose('io', socketio(server.listener))\n    next()\n  }\n}\n\nSocketPlugin.register.attributes = {\n  name: 'socket',\n  version: '1.0.0'\n}\n\nmodule.exports = SocketPlugin\n"
  },
  {
    "path": "packages/core/src/plugins/static/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./static')\n"
  },
  {
    "path": "packages/core/src/plugins/static/static.js",
    "content": "'use strict'\n\nconst { join } = require('path')\n\nconst AssetsPlugin = {\n  register: function (server, options, next) {\n    server.route({\n      method: 'GET',\n      path: '/assets/{param*}',\n      handler: {\n        directory: {\n          path: join(__dirname, '..', '..', '..', 'src/public')\n        }\n      }\n    })\n\n    next()\n  }\n}\n\nAssetsPlugin.register.attributes = {\n  name: 'assets',\n  version: '1.0.0'\n}\n\nmodule.exports = AssetsPlugin\n"
  },
  {
    "path": "packages/core/src/plugins/ui/handlers/dashboard/index.js",
    "content": "'use strict'\n\nconst dashboardLoader = require('../../../../dashboard/loader')\nconst { NotFoundError } = require('../../../../errors')\nconst Boom = require('boom')\n\nasync function buildViewModel (dashboard, server) {\n  const serverUrl = server.settings.app.serverUrl\n  const { name, html, js, css } = await dashboard.toRenderModel()\n  return {\n    serverUrl,\n    html,\n    name,\n    bundle: js.code,\n    map: js.map,\n    css\n  }\n}\n\nexports.handler = async function (request, reply) {\n  const { board } = request.params\n  const { server } = request\n  const { io } = server.plugins.socket\n  const { dashboards } = server.plugins.ui\n  try {\n    const dashboard = dashboardLoader.find(dashboards, board, io)\n    const model = await buildViewModel(dashboard, server)\n    return reply.view('dashboard', model)\n  } catch (e) {\n    console.error(e)\n    if (e instanceof NotFoundError) {\n      return reply.redirect('/')\n    }\n    return reply(Boom.boomify(e, { statusCode: 400 }))\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/ui/handlers/index/index.js",
    "content": "'use strict'\n\nconst Path = require('path')\nconst { readdirSync } = require('fs')\nconst { internal } = require('boom')\nconst { join } = require('path')\n\nfunction loadFromDisk (boards) {\n  const path = Path.join(process.cwd(), 'dashboards')\n  const files = readdirSync(path)\n  return files.reduce((curr, d) => {\n    const descriptor = require(join(path, d))\n    curr.add({\n      name: descriptor.name,\n      path: '/' + d.replace('.json', '') + '.dashboard'\n    })\n    return curr\n  }, boards)\n}\n\nfunction loadFromCache (boards, request) {\n  const { dashboards } = request.server.plugins.ui\n  return Object.keys(dashboards).reduce((curr, d) => {\n    curr.add(d)\n    return curr\n  }, boards)\n}\n\nexports.handler = function (request, reply) {\n  const defaultDashboard = process.env.DEFAULT_DASHBOARD\n  if (defaultDashboard) {\n    return reply.redirect(`/${defaultDashboard}.dashboard`)\n  }\n\n  try {\n    const boards = new Set()\n    loadFromDisk(boards)\n    loadFromCache(boards, request)\n    reply.view('listing', { boards: Array.from(boards) })\n  } catch (e) {\n    return reply(internal(e))\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/ui/handlers/index/index.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst { handler } = require('.')\n\ndescribe('plugins/ui/handlers/index', () => {\n  context('Default Dashboard', () => {\n    before(() => {\n      process.env.DEFAULT_DASHBOARD = 'xxyyzz'\n    })\n\n    after(() => {\n      process.env.DEFAULT_DASHBOARD = undefined\n    })\n\n    it('Index points to default dashboard', done => {\n      const replyFunc = {\n        redirect: (uri) => {\n          expect(uri).to.equal('/xxyyzz.dashboard')\n          done()\n        }\n      }\n\n      handler({}, replyFunc)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/plugins/ui/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./ui')\n"
  },
  {
    "path": "packages/core/src/plugins/ui/ui.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst { handler: indexHandler } = require('./handlers/index')\nconst { handler: dashboardHandler } = require('./handlers/dashboard')\n\nconst DashboardPlugin = {\n  register: function (server, options, next) {\n    server.route({\n      method: 'GET',\n      path: '/',\n      handler: indexHandler\n    })\n\n    server.route({\n      method: 'GET',\n      path: '/{board}.dashboard',\n      config: {\n        validate: {\n          params: {\n            board: Joi.string().required().description('Board name')\n          }\n        },\n        cache: {\n          expiresIn: 15 * 60 * 1000,\n          privacy: 'private'\n        }\n      },\n      handler: dashboardHandler\n    })\n\n    server.route({\n      method: '*',\n      path: '/{p*}',\n      handler: indexHandler\n    })\n\n    server.expose('dashboards', {})\n\n    next()\n  }\n}\n\nDashboardPlugin.register.attributes = {\n  name: 'ui',\n  version: '1.0.0',\n  dependencies: ['socket']\n}\n\nmodule.exports = DashboardPlugin\n"
  },
  {
    "path": "packages/core/src/plugins/ui/ui.spec.js",
    "content": "'use strict'\n\nconst server = require('server')\nconst { expect } = require('code')\n\ndescribe('core/plugins/ui', function () {\n  this.timeout(10000)\n\n  let app\n  const dashboard = 'simple'\n\n  before(async () => {\n    app = await server.register()\n  })\n\n  after(async () => {\n    await server.stop(app)\n  })\n\n  it('Loads dashboards into memory', () => {\n    return app.inject({ url: `/${dashboard}.dashboard` })\n      .then(({ statusCode }) => {\n        expect(statusCode).to.equal(200)\n      })\n  })\n\n  it('Builds dashboard cache', () => {\n    const cachedDashboards = Object.keys(app.plugins.ui.dashboards)\n    expect(cachedDashboards).to.only.include('simple')\n  })\n})\n"
  },
  {
    "path": "packages/core/src/public/css/listing.css",
    "content": "body {\n  font-family: 'Ubuntu', sans-serif;\n  color: #fff;\n  background-color: #191919;\n  text-align: center;\n}\n\n.logo {\n  width: 25vw;\n  margin: 5vh;\n}\n\npath {\n  fill: #fff;\n  stroke: transparent;\n}\n\nrect {\n  fill: tomato;\n}\n\n.sub-page {\n  font-size: 20px;\n}\n\nul {\n  list-style-type: none;\n  padding: 0;\n}\n\nul > li {\n  margin: 5px 0;\n}\n\na, a:visited, a:active {\n  color: #fff;\n  text-decoration: none;\n}\n\na:hover {\n  color: tomato;\n}\n"
  },
  {
    "path": "packages/core/src/public/css/style.css",
    "content": "@font-face {\n  font-family: 'Ubuntu';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Ubuntu Regular'), local('Ubuntu-Regular'),\n       url('/assets/fonts/ubuntu-v11-latin-regular.woff2') format('woff2'),\n       url('/assets/fonts/ubuntu-v11-latin-regular.woff') format('woff');\n}\n\nbody {\n  font-family: 'Ubuntu', sans-serif;\n  color: #fff;\n  overflow: hidden;\n  background-color: #000;\n}\n\n.widget-container {\n  position: absolute;\n  display: block;\n  box-sizing: border-box;\n  background-color: #191919;\n  border: 3px solid #000;\n  border-radius: 10px;\n}\n\n.widget-container > *:first-child {\n  display: flex;\n  flex-direction: column;\n  align-items: center; \n  justify-content: center;\n}\n\n.widget-container > *:first-child {\n    height: 100%;\n    width: 100%;\n}"
  },
  {
    "path": "packages/core/src/public/js/audio.js",
    "content": "'use strict'\n\nvar VUDASH = window.VUDASH\n\nvar Player = function () {\n  this.audio = new window.Audio()\n}\n\nPlayer.prototype.play = function (data) {\n  this.audio.src = data\n  this.audio.addEventListener('canplaythrough', function () {\n    this.play()\n  })\n}\n\nVUDASH.player = new Player()\n"
  },
  {
    "path": "packages/core/src/public/js/object-assign.polyfill.js",
    "content": "'use strict'\n\nif (typeof Object.assign !== 'function') {\n  Object.assign = function (target, varArgs) {\n    'use strict'\n    if (target == null) {\n      throw new TypeError('Cannot convert undefined or null to object')\n    }\n\n    var to = Object(target)\n\n    for (var index = 1; index < arguments.length; index++) {\n      var nextSource = arguments[index]\n\n      if (nextSource != null) {\n        for (var nextKey in nextSource) {\n          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {\n            to[nextKey] = nextSource[nextKey]\n          }\n        }\n      }\n    }\n    return to\n  }\n}\n"
  },
  {
    "path": "packages/core/src/resolver/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./resolver')\n"
  },
  {
    "path": "packages/core/src/resolver/resolver.js",
    "content": "'use strict'\n\nconst fs = require('fs')\nconst { join } = require('path')\n\nfunction discoverNpmModule (moduleName) {\n  try {\n    return require.resolve(moduleName)\n  } catch (e) {\n    return null\n  }\n}\n\nfunction discoverLocalModule (moduleName) {\n  const path = join(process.cwd(), moduleName)\n  return fs.existsSync(path) ? path : null\n}\n\nfunction throwNotFound (moduleName) {\n  throw new Error(`Module ${moduleName} could not be resolved as an NPM module or a local module`)\n}\n\nfunction discover (moduleName) {\n  return [\n    discoverNpmModule,\n    discoverLocalModule,\n    throwNotFound\n  ].find(method => {\n    return method(moduleName)\n  })(moduleName)\n}\n\nfunction resolve (moduleName) {\n  return require(discover(moduleName))\n}\n\nmodule.exports = {\n  discover,\n  resolve\n}\n"
  },
  {
    "path": "packages/core/src/resolver/resolver.spec.js",
    "content": "'use strict'\n\nconst { discover } = require('.')\nconst { expect } = require('code')\nconst { resolve } = require('path')\n\ndescribe('resolver', () => {\n  context('An npm module', () => {\n    const rootDir = resolve(process.cwd(), '../..')\n\n    it('returns path', () => {\n      expect(\n        discover('code')\n      ).to.equal(\n        `${rootDir}/node_modules/code/lib/index.js`\n      )\n    })\n  })\n\n  context('A local module', () => {\n    it('returns path', () => {\n      expect(\n        discover('test/resources/widgets/example')\n      ).to.equal(\n        `${process.cwd()}/test/resources/widgets/example`\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/server.js",
    "content": "'use strict'\n\nconst requirePaths = require('app-module-path')\nrequirePaths.addPath(process.cwd())\nrequirePaths.addPath(`${process.cwd()}/node_modules`)\n\nconst Hapi = require('hapi')\nconst fs = require('fs')\nconst Path = require('path')\nconst chalk = require('chalk')\nconst unhandled = require('unhandled-rejection')\nconst id = require('./id-gen')\n\nconst rejectionEmitter = unhandled({\n  timeout: 5\n})\n\nrejectionEmitter.on('unhandledRejection', (error, promise) => {\n  console.error(error)\n})\n\nfunction register () {\n  const server = new Hapi.Server()\n  server.connection({ port: process.env.PORT || 3300 })\n\n  server.settings.app = { serverUrl: process.env.SERVER_URL || server.info.uri }\n  const apiKey = process.env['API_KEY'] || id()\n\n  return server.register([\n    require('vision'),\n    require('inert'),\n    require('./plugins/socket'),\n    require('./plugins/static'),\n    require('./plugins/ui'),\n    {\n      register: require('hapi-api-secret-key').plugin,\n      options: {\n        secrets: [ apiKey ]\n      }\n    },\n    require('./plugins/api')\n  ])\n    .then(() => {\n      server.views({\n        engines: {\n          html: require('handlebars')\n        },\n        relativeTo: __dirname,\n        path: './views'\n      })\n\n      console.log(`Loading dashboards from ${chalk.blue(process.cwd())}`)\n      console.log(`Server ${chalk.green.bold('running')}`)\n      console.log(`Api key: ${chalk.magenta.bold(apiKey)}`)\n      console.log('Dashboards available:')\n      const dashboardDir = Path.join(process.cwd(), 'dashboards')\n      const boards = fs.readdirSync(dashboardDir)\n      for (let board of boards) {\n        const loaded = require(Path.join(dashboardDir, board))\n        const boardUrl = `${Path.basename(board, '.json')}.dashboard`\n        console.log(chalk.blue.bold(loaded.name), 'at', chalk.cyan.underline(`${server.settings.app.serverUrl}/${boardUrl}`))\n      }\n\n      return Promise.resolve(server)\n    })\n}\n\nfunction start (server) {\n  return server.start()\n    .then(() => {\n      if (process.env.BROWSER_SYNC) {\n        const bs = require('browser-sync').create()\n\n        bs.init({\n          open: false,\n          proxy: server.info.uri,\n          files: ['src/public/**/*.{js,css}']\n        })\n      }\n\n      return Promise.resolve()\n    })\n}\n\nfunction cleanup (server) {\n  const cache = Object.values(server.plugins.ui.dashboards)\n  cache.forEach(dashboard => {\n    dashboard.destroy()\n  })\n}\n\nfunction stop (server) {\n  server.stop({\n    timeout: 60000\n  }, () => {\n    cleanup(server)\n    return Promise.resolve()\n  })\n}\n\nmodule.exports = {\n  register,\n  start,\n  stop\n}\n"
  },
  {
    "path": "packages/core/src/transform-loader/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./transform-loader')\n"
  },
  {
    "path": "packages/core/src/transform-loader/transform-loader.js",
    "content": "'use strict'\n\nconst resolver = require('../resolver')\nconst configValidator = require('../config-validator')\nconst Joi = require('joi')\n\nfunction validate (widgetName, configuration) {\n  const transformerSchema = Joi.object({\n    transformer: Joi.string().required().label('Transformer module name'),\n    options: Joi.object().optional().label('Transform configuration')\n  })\n\n  const schema = Joi.array().items(\n    transformerSchema\n  ).required()\n\n  return configValidator.validate(widgetName, schema, configuration)\n}\n\nexports.load = function (widgetName, configuration) {\n  validate(widgetName, configuration)\n  return configuration.map(({ transformer, options }) => {\n    const Constructor = resolver.resolve(transformer)\n    return new Constructor(options)\n  })\n}\n"
  },
  {
    "path": "packages/core/src/transform-loader/transform-loader.spec.js",
    "content": "'use strict'\n\nconst { load } = require('.')\nconst { expect } = require('code')\nconst { stub } = require('sinon')\nconst resolver = require('../resolver')\nconst { ConfigurationError } = require('../errors')\n\ndescribe('transform-loader', () => {\n  class SomeTransformer {}\n  class OtherTransformer {}\n\n  context('transformers with configuration', () => {\n    let transformers\n\n    const configuration = [\n      {\n        transformer: 'some-transformer',\n        options: {\n          foo: 'bar'\n        }\n      },\n      {\n        transformer: 'other-transformer',\n        options: {}\n      }\n    ]\n\n    beforeEach(() => {\n      stub(resolver, 'resolve')\n\n      resolver.resolve\n        .withArgs('some-transformer')\n        .returns(SomeTransformer)\n      resolver.resolve\n        .withArgs('other-transformer')\n        .returns(OtherTransformer)\n      transformers = load('x', configuration)\n    })\n\n    afterEach(() => {\n      resolver.resolve.restore()\n    })\n\n    it('loads two transformers', () => {\n      expect(transformers.length).to.equal(2)\n    })\n\n    it('loads first transformer', () => {\n      expect(transformers[0]).to.be.an.instanceof(SomeTransformer)\n    })\n\n    it('loads second transformer', () => {\n      expect(transformers[1]).to.be.an.instanceof(OtherTransformer)\n    })\n\n    it('requests transformer modules', () => {\n      expect(resolver.resolve.callCount).to.equal(2)\n    })\n  })\n\n  context('empty transformer list', () => {\n    const configuration = []\n\n    it('loads no transformers', () => {\n      expect(\n        load('x', configuration)\n      )\n        .to.be.an.array()\n        .and.to.have.length(0)\n    })\n  })\n\n  describe('validation', () => {\n    context('missing transformer name', () => {\n      const configuration = [{}]\n\n      beforeEach(() => {\n        stub(resolver, 'resolve')\n      })\n\n      afterEach(() => {\n        resolver.resolve.restore()\n      })\n\n      it('throws a validation error', () => {\n        expect(() => {\n          load('x', configuration)\n        }).to.throw(ConfigurationError, /\"Transformer module name\" is required/)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/upper-camel/index.js",
    "content": "'use strict'\n\nconst { flow, camelCase, upperFirst } = require('lodash')\n\nexports.upperCamel = function (name) {\n  const rename = flow([camelCase, upperFirst])\n  return rename(name)\n}\n"
  },
  {
    "path": "packages/core/src/upper-camel/upper-camel.spec.js",
    "content": "'use strict'\n\nconst { upperCamel } = require('.')\nconst { expect } = require('code')\n\ndescribe('upper-camel', () => {\n  const scenarios = [\n    { input: 'some-name', output: 'SomeName' },\n    { input: 'SomeName', output: 'SomeName' },\n    { input: '@scope/package-name', output: 'ScopePackageName' },\n    { input: '__some/name%&', output: 'SomeName' }\n  ]\n\n  scenarios.forEach(({ input, output }) => {\n    it(`${input} becomes ${output}`, () => {\n      expect(upperCamel(input))\n        .to.exist()\n        .and.to.be.a.string()\n        .and.to.equal(output)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/views/dashboard.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n  <script src=\"/assets/js/object-assign.polyfill.js\"></script>\n\n  <title>{{name}} Dashboard</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/css/style.css\">\n</head>\n<body>\n  {{{html}}}\n  <script src=\"/socket.io/socket.io.js\"></script>\n  <script>\n    window.VUDASH = {};\n    window.VUDASH.config = {};\n    window.VUDASH.config.serverUrl = '{{{serverUrl}}}';\n    {{{bundle}}}\n  </script>\n  <style>\n    {{{css}}}\n  </style>\n  <script src=\"/assets/js/audio.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "packages/core/src/views/listing.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n  <title>Available Boards</title>\n  <style>\n    @import 'https://fonts.googleapis.com/css?family=Ubuntu';\n    @import 'https://fonts.googleapis.com/icon?family=Material+Icons';\n  </style>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/css/listing.css\" />\n</head>\n<body>\n  <svg class=\"logo\" viewBox=\"0 0 176.08649 106.1699\">\n      <g transform=\"translate(-24.377661,-51.731245)\">\n        <g>\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n        </g>\n        <g transform=\"matrix(0.69851314,0,0,0.7944649,208.52468,40.27109)\">\n          <g>\n            <rect y=\"14.424998\" x=\"-177.00626\" height=\"18.785416\" width=\"37.570831\" />\n            <rect y=\"36.782299\" x=\"-177.27081\" height=\"43.65625\" width=\"37.570835\" />\n          </g>\n          <g transform=\"matrix(1,0,0,-1,41.539557,94.863551)\">\n            <rect y=\"14.424998\" x=\"-177.00626\" height=\"18.785416\" width=\"37.570831\" />\n            <rect y=\"36.782299\" x=\"-177.27081\" height=\"43.65625\" width=\"37.570835\" />\n          </g>\n        </g>\n        <g>\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <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\" />\n          <path d=\"m 149.79531,151.0302 h 0.9142 v 6.73622 h -0.9142 z\" />\n          <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\" />\n          <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\" />\n        </g>\n      </g>\n    </svg>\n  <h1>\n    <span class=\"sub-page\">Available Boards</span>\n  </h1>\n  <ul>\n    {{#each boards}}\n      <li>\n        <a href=\"{{this.path}}\">{{this.name}}</a>\n      </li>\n    {{/each}}\n  </ul>\n</body>\n\n</html>\n"
  },
  {
    "path": "packages/core/src/widget/history/history.js",
    "content": "'use strict'\n\nclass History {\n  constructor (size = 10) {\n    this.size = size\n    this.items = []\n  }\n\n  insert (entry) {\n    this.items.push(entry)\n    if (this.items.length > this.size) {\n      this.items.shift()\n    }\n  }\n\n  fetch () {\n    return this.items\n  }\n}\n\nexports.create = function (size) {\n  return new History(size)\n}\n"
  },
  {
    "path": "packages/core/src/widget/history/history.spec.js",
    "content": "'use strict'\n\nconst { create } = require('.')\nconst { times } = require('lodash')\nconst { expect } = require('code')\n\ndescribe('widget-binder/history', () => {\n  context('three items', () => {\n    let contents\n    beforeEach(() => {\n      const history = create()\n      history.insert('a')\n      history.insert('b')\n      history.insert('c')\n      contents = history.fetch()\n    })\n\n    it('has all entries', () => {\n      expect(contents[2]).to.equal('c')\n    })\n\n    it('is earliest first', () => {\n      expect(contents[0]).to.equal('a')\n    })\n  })\n\n  context('history overflow', () => {\n    let contents\n    beforeEach(() => {\n      const history = create(2)\n      history.insert('a')\n      history.insert('b')\n      history.insert('c')\n      contents = history.fetch()\n    })\n\n    it('has length of two', () => {\n      expect(contents.length).to.equal(2)\n    })\n\n    it('has all entries', () => {\n      expect(contents[1]).to.equal('c')\n    })\n\n    it('is earliest first', () => {\n      expect(contents[0]).to.equal('b')\n    })\n  })\n\n  context('size not specified', () => {\n    let contents\n    let history\n    beforeEach(() => {\n      history = create()\n      times(11, i => {\n        history.insert(i)\n      })\n      contents = history.fetch()\n    })\n\n    it('history size is 10', () => {\n      expect(history.size).to.equal(10)\n    })\n\n    it('is limited by size', () => {\n      expect(contents).to.have.length(10)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget/history/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./history')\n"
  },
  {
    "path": "packages/core/src/widget/index.js",
    "content": "'use strict'\n\nconst id = require('../id-gen')\nconst WidgetPosition = require('./widget-position')\nconst loader = require('./loader')\nconst renderer = require('./renderer')\nconst History = require('./history')\nconst validator = require('./validator')\n\nclass Widget {\n  constructor (widgetPath, config) {\n    const { position, background, options = {}, history } = config\n\n    this.id = id()\n    this.widgetPath = widgetPath\n    this.options = options\n    this.background = background\n    this.position = position\n    this.history = History.create(history)\n  }\n\n  register (emitter) {\n    const { widget, name, componentPath } = loader.load(this.widgetPath)\n    this.componentPath = componentPath\n    this.name = name\n\n    this.options = validator.validate(name, widget.validation, this.options)\n    this.widget = widget.register(this.options, emitter)\n  }\n\n  update (value) {\n    return this.widget.update ? this.widget.update(value) : value\n  }\n\n  toRenderModel (dashboardLayout) {\n    const {\n      id,\n      name,\n      options,\n      componentPath,\n      background,\n      position\n    } = this\n\n    const widgetPosition = new WidgetPosition(dashboardLayout, position)\n\n    return {\n      id,\n      name,\n      componentPath,\n      markup: renderer.renderHtml(id),\n      css: renderer.renderStyles(id, widgetPosition, background),\n      js: renderer.renderScript(id, name, options)\n    }\n  }\n}\n\nexports.create = function (widgetPath, config) {\n  return new Widget(widgetPath, config)\n}\n"
  },
  {
    "path": "packages/core/src/widget/loader/index.js",
    "content": "'use strict'\n\nconst { reach } = require('hoek')\nconst { join } = require('path')\nconst resolver = require('../../resolver')\nconst { upperCamel } = require('../../upper-camel')\nconst { ConfigurationError } = require('../../errors')\nconst slash = require('slash')\nconst findRoot = require('find-root')\n\nfunction discoverComponentPath (packagePath, packageJson) {\n  const relativeComponentPath = readComponentStanza(packageJson)\n  const absoluteComponentPath = join(packagePath, relativeComponentPath)\n  const localPath = slash(absoluteComponentPath)\n  if (!localPath) {\n    const packageName = reach(packageJson, 'name')\n    throw new ConfigurationError(`Cannot find component at ${localPath} for widget ${packageName}.`)\n  }\n  return localPath\n}\n\nfunction findPackageRoot (directory) {\n  const moduleEntrypoint = resolver.discover(directory)\n  return findRoot(moduleEntrypoint)\n}\n\nfunction loadPackageJson (packageRoot) {\n  return require(join(packageRoot, 'package.json'))\n}\n\nfunction readPackage (directory) {\n  const packageRoot = findPackageRoot(directory)\n  const widget = require(packageRoot)\n  const packageJson = loadPackageJson(packageRoot)\n  const componentPath = discoverComponentPath(packageRoot, packageJson)\n\n  const name = upperCamel(packageJson.name)\n  return { widget, name, componentPath }\n}\n\nfunction readComponentStanza (packageJson) {\n  const path = 'vudash.component'\n  const componentPath = reach(packageJson, path)\n  if (!componentPath) {\n    const packageName = reach(packageJson, 'name')\n    throw new ConfigurationError(`Widget ${packageName} is missing '${path}' in package.json`)\n  }\n  return componentPath\n}\n\nexports.load = function (pathOrDescriptor) {\n  const isPreParsed = typeof pathOrDescriptor === 'object'\n  if (isPreParsed) {\n    return pathOrDescriptor\n  }\n  return readPackage(pathOrDescriptor)\n}\n"
  },
  {
    "path": "packages/core/src/widget/loader/loader.spec.js",
    "content": "'use strict'\n\nconst loader = require('.')\nconst { ComponentCompilationError } = require('errors')\nconst { expect } = require('code')\n\ndescribe('widget/loader', () => {\n  context('Programmatic Config', () => {\n    const pkg = {\n      Module: { register: () => {} },\n      component: 'some-component',\n      name: 'vudash-some-component'\n    }\n\n    it('Parses Component', () => {\n      const resolved = loader.load(pkg)\n      expect(resolved).to.equal(pkg)\n    })\n  })\n\n  context('Vudash Metadata', () => {\n    it('fails to read component metadata', () => {\n      const message = \"Widget vudash-widget-missing is missing 'vudash.component' in package.json\"\n      const fn = () => { loader.load('test/resources/widgets/missing') }\n      expect(fn).to.throw(ComponentCompilationError, message)\n    })\n  })\n\n  context('Valid Component', () => {\n    let component\n\n    beforeEach(() => {\n      component = loader.load('test/resources/widgets/example')\n    })\n\n    it('returns registration method', () => {\n      expect(component.widget.register).to.be.a.function()\n    })\n\n    it('returns markup path', () => {\n      expect(component.componentPath).to.endWith('test/resources/widgets/example/markup.html')\n    })\n\n    it('returns registration method', () => {\n      expect(component.name).to.exist().and.to.equal('VudashWidgetExample')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget/renderer/index.js",
    "content": "'use strict'\n\nexports.renderScript = function (id, name, config) {\n  return `\n    const widget_${id} = new ${name}({ \n      target: document.getElementById(\"widget-container-${id}\"), \n      data: { config: ${JSON.stringify(config)} }\n    });\n\n    socket.on('${id}:update', ($data) => {\n      if ($data.error) {\n        console.error('Widget \"${id}\" encountered error: ' + $data.error.message)\n      }\n      widget_${id}.update($data)\n    })\n  `.trim()\n}\n\nexports.renderHtml = function (id) {\n  return `<div id=\"widget-container-${id}\" class=\"widget-container\"></div>`\n}\n\nexports.renderStyles = function (id, widgetPosition, background) {\n  const { top, left, width, height } = widgetPosition\n  const rules = [\n    `top:${top}%`,\n    `left:${left}%`,\n    `width:${width}%`,\n    `height:${height}%`\n  ]\n\n  if (background) {\n    rules.push(`background:${background}`)\n  }\n\n  return `#widget-container-${id}{${rules.join(';')}}`\n}\n"
  },
  {
    "path": "packages/core/src/widget/renderer/renderer.spec.js",
    "content": "'use strict'\n\nconst renderer = require('.')\nconst WidgetPosition = require('../widget-position')\nconst Cheerio = require('cheerio')\nconst { expect } = require('code')\n\ndescribe('widget/renderer', () => {\n  context('#renderScript()', () => {\n    const id = 'abc'\n    const config = { a: 'b' }\n    let rendered\n\n    before(() => {\n      rendered = renderer.renderScript(id, 'AbcWidget', config)\n    })\n\n    it('update method is rendered', () => {\n      expect(rendered).to.include(\"socket.on('abc:update', ($data) => {\")\n    })\n\n    it('target is set correctly', () => {\n      expect(rendered).to.include('target: document.getElementById(\"widget-container-abc\")')\n    })\n\n    it('error handling exists', () => {\n      expect(rendered).to.include('Widget \"abc\" encountered error')\n    })\n\n    it('default data contains config', () => {\n      expect(rendered).to.include('data: { config: {\"a\":\"b\"} }')\n    })\n\n    it('widget update method is called', () => {\n      expect(rendered).to.include('widget_abc.update($data)')\n    })\n\n    it('component is rendered', () => {\n      expect(rendered).to.startWith('const widget_abc = new AbcWidget')\n    })\n  })\n\n  context('#renderHtml()', () => {\n    let $\n    before(() => {\n      const widget = { id: 'xyz' }\n      const markup = renderer.renderHtml(widget.id)\n      $ = Cheerio.load(markup)\n    })\n\n    it('Has correct id', () => {\n      expect($('div').attr('id')).to.equal('widget-container-xyz')\n    })\n\n    it('Has correct class', () => {\n      expect($('div').hasClass('widget-container')).to.be.true()\n    })\n  })\n\n  describe('#renderStyles()', () => {\n    const widgetPosition = new WidgetPosition({\n      rows: 4,\n      columns: 5\n    }, {\n      x: 1, y: 2, w: 3, h: 4\n    })\n\n    const background = '#fff'\n\n    context('Css', () => {\n      let css\n      before(() => {\n        css = renderer.renderStyles('xyz', widgetPosition, background)\n      })\n\n      it('Renders widget id', () => {\n        expect(css).to.startWith('#widget-container-xyz{')\n      })\n\n      it('Renders background correctly', () => {\n        expect(css).to.contain('background:#fff')\n      })\n\n      it('Renders position correctly', () => {\n        expect(css).to.contain('left:20%;')\n        expect(css).to.contain('top:50%;')\n        expect(css).to.contain('width:60%;')\n        expect(css).to.contain('height:100%;')\n      })\n    })\n\n    context('No Background', () => {\n      let css\n      before(() => {\n        css = renderer.renderStyles('abc', widgetPosition, undefined)\n      })\n\n      it('Does not contain background rule', () => {\n        expect(css).not.to.contain('background:')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget/validator/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./validator')\n"
  },
  {
    "path": "packages/core/src/widget/validator/validator.js",
    "content": "'use strict'\n\nconst configValidator = require('../../config-validator')\nconst Joi = require('joi')\n\nconst defaultSchema = {\n  description: Joi.string().optional().description('Widget display label')\n}\n\nexports.validate = function (name, baseSchema, options) {\n  if (!baseSchema) { return options }\n\n  const schema = baseSchema.keys(defaultSchema)\n  return configValidator.validate(name, schema, options)\n}\n"
  },
  {
    "path": "packages/core/src/widget/validator/validator.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst { validate } = require('.')\n\ndescribe('widget/validator', () => {\n  it('has no validation', () => {\n    const descriptor = { a: 'b' }\n    expect(\n      validate('xyz', null, descriptor)\n    ).to.equal(descriptor)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget/widget-position/index.js",
    "content": "class WidgetPosition {\n  constructor (dashboardLayout, position) {\n    this.columns = dashboardLayout.columns\n    this.rows = dashboardLayout.rows\n    this.position = position\n  }\n\n  get rowHeight () {\n    return 100 / this.rows\n  }\n\n  get columnWidth () {\n    return 100 / this.columns\n  }\n\n  get height () {\n    return this.position.h * this.rowHeight\n  }\n\n  get width () {\n    return this.position.w * this.columnWidth\n  }\n\n  get left () {\n    return this.position.x * this.columnWidth\n  }\n\n  get top () {\n    return this.position.y * this.rowHeight\n  }\n}\n\nmodule.exports = WidgetPosition\n"
  },
  {
    "path": "packages/core/src/widget/widget-position/widget-position.spec.js",
    "content": "'use strict'\n\nconst WidgetPosition = require('.')\nconst { expect } = require('code')\n\ndescribe('css-builder/widget-position', () => {\n  const dashboard = { columns: 5, rows: 4 }\n\n  it('Calculates first widget dimensions', () => {\n    const position = { x: 0, y: 0, w: 1, h: 1 }\n    const widgetPosition = new WidgetPosition(dashboard, position)\n    expect(widgetPosition.top).to.equal(0)\n    expect(widgetPosition.left).to.equal(0)\n    expect(widgetPosition.width).to.equal(20)\n    expect(widgetPosition.height).to.equal(25)\n  })\n\n  it('Calculates middle widget dimensions', () => {\n    const position = { x: 2, y: 4, w: 2, h: 1 }\n    const widgetPosition = new WidgetPosition(dashboard, position)\n    expect(widgetPosition.top).to.equal(100)\n    expect(widgetPosition.left).to.equal(40)\n    expect(widgetPosition.width).to.equal(40)\n    expect(widgetPosition.height).to.equal(25)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget/widget.spec.js",
    "content": "'use strict'\n\nconst { create } = require('.')\nconst { stub } = require('sinon')\nconst loader = require('./loader')\nconst { expect } = require('code')\nconst WidgetPosition = require('./widget-position')\nconst renderer = require('./renderer')\n\ndescribe('widget', () => {\n  describe('#create()', () => {\n    it('has an auto-generated id', () => {\n      const widget = create('xyz', {})\n      expect(widget.id).to.exist()\n    })\n\n    it('has options', () => {\n      const widget = create('xyz', {})\n      expect(widget.options).to.equal({})\n    })\n\n    it('has options', () => {\n      const widget = create('xyz', {})\n      expect(widget.history).to.exist()\n    })\n  })\n\n  describe('#register()', () => {\n    let widget\n    const register = stub()\n    const options = { foo: 'bar' }\n    const dashboardEmitter = { xyz: 'abc' }\n\n    beforeEach(() => {\n      stub(loader, 'load').returns({\n        widget: { register }\n      })\n      widget = create('xyz', { options })\n      widget.register(dashboardEmitter)\n    })\n\n    afterEach(() => {\n      loader.load.restore()\n    })\n\n    it('widget is registered', () => {\n      expect(register.callCount).to.equal(1)\n    })\n\n    it('registers options with widget', () => {\n      expect(register.firstCall.args[0]).to.equal(options)\n    })\n\n    it('passes the emitter to the register method', () => {\n      expect(register.firstCall.args[1]).to.equal(dashboardEmitter)\n    })\n  })\n\n  describe('update', () => {\n    let widget\n    const data = { foo: 'bar' }\n\n    context('widget implements update function', () => {\n      const modified = { foo: 'bar' }\n\n      beforeEach(() => {\n        widget = create('xyz', {})\n        widget.widget = { update: stub().returns(modified) }\n      })\n\n      it('update calls widget update', () => {\n        widget.update(data)\n        expect(widget.widget.update.callCount).to.equal(1)\n      })\n\n      it('calls with update data', () => {\n        widget.update(data)\n        expect(widget.widget.update.firstCall.args[0]).to.equal(data)\n      })\n\n      it('returns modified data', () => {\n        expect(widget.update(data)).to.equal(modified)\n      })\n    })\n\n    context('widget does not implement update function', () => {\n      it('returns update data', () => {\n        widget = create('xyz', {})\n        widget.widget = {}\n        expect(widget.update(data)).to.equal(data)\n      })\n    })\n  })\n\n  describe('#toRenderModel()', () => {\n    let widget\n    let renderModel\n\n    const dashboardLayout = { rows: 1, columns: 1 }\n    const background = '#fff'\n    const options = { foo: 'bar' }\n\n    beforeEach(() => {\n      stub(renderer, 'renderHtml').returns('html')\n      stub(renderer, 'renderStyles').returns('css')\n      stub(renderer, 'renderScript').returns('js')\n\n      widget = create('xxx', { options, background })\n      widget.id = 'my-id'\n      widget.name = 'some-widget'\n      widget.componentPath = 'xyz'\n      renderModel = widget.toRenderModel(dashboardLayout)\n    })\n\n    afterEach(() => {\n      renderer.renderHtml.restore()\n      renderer.renderStyles.restore()\n      renderer.renderScript.restore()\n    })\n\n    describe('renders HTML', () => {\n      it('renders html', () => {\n        expect(renderer.renderHtml.callCount).to.equal(1)\n      })\n\n      it('passes id to html renderer', () => {\n        expect(renderer.renderHtml.firstCall.args[0]).to.equal(widget.id)\n      })\n    })\n\n    describe('renders styles', () => {\n      it('renders css', () => {\n        expect(renderer.renderStyles.callCount).to.equal(1)\n      })\n\n      it('passes id to style renderer', () => {\n        expect(renderer.renderStyles.firstCall.args[0]).to.equal(widget.id)\n      })\n\n      it('passes position to style renderer', () => {\n        expect(renderer.renderStyles.firstCall.args[1]).to.be.an.instanceOf(WidgetPosition)\n      })\n\n      it('passes background to style renderer', () => {\n        expect(renderer.renderStyles.firstCall.args[2]).to.equal(background)\n      })\n    })\n\n    describe('renders scripts', () => {\n      it('renders js', () => {\n        expect(renderer.renderScript.callCount).to.equal(1)\n      })\n\n      it('passes id to style renderer', () => {\n        expect(renderer.renderScript.firstCall.args[0]).to.equal(widget.id)\n      })\n\n      it('passes position to style renderer', () => {\n        expect(renderer.renderScript.firstCall.args[1]).to.equal(widget.name)\n      })\n\n      it('passes background to style renderer', () => {\n        expect(renderer.renderScript.firstCall.args[2]).to.equal(options)\n      })\n    })\n\n    describe('rendered model', () => {\n      it('outputs model for renderer', () => {\n        expect(renderModel).to.equal({\n          id: widget.id,\n          name: widget.name,\n          componentPath: widget.componentPath,\n          markup: 'html',\n          css: 'css',\n          js: 'js'\n        })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget-binder/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget-binder')\n"
  },
  {
    "path": "packages/core/src/widget-binder/widget-binder.js",
    "content": "'use strict'\n\nconst Widget = require('../widget')\nconst EventEmitter = require('events')\nconst transformLoader = require('../transform-loader')\nconst widgetDatasourceBinding = require('../widget-datasource-binding')\n\nfunction fetchDatasource (datasources, datasourceId) {\n  const loopbackDatasource = {\n    emitter: new EventEmitter()\n  }\n\n  return datasources[datasourceId] || loopbackDatasource\n}\n\nexports.load = function (dashboard, widgets = [], datasources = {}) {\n  return widgets.reduce((curr, descriptor) => {\n    const {\n      name,\n      position,\n      background,\n      datasource: datasourceId,\n      transformations,\n      widget: widgetPath,\n      options\n    } = descriptor\n\n    const widget = Widget.create(widgetPath, { position, background, options })\n\n    const datasource = fetchDatasource(datasources, datasourceId)\n    widget.register(datasource.emitter)\n\n    const transforms = transformations ? transformLoader.load(name, transformations) : []\n    widgetDatasourceBinding.bindEvent(dashboard, widget, datasource, transforms)\n\n    datasource.emitter.on('plugin', (eventName, data) => {\n      dashboard.emit(eventName, data)\n    })\n\n    curr[widget.id] = widget\n    return curr\n  }, {})\n}\n"
  },
  {
    "path": "packages/core/src/widget-binder/widget-binder.spec.js",
    "content": "'use strict'\n\nconst { load } = require('.')\nconst { expect } = require('code')\nconst { stub } = require('sinon')\nconst Widget = require('../widget')\nconst widgetDatasourceBinding = require('../widget-datasource-binding')\nconst EventEmitter = require('events')\nconst transformLoader = require('../transform-loader')\n\ndescribe('widget-binder', () => {\n  context('no widgets specified', () => {\n    it('has empty widget list', () => {\n      const widgets = load({}, [], {})\n      expect(widgets).to.equal({})\n    })\n  })\n\n  context('widget specified without datasource', () => {\n    let result\n    let stubWidget\n\n    beforeEach(() => {\n      const widgets = [{}]\n      stubWidget = { register: stub() }\n      stub(Widget, 'create').returns(stubWidget)\n      stub(widgetDatasourceBinding, 'bindEvent')\n      result = load({}, widgets, {})\n    })\n\n    afterEach(() => {\n      widgetDatasourceBinding.bindEvent.restore()\n      Widget.create.restore()\n    })\n\n    it('returns a list of initialised widgets', () => {\n      const widgetIds = Object.keys(result)\n      expect(widgetIds).to.have.length(1)\n    })\n\n    it('widget is registered', () => {\n      expect(stubWidget.register.callCount).to.equal(1)\n    })\n\n    it('loopback datasource is wired up', () => {\n      expect(stubWidget.register.firstCall.args[0]).to.be.an.instanceof(EventEmitter)\n    })\n  })\n\n  context('widget and datasource specified', () => {\n    const datasources = { xyz: { emitter: new EventEmitter() } }\n    let stubWidget\n\n    beforeEach(() => {\n      const widgets = [{ datasource: 'xyz' }]\n      stubWidget = { register: stub() }\n      stub(Widget, 'create').returns(stubWidget)\n      stub(widgetDatasourceBinding, 'bindEvent')\n      load({}, widgets, datasources)\n    })\n\n    afterEach(() => {\n      widgetDatasourceBinding.bindEvent.restore()\n      Widget.create.restore()\n    })\n\n    it('widget is registered', () => {\n      expect(stubWidget.register.callCount).to.equal(1)\n    })\n\n    it('datasource is wired up', () => {\n      expect(stubWidget.register.firstCall.args[0]).to.equal(datasources.xyz.emitter)\n    })\n  })\n\n  context('loads transformations from configuration', () => {\n    let stubWidget\n\n    const widgets = [{\n      name: 'xyz',\n      transformations: [{\n        transformer: 'xxx',\n        options: {\n          foo: 'bar'\n        }\n      }]\n    }]\n\n    beforeEach(() => {\n      stubWidget = { register: stub() }\n      stub(Widget, 'create').returns(stubWidget)\n      stub(transformLoader, 'load').returns([{ baz: 'qux' }])\n      stub(widgetDatasourceBinding, 'bindEvent')\n      load({}, widgets, {})\n    })\n\n    afterEach(() => {\n      transformLoader.load.restore()\n      widgetDatasourceBinding.bindEvent.restore()\n      Widget.create.restore()\n    })\n\n    it('widget is registered', () => {\n      expect(stubWidget.register.callCount).to.equal(1)\n    })\n\n    it('transformations are loaded', () => {\n      expect(transformLoader.load.callCount).to.equal(1)\n    })\n\n    it('transformation loader gets widget name', () => {\n      expect(transformLoader.load.firstCall.args[0]).to.equal('xyz')\n    })\n\n    it('transformation loader gets transformation configuration', () => {\n      expect(transformLoader.load.firstCall.args[1]).to.equal(widgets[0].transformations)\n    })\n\n    it('calls bindEvent with transformer map', () => {\n      expect(widgetDatasourceBinding.bindEvent.firstCall.args[3]).to.equal([{ baz: 'qux' }])\n    })\n  })\n\n  context('plugin event fired', () => {\n    const dashboard = { emit: null }\n\n    beforeEach(() => {\n      const widgets = [{ datasource: 'xyz' }]\n      const datasources = { xyz: { emitter: new EventEmitter() } }\n      stub(Widget, 'create').returns({ register: stub() })\n      dashboard.emit = stub()\n      load(dashboard, widgets, datasources)\n      datasources.xyz.emitter.emit('plugin', 'xyzzy', 'abcde')\n    })\n\n    afterEach(() => {\n      Widget.create.restore()\n    })\n\n    it('plugin event from widget causes dashboard emit', () => {\n      expect(dashboard.emit.callCount).to.equal(1)\n    })\n\n    it('dashboard plugin event has correct name', () => {\n      expect(dashboard.emit.firstCall.args[0]).to.equal('xyzzy')\n    })\n\n    it('dashboard plugin event has correct data', () => {\n      expect(dashboard.emit.firstCall.args[1]).to.equal('abcde')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/widget-datasource-binding/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget-datasource-binding')\n"
  },
  {
    "path": "packages/core/src/widget-datasource-binding/widget-datasource-binding.js",
    "content": "'use strict'\n\nconst dashboardEvent = require('../dashboard-event')\n\nfunction transform (data, transformers) {\n  return transformers.reduce((current, next) => {\n    return next.transform(current)\n  }, data)\n}\n\nexports.bindEvent = function (dashboard, widget, datasource, transformers) {\n  datasource.emitter.on('update', value => {\n    const event = `${widget.id}:update`\n    const hasTransformers = !!(transformers && transformers.length)\n    const transformed = hasTransformers ? transform(value, transformers) : value\n    const result = widget.update(transformed)\n    const payload = dashboardEvent.build(result)\n    dashboard.emit(event, payload)\n  })\n}\n"
  },
  {
    "path": "packages/core/src/widget-datasource-binding/widget-datasource-binding.spec.js",
    "content": "'use strict'\n\nconst widgetDatasourceBinder = require('.')\nconst { stub } = require('sinon')\nconst { expect } = require('code')\nconst EventEmitter = require('events')\n\ndescribe('widget-datasource-binding', () => {\n  context('datasource has emitter', () => {\n    const widget = {}\n    const dashboard = {}\n\n    it('event is bound', () => {\n      const datasource = { emitter: { on: stub() } }\n      widgetDatasourceBinder.bindEvent(dashboard, widget, datasource, [])\n      expect(datasource.emitter.on.callCount).to.equal(1)\n    })\n  })\n\n  context('widget emits data', () => {\n    const rawData = {\n      user: {\n        firstName: 'Alfred',\n        lastName: 'Wilks'\n      }\n    }\n\n    const widget = { id: 'abc', update: stub().returns(rawData) }\n    const dashboard = { emit: stub() }\n\n    before(() => {\n      const emitter = new EventEmitter()\n      const datasource = { emitter }\n\n      widgetDatasourceBinder.bindEvent(dashboard, widget, datasource, [])\n      dashboard.emit = stub()\n\n      datasource.emitter.emit('update')\n    })\n\n    it('dashboard event is emitted', () => {\n      expect(dashboard.emit.callCount).to.equal(1)\n    })\n\n    it('emitted event has widget id', () => {\n      expect(dashboard.emit.firstCall.args[0]).to.equal('abc:update')\n    })\n\n    it('emitted event has data', () => {\n      expect(dashboard.emit.firstCall.args[1].data).to.equal(rawData)\n    })\n\n    it('emitted event has metaData', () => {\n      expect(dashboard.emit.firstCall.args[1].meta).to.include('updated')\n    })\n  })\n\n  context('output is transformed', () => {\n    const widgetOutput = {\n      fullName: 'Alfred Wilks'\n    }\n\n    const transformedData = { foo: 'bar' }\n\n    const transformers = [{\n      transform: stub().returns(transformedData)\n    }]\n\n    const widget = {\n      update: stub()\n        .withArgs(transformedData)\n        .returns(widgetOutput)\n    }\n    const dashboard = { emit: stub() }\n    const datasource = { emitter: new EventEmitter() }\n\n    before(() => {\n      widgetDatasourceBinder.bindEvent(dashboard, widget, datasource, transformers)\n      dashboard.emit = stub()\n\n      datasource.emitter.emit('update')\n    })\n\n    it('dashboard event is emitted', () => {\n      expect(dashboard.emit.callCount).to.equal(1)\n    })\n\n    it('emitted event has transformed data', () => {\n      expect(\n        dashboard.emit.firstCall.args[1].data\n      ).to.equal(widgetOutput)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/test/resources/widgets/broken/package.json",
    "content": "{\n  \"name\": \"vudash-widget-broken\",\n  \"main\": \"widget.js\"\n}\n"
  },
  {
    "path": "packages/core/test/resources/widgets/broken/widget.js",
    "content": "'use strict'\n\nexports.register = () => {\n  return {}\n}\n"
  },
  {
    "path": "packages/core/test/resources/widgets/configurable/component.html",
    "content": "<h1>hi</h1>"
  },
  {
    "path": "packages/core/test/resources/widgets/configurable/package.json",
    "content": "{\n  \"name\": \"vudash-widget-configurable\",\n  \"main\": \"widget.js\",\n  \"vudash\": {\n    \"component\": \"./component.html\"\n  }\n}\n"
  },
  {
    "path": "packages/core/test/resources/widgets/configurable/widget.js",
    "content": "'use strict'\n\nconst { Promise } = require('bluebird')\nconst defaults = {\n  foo: 'bar',\n  working: false\n}\n\nclass ExampleWidget {\n  constructor (options) {\n    this.options = Object.assign({}, defaults, options)\n  }\n\n  update (data) {\n    return Promise.resolve(this.options)\n  }\n}\n\nexports.register = (options) => {\n  return new ExampleWidget(options)\n}\n"
  },
  {
    "path": "packages/core/test/resources/widgets/example/markup.html",
    "content": "<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",
    "content": "{\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",
    "content": "'use strict'\n\nconst { Promise } = require('bluebird')\n\nclass ExampleWidget {\n  update (data) {\n    return Promise.resolve({x: 'y'})\n  }\n}\n\nexports.register = () => {\n  return new ExampleWidget()\n}\n"
  },
  {
    "path": "packages/core/test/resources/widgets/missing/package.json",
    "content": "{\n  \"name\": \"vudash-widget-missing\",\n  \"main\": \"widget.js\"\n}\n"
  },
  {
    "path": "packages/core/test/resources/widgets/missing/widget.js",
    "content": "'use strict'\n\nexports.register = () => {\n  return {}\n}\n"
  },
  {
    "path": "packages/core/test/util/dashboard.builder.js",
    "content": "'use strict'\n\nconst WidgetBuilder = require('./widget.builder')\n\nclass DashboardBuilder {\n  constructor () {\n    this.overrides = {\n      widgets: []\n    }\n  }\n\n  withName (name = 'Some Dashboard') {\n    this.overrides.name = name\n    return this\n  }\n\n  addDatasource (moduleName, options) {\n    this.overrides.datasources = this.datasources || {}\n    this.overrides.datasources[moduleName] = {\n      module: moduleName,\n      options\n    }\n    return this\n  }\n\n  addWidget (widget = WidgetBuilder.create().build()) {\n    this.overrides.widgets.push(widget)\n    return this\n  }\n\n  build () {\n    return Object.assign({}, {\n      layout: {\n        rows: 4,\n        columns: 5\n      }\n    }, this.overrides)\n  }\n}\n\nmodule.exports = {\n  create: () => { return new DashboardBuilder() }\n}\n"
  },
  {
    "path": "packages/core/test/util/datasource.builder.js",
    "content": "'use strict'\n\nclass Datasource {\n  constructor (options) {\n    this.options = options\n  }\n\n  fetch () {\n    return this.options\n  }\n}\n\nclass DatasourceBuilder {\n  constructor () {\n    this.ds = Datasource\n  }\n\n  build () {\n    return this.ds\n  }\n}\n\nmodule.exports = {\n  create: () => { return new DatasourceBuilder() }\n}\n"
  },
  {
    "path": "packages/core/test/util/widget.builder.js",
    "content": "'use strict'\n\nconst { Promise } = require('bluebird')\n\nclass WidgetBuilder {\n  constructor () {\n    this.widget = this._createWidgetModule()\n    this.position = { x: 0, y: 0, w: 0, h: 0 }\n  }\n\n  _createWidgetModule (internals = { schedule: 1000, job: () => { return Promise.resolve({}) } }) {\n    const Module = class MyWidget {\n      register (options) {\n        return Object.assign({}, internals, { config: options })\n      }\n    }\n    const component = './src/component.html'\n    const name = 'VudashMyWidget'\n\n    return {\n      Module,\n      component,\n      name\n    }\n  }\n\n  withJob (job = Promise.resolve({}), schedule = 1000) {\n    this.widget = this._createWidgetModule({ job, schedule })\n    return this\n  }\n\n  withOptions (options = {}) {\n    this.options = options\n    return this\n  }\n\n  withWidget (widget) {\n    this.widget = widget\n    return this\n  }\n\n  build () {\n    return {\n      position: this.position,\n      widget: this.widget,\n      options: this.options\n    }\n  }\n}\n\nmodule.exports = {\n  create: () => { return new WidgetBuilder() }\n}\n"
  },
  {
    "path": "packages/datasource-google-sheets/datasource.js",
    "content": "'use strict'\n\nconst GoogleSheetsTransport = require('./src/google-sheets-transport')\nconst { validation } = require('./src/config-validator')\n\nexports.validation = validation\n\nexports.register = function (options) {\n  return new GoogleSheetsTransport(options)\n}\n"
  },
  {
    "path": "packages/datasource-google-sheets/datasource.spec.js",
    "content": "'use strict'\n\nconst datasource = require('./datasource')\nconst { expect } = require('code')\n\ndescribe('datasource-google-sheets/datasource', () => {\n  it('has register method', () => {\n    expect(datasource.register).to.exist().and.to.be.a.function()\n  })\n\n  it('exports validation', () => {\n    expect(datasource.validation).to.exist()\n  })\n})\n"
  },
  {
    "path": "packages/datasource-google-sheets/package.json",
    "content": "{\n  \"name\": \"@vudash/datasource-google-sheets\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Google Sheets datasource for Vudash\",\n  \"main\": \"datasource.js\",\n  \"scripts\": {\n    \"lint\": \"../../node_modules/.bin/standard\",\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.7\",\n    \"hoek\": \"^4.1.0\",\n    \"joi\": \"^10.2.1\",\n    \"spreadsheet-to-json\": \"^1.0.5\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/datasource-google-sheets/src/config-validator/config-validator.spec.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst configUtil = require('../../test/config.util')\nconst sinon = require('sinon')\nconst validator = require('.')\nconst { expect } = require('code')\n\ndescribe('datasource-google-sheets.config-validator', () => {\n  const sandbox = sinon.sandbox.create()\n\n  afterEach(() => {\n    sandbox.restore()\n  })\n\n  it('With invalid config', () => {\n    const { error } = Joi.validate({}, validator.validation)\n    expect(error).to.be.an.error(/fails because/)\n  })\n\n  it('With valid single-cell config', () => {\n    const { error } = Joi.validate(configUtil.getSingleCellConfig(), validator.validation)\n    expect(error).not.to.exist()\n  })\n\n  it('With valid range config', () => {\n    const { error } = Joi.validate(configUtil.getRangeConfig(), validator.validation)\n    expect(error).not.to.exist()\n  })\n\n  it('Invalid credentials file', () => {\n    const credentials = 'xxx:yyy'\n    const config = configUtil.getSingleCellConfig(credentials)\n    const { error } = Joi.validate(config, validator.validation)\n    expect(error).to.be.an.error()\n  })\n})\n"
  },
  {
    "path": "packages/datasource-google-sheets/src/config-validator/index.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\n\nclass ConfigValidator {\n  get inlineCredentialsValidation () {\n    return Joi.object().required().keys({\n      type: Joi.string().required().only('service_account').description('Key type'),\n      project_id: Joi.string().required().description('Project name'),\n      private_key_id: Joi.string().required().description('Project name'),\n      private_key: Joi.string().required().description('Project name'),\n      client_email: Joi.string().email().required().description('Project name'),\n      client_id: Joi.string().required().description('Project name'),\n      auth_uri: Joi.string().uri().required().description('Auth Uri'),\n      token_uri: Joi.string().uri().required().description('Token Uri'),\n      auth_provider_x509_cert_url: Joi.string().uri().required().description('Auth provider x509 certificate url'),\n      client_x509_cert_url: Joi.string().uri().required().description('Client x509 certificate url')\n    })\n  }\n\n  get fileCredentialsValidation () {\n    return Joi.string().regex(/^file:.*/).required().description('Filesystem path to credentials json, prefixed with \"file:\"')\n  }\n\n  get validation () {\n    return {\n      sheet: Joi.string().required().description('Sheet id'),\n      tab: Joi.string().required().description('Tab Name'),\n      columns: this.columnSchema,\n      rows: this.rowSchema,\n      credentials: Joi.alternatives([\n        this.inlineCredentialsValidation,\n        this.fileCredentialsValidation\n      ]).required().description('Service account credentials')\n    }\n  }\n\n  get columnSchema () {\n    return Joi.alternatives([\n      Joi.string().required().description('Column heading'),\n      Joi.array().required().description('Array of column headings')\n    ]).required().description('Column or an array of Columns to retrieve')\n  }\n\n  get rowSchema () {\n    return Joi.alternatives([\n      Joi.number().required().description('Row number'),\n      Joi.object().keys({\n        from: Joi.number().required().description('First row in range'),\n        to: Joi.number().required().description('Last row in range')\n      }).required().description('Range selector')\n    ]).required().description('Row number to retrieve, or object with \"from\" and \"to\" row numbers to select a range')\n  }\n}\n\nmodule.exports = new ConfigValidator()\n"
  },
  {
    "path": "packages/datasource-google-sheets/src/google-sheets-transport/google-sheets-transport.spec.js",
    "content": "'use strict'\n\nconst GoogleSheetsTransport = require('.')\nconst configUtil = require('../../test/config.util')\nconst sinon = require('sinon')\nconst { expect } = require('code')\n\ndescribe('google-sheets-transport', () => {\n  const sandbox = sinon.sandbox.create()\n\n  afterEach(() => {\n    sandbox.restore()\n  })\n\n  context('External credentials', () => {\n    it('Loads credentials from disk', () => {\n      const contents = require('../../test/example.credentials.test.json')\n      const credentials = 'file:../../test/example.credentials.test.json'\n      const config = configUtil.getSingleCellConfig(credentials)\n      const transport = new GoogleSheetsTransport(config)\n      expect(transport.credentials).to.equal(contents)\n    })\n\n    it('File not found', () => {\n      expect(() => {\n        return new GoogleSheetsTransport(configUtil.getSingleCellConfig('file:some-nonexistent-file'))\n      }).to.throw(Error, /some-nonexistent-file\" as it could not be found/)\n    })\n\n    it('Validates credentials loaded from disk', () => {\n      expect(() => {\n        return new GoogleSheetsTransport(configUtil.getSingleCellConfig('file:../../test/example.invalid-credentials.test.json'))\n      }).to.throw(Error, /fails because/)\n    })\n\n    it('Read credentials from file', () => {\n      const credentials = 'file:../../test/example.credentials.test.json'\n      const transport = new GoogleSheetsTransport(configUtil.getSingleCellConfig(credentials))\n      expect(transport).to.be.an.instanceOf(GoogleSheetsTransport)\n    })\n  })\n\n  it('Fetches single cell sheet data', () => {\n    const config = configUtil.getSingleCellConfig()\n    const transport = new GoogleSheetsTransport(config)\n\n    sinon.stub(transport, 'extract')\n      .withArgs({\n        spreadsheetKey: config.sheet,\n        credentials: config.credentials,\n        sheetsToExtract: [config.tab]\n      }).returns(Promise.resolve({\n        [config.tab]: [\n          {\n            [config.columns]: 'myValue'\n          }\n        ]\n      }))\n\n    return transport.fetch().then((result) => {\n      expect(transport.extract.callCount).to.equal(1)\n      expect(result).to.equal('myValue')\n    })\n  })\n\n  it('Fetches range of sheet data', () => {\n    const config = configUtil.getRangeConfig()\n    const transport = new GoogleSheetsTransport(config)\n\n    sinon.stub(transport, 'extract')\n      .withArgs({\n        spreadsheetKey: config.sheet,\n        credentials: config.credentials,\n        sheetsToExtract: [config.tab]\n      }).returns(Promise.resolve({\n        [config.tab]: [\n          {\n            [config.columns[0]]: 'cell0,0',\n            [config.columns[1]]: 'cell0,1'\n          },\n          {\n            [config.columns[0]]: 'cell1,0',\n            [config.columns[1]]: 'cell1,1'\n          },\n          {\n            [config.columns[0]]: 'cell2,0',\n            [config.columns[1]]: 'cell2,1'\n          }\n        ]\n      }))\n\n    return transport.fetch().then((result) => {\n      expect(transport.extract.callCount).to.equal(1)\n      expect(result).to.equal([\n        ['cell0,0', 'cell0,1'],\n        ['cell1,0', 'cell1,1'],\n        ['cell2,0', 'cell2,1']\n      ])\n    })\n  })\n\n  it('Fetches single column of sheet data', () => {\n    const config = configUtil.getSingleColumnConfig()\n    const transport = new GoogleSheetsTransport(config)\n\n    sinon.stub(transport, 'extract')\n      .withArgs({\n        spreadsheetKey: config.sheet,\n        credentials: config.credentials,\n        sheetsToExtract: [config.tab]\n      }).returns(Promise.resolve({\n        [config.tab]: [\n          {\n            [config.columns[0]]: 'cell0,0',\n            unused: 'cell0,1'\n          },\n          {\n            [config.columns[0]]: 'cell1,0',\n            unused: 'cell1,1'\n          },\n          {\n            [config.columns[0]]: 'cell2,0',\n            unused: 'cell2,1'\n          }\n        ]\n      }))\n\n    return transport.fetch().then((result) => {\n      expect(transport.extract.callCount).to.equal(1)\n      expect(result).to.equal([\n        'cell0,0',\n        'cell1,0',\n        'cell2,0'\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/datasource-google-sheets/src/google-sheets-transport/index.js",
    "content": "'use strict'\n\nconst path = require('path')\nconst fs = require('fs')\nconst { Promise } = require('bluebird')\nconst spreadsheetToJson = require('spreadsheet-to-json')\nconst configValidator = require('../config-validator')\n\nclass GoogleSheetsTransport {\n  constructor (options) {\n    this.config = options\n    this.extract = Promise.promisify(spreadsheetToJson.extractSheets)\n\n    this.credentials = options.credentials\n    if (typeof this.credentials === 'string') {\n      this.credentials = this.loadCredentialsFromDisk(this.credentials)\n    }\n  }\n\n  static get widgetValidation () {\n    return configValidator.widgetValidation\n  }\n\n  loadCredentialsFromDisk () {\n    if (this.credentials.indexOf('file:') !== 0) {\n      throw new Error('File credentials must be prefixed with \"file:\" and reference a local json service account credentials file')\n    }\n    const fileName = this.credentials.split(':')[1]\n    const resolvedFile = path.join(__dirname, fileName)\n\n    if (!fs.existsSync(resolvedFile)) {\n      throw new Error(`Credentials could not be loaded from \"${resolvedFile}\" as it could not be found`)\n    }\n\n    const credentials = require(resolvedFile)\n    this.validate(configValidator.inlineCredentialsValidation, credentials)\n\n    return credentials\n  }\n\n  validate (schema, credentials) {\n    schema.validate(credentials, (err) => {\n      if (err) { throw err }\n    })\n  }\n\n  fetch () {\n    const conf = this.config\n\n    return this.extract({\n      spreadsheetKey: conf.sheet,\n      credentials: conf.credentials,\n      sheetsToExtract: [conf.tab]\n    }).then((data) => {\n      return this._extractCellData(data)\n    })\n  }\n\n  _extractCellData (data) {\n    const conf = this.config\n    const tab = data[conf.tab]\n    const multiCell = (typeof conf.rows === 'object' || Array.isArray(conf.columns))\n\n    return multiCell ? this._toMatrix(tab) : this._extractCellValue(tab)\n  }\n\n  _toMatrix (tab) {\n    const conf = this.config\n\n    return tab.map((row) => {\n      const columns = Object.keys(row).filter((col) => {\n        return conf.columns.includes(col)\n      })\n\n      const values = columns.map((column) => {\n        return row[column]\n      })\n\n      return values.length > 1 ? values : values[0]\n    })\n  }\n\n  _extractCellValue (tab) {\n    const conf = this.config\n    return tab[conf.rows - 1][conf.columns]\n  }\n}\n\nmodule.exports = GoogleSheetsTransport\n"
  },
  {
    "path": "packages/datasource-google-sheets/test/config.util.js",
    "content": "'use strict'\n\nfunction getConfig (credentialsOverride) {\n  const uri = 'http://a.b'\n\n  const credentials = credentialsOverride || {\n    type: 'service_account',\n    project_id: 'd',\n    private_key_id: 'a',\n    private_key: 'b',\n    client_email: 'p@x.y',\n    client_id: '123',\n    auth_uri: uri,\n    token_uri: uri,\n    auth_provider_x509_cert_url: uri,\n    client_x509_cert_url: uri\n  }\n\n  return {\n    sheet: 'x',\n    tab: 'y',\n    credentials\n  }\n}\n\nexports.getRangeConfig = function (credentialsOverride) {\n  const baseConfig = getConfig(credentialsOverride)\n  return Object.assign({ columns: ['a', 'b'], rows: { from: 1, to: 3 } }, baseConfig)\n}\n\nexports.getSingleColumnConfig = function (credentialsOverride) {\n  const baseConfig = getConfig(credentialsOverride)\n  return Object.assign({ columns: ['a'], rows: { from: 1, to: 3 } }, baseConfig)\n}\n\nexports.getSingleCellConfig = function (credentialsOverride) {\n  const baseConfig = getConfig(credentialsOverride)\n  return Object.assign({ columns: 'z', rows: 1 }, baseConfig)\n}\n"
  },
  {
    "path": "packages/datasource-google-sheets/test/example.credentials.test.json",
    "content": "{\"type\":\"service_account\",\"project_id\":\"d\",\"private_key_id\":\"a\",\"private_key\":\"b\",\"client_email\":\"p@x.y\",\"client_id\":\"123\",\"auth_uri\":\"http://x.y\",\"token_uri\":\"http://x.y\",\"auth_provider_x509_cert_url\":\"http://x.y\",\"client_x509_cert_url\":\"http://x.y\"}\n"
  },
  {
    "path": "packages/datasource-google-sheets/test/example.invalid-credentials.test.json",
    "content": "{}\n"
  },
  {
    "path": "packages/datasource-random/datasource.js",
    "content": "'use strict'\n\nconst RandomTransport = require('./src/random-transport')\nconst { validation } = require('./src/datasource-validation')\n\nexports.validation = validation\n\nexports.register = function (options) {\n  return new RandomTransport(options)\n}\n"
  },
  {
    "path": "packages/datasource-random/datasource.spec.js",
    "content": "'use strict'\n\nconst RandomTransport = require('./src/random-transport')\nconst { expect } = require('code')\n\ndescribe('plugin', () => {\n  const MT_SEED = 'a'\n  const AN_EMAIL = 'urijihed@ocekode.lr'\n\n  it('Allows method to be specified', () => {\n    const transport = new RandomTransport({ method: 'email' }, MT_SEED)\n    return transport.fetch().then((email) => {\n      expect(email).to.equal(AN_EMAIL)\n    })\n  })\n\n  it('Unknown method returns an error', () => {\n    const transport = new RandomTransport({ method: 'abcdefg' }, MT_SEED)\n    expect(transport.fetch.bind(transport)).to.throw(Error, /is not a known chance method/)\n  })\n\n  it('Passes options to method', () => {\n    const options = { domain: 'xyz.com' }\n    const transport = new RandomTransport({ method: 'email', options }, MT_SEED)\n    return transport.fetch().then((email) => {\n      expect(email).to.endWith(options.domain)\n    })\n  })\n\n  it('If method is not defaulted, options are not defaulted', () => {\n    const transport = new RandomTransport({ method: 'email' }, MT_SEED)\n    return transport.fetch().then((email) => {\n      expect(email).to.equal(AN_EMAIL)\n    })\n  })\n\n  it('Allow multiple arguments to options', () => {\n    const options = ['chance.integer', 12, { min: 15, max: 32 }]\n    const transport = new RandomTransport({ method: 'n', options }, MT_SEED)\n    return transport.fetch().then((numbers) => {\n      expect(numbers).to.be.an.array()\n      expect(numbers.length).to.equal(12)\n      for (const val of numbers) {\n        expect(val, val).to.be.between(14, 33)\n      }\n    })\n  })\n\n  it('Validate for unknown method references', () => {\n    const options = ['chance.abcde']\n    const transport = new RandomTransport({ method: 'n', options }, MT_SEED)\n    expect(transport.fetch.bind(transport)).to.throw(Error, /is not a known chance method/)\n  })\n\n  it('Allow shorthand chance method names', () => {\n    const options = ['integer', 12, { min: 0, max: 1 }]\n    const transport = new RandomTransport({ method: 'n', options }, MT_SEED)\n    expect(transport.fetch.bind(transport)).not.to.throw()\n  })\n})\n"
  },
  {
    "path": "packages/datasource-random/package.json",
    "content": "{\n  \"name\": \"@vudash/datasource-random\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Random Datasource for Vudash\",\n  \"main\": \"datasource.js\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"transport\",\n    \"datasource\",\n    \"chance\",\n    \"random\",\n    \"values\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.7\",\n    \"chance\": \"^1.0.4\",\n    \"joi\": \"^13.0.2\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/datasource-random/src/datasource-validation/index.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\n\nmodule.exports.validation = {\n  method: Joi.string().optional().description('Chance method name'),\n  options: Joi.alternatives([\n    Joi.object().optional().description('Chance method options'),\n    Joi.array().optional().description('Chance method arguments')\n  ])\n}\n"
  },
  {
    "path": "packages/datasource-random/src/random-transport/index.js",
    "content": "'use strict'\n\nconst Chance = require('chance')\nconst Promise = require('bluebird').Promise\n\nclass RandomTransport {\n  constructor (options, seed = new Date().getTime()) {\n    this.config = options\n    this.chance = new Chance(seed)\n  }\n\n  prepareOptions () {\n    const options = this.config.method ? this.config.options : { min: 0, max: 999 }\n    const args = Array.isArray(options) ? options : [options]\n\n    if (this.config.method === 'n') {\n      const functionReference = options[0]\n      const referenceParts = functionReference.split('.')\n      const functionName = referenceParts.length > 1 ? referenceParts[1] : referenceParts[0]\n      this.validateMethod(functionName)\n      args[0] = this.chance[functionName]\n    }\n\n    return {\n      method: this.config.method || 'natural',\n      options: args\n    }\n  }\n\n  validateMethod (method) {\n    if (typeof this.chance[method] !== 'function') {\n      throw new Error(`${method} is not a known chance method`)\n    }\n  }\n\n  fetch () {\n    const conf = this.prepareOptions()\n    this.validateMethod(conf.method)\n    const result = this.chance[conf.method].apply(this.chance, conf.options)\n    return Promise.resolve(result)\n  }\n}\n\nmodule.exports = RandomTransport\n"
  },
  {
    "path": "packages/datasource-random/src/random-transport/index.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst RandomTransport = require('.')\n\ndescribe('random-transport', () => {\n  const MT_SEED = 'a'\n\n  it('Returns a random number', () => {\n    const transport = new RandomTransport({ config: {} }, MT_SEED)\n    return transport.fetch().then((number) => {\n      expect(number).to.be.above(0).and.below(1000)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/datasource-rest/datasource.js",
    "content": "'use strict'\n\nconst RestTransport = require('./src/rest-transport')\nconst { validation } = require('./src/datasource-validation')\n\nexports.validation = validation\n\nexports.register = function (options) {\n  return new RestTransport(options)\n}\n"
  },
  {
    "path": "packages/datasource-rest/package.json",
    "content": "{\n  \"name\": \"@vudash/datasource-rest\",\n  \"version\": \"9.9.0\",\n  \"description\": \"REST Datasource for Vudash\",\n  \"main\": \"datasource.js\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"got\": \"^6.7.1\",\n    \"hoek\": \"^4.1.0\",\n    \"joi\": \"^10.6.0\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/datasource-rest/src/datasource-validation/datasource-validation.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\n\nexports.validation = {\n  url: Joi.string().description('Url to call'),\n  method: Joi.string().optional().default('get').only('get', 'post', 'put', 'options', 'delete', 'head').description('Http Method'),\n  headers: Joi.object().optional().description('additional headers'),\n  body: Joi.object().optional().description('request body'),\n  graph: Joi.string().optional().description('Graph expression (json path) to reach json values')\n}\n"
  },
  {
    "path": "packages/datasource-rest/src/datasource-validation/datasource-validation.spec.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst { validation } = require('.')\nconst { expect } = require('code')\n\ndescribe('datasource-rest/datasource-validation', () => {\n  it('defaults method to get', () => {\n    const { value } = Joi.validate({}, validation)\n    expect(value.method).to.equal('get')\n  })\n})\n"
  },
  {
    "path": "packages/datasource-rest/src/datasource-validation/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./datasource-validation')\n"
  },
  {
    "path": "packages/datasource-rest/src/rest-transport/index.js",
    "content": "'use strict'\n\nconst got = require('got')\nconst { applyToDefaults } = require('hoek')\nconst pkg = require('../../package.json')\n\nconst internals = {\n  prepareRequest (config) {\n    const options = applyToDefaults({\n      json: true,\n      headers: {\n        'user-agent': `vudash/${pkg.version} (https://github.com/vudash/vudash)`,\n        'content-type': 'application/json'\n      }\n    }, config)\n\n    if (config.body) {\n      options.body = JSON.stringify(config.body)\n    }\n\n    return options\n  }\n}\n\nclass RestTransport {\n  constructor (options) {\n    this.config = options\n  }\n\n  fetch () {\n    const options = internals.prepareRequest(this.config)\n    return got[this.config.method](this.config.url, options)\n      .then(({ body }) => body)\n  }\n}\n\nmodule.exports = RestTransport\n"
  },
  {
    "path": "packages/datasource-rest/src/rest-transport/rest-transport.spec.js",
    "content": "'use strict'\n\nconst nock = require('nock')\nconst RestTransport = require('.')\nconst { expect } = require('code')\n\ndescribe('transports.rest', () => {\n  const host = 'http://example.net'\n  const scenarios = [\n    { method: 'get', host, path: '/' },\n    { method: 'post', host, path: '/some/path' },\n    { method: 'post', host, path: '/some/path', body: { a: 'b', one: 2, true: false } },\n    { method: 'get', host, path: '/lala', query: { foot: 'bart' } },\n    { method: 'post', host, path: '/some/path', body: { a: 'b', one: 2, true: false }, query: { foot: 'bath' } }\n  ]\n\n  afterEach(() => {\n    nock.cleanAll()\n  })\n\n  scenarios.forEach(scenario => {\n    it(`#fetch() with ${scenario.method} ${scenario.host}`, async () => {\n      const config = Object.assign({}, scenario)\n      config.url = `${config.host}${config.path}`\n      delete config.host\n      delete config.path\n      const transport = new RestTransport(config)\n\n      nock(scenario.host)[scenario.method](scenario.path, scenario.body)\n        .query(scenario.query)\n        .reply(200, { a: 'b' })\n\n      const body = await transport.fetch()\n      expect(nock.isDone(), nock.pendingMocks()).to.equal(true)\n      expect(body.a).to.equal('b')\n    })\n  })\n\n  context('Json body', () => {\n    const body = { i: { love: { animals: 'dogs' } } }\n    const options = { method: 'get', url: 'http://example.com/some/stuff' }\n\n    beforeEach(() => {\n      nock('http://example.com')\n        .get('/some/stuff')\n        .reply(200, body)\n    })\n\n    afterEach(() => {\n      nock.cleanAll()\n    })\n\n    it('returns full body', async () => {\n      const transport = new RestTransport(options)\n      const value = await transport.fetch()\n      expect(value).to.equal(body)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/datasource-value/datasource.js",
    "content": "'use strict'\n\nconst ValueTransport = require('./src/value-transport')\nconst { validation } = require('./src/datasource-validation')\n\nexports.validation = validation\n\nexports.register = function (options) {\n  return new ValueTransport(options)\n}\n"
  },
  {
    "path": "packages/datasource-value/package.json",
    "content": "{\n  \"name\": \"@vudash/datasource-value\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Value datasource for Vudash dashboards\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"main\": \"datasource.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.7\",\n    \"joi\": \"^10.2.2\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/datasource-value/src/datasource-validation/datasource-validation.spec.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst { validation } = require('.')\nconst { expect } = require('code')\n\ndescribe('value', () => {\n  context('Widget Validation', () => {\n    it('Requires a value to be passed in config', () => {\n      const { error } = Joi.validate({}, validation)\n      expect(error).to.be.an.error(/\"value\" is required/)\n    })\n\n    const valueTypes = [ 'string', 123, Date.now(), { a: 'x' }, () => {} ]\n\n    valueTypes.forEach((value) => {\n      it(`Allows value ${typeof value} to be passed in`, () => {\n        const { error } = Joi.validate({ value }, validation)\n        expect(error).not.to.exist()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/datasource-value/src/datasource-validation/index.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\n\nmodule.exports.validation = {\n  value: Joi.any().required().description('Value to return')\n}\n"
  },
  {
    "path": "packages/datasource-value/src/value-transport/index.js",
    "content": "'use strict'\n\nconst Promise = require('bluebird').Promise\n\nclass ValueTransport {\n  constructor (options) {\n    this.config = options\n  }\n\n  fetch () {\n    return Promise.resolve(this.config.value)\n  }\n}\n\nmodule.exports = ValueTransport\n"
  },
  {
    "path": "packages/datasource-value/src/value-transport/index.spec.js",
    "content": "'use strict'\n\nconst ValueTransport = require('.')\nconst { expect } = require('code')\n\ndescribe('value', () => {\n  it('Returns value passed in config', () => {\n    const config = { value: 'abc' }\n    const transport = new ValueTransport(config)\n    return transport.fetch().then((value) => {\n      expect(value).to.equal('abc')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/transformer-jq/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./lib/transformer')\n"
  },
  {
    "path": "packages/transformer-jq/lib/transformer.js",
    "content": "'use strict'\n\nconst { jq } = require('jq.node')\nconst { Promise } = require('bluebird')\n\nclass JqTransformer {\n  constructor (transformation) {\n    this.transformation = transformation\n  }\n\n  transform (data) {\n    const input = JSON.stringify(data)\n    return new Promise((resolve, reject) => {\n      jq(input, this.transformation.value, {}, function (err, result) {\n        if (err) {\n          return reject(err)\n        }\n        const json = JSON.parse(result)\n        return resolve(json)\n      })\n    })\n  }\n}\n\nmodule.exports = JqTransformer\n"
  },
  {
    "path": "packages/transformer-jq/lib/transformer.spec.js",
    "content": "'use strict'\n\nconst JqTransformer = require('./transformer')\nconst { expect } = require('code')\n\ndescribe('transformer', () => {\n  const usersIn = {\n    jeff: {\n      email: 'jeff@example.net'\n    },\n    joe: {\n      phone: '+447721981546'\n    },\n    emma: {\n      email: 'emma@example.com'\n    },\n    grayson: {\n      email: 'wailo@example.net'\n    }\n  }\n\n  const groupsOut = {\n    'example.net': [\n      {\n        email: 'jeff@example.net'\n      },\n      {\n        email: 'wailo@example.net'\n      }\n    ],\n    'example.com': [\n      {\n        email: 'emma@example.com'\n      }\n    ]\n  }\n\n  it('transforms value', async () => {\n    const value = 'filter(has(\"email\")) | groupBy(flow(get(\"email\"), split(\"@\"), get(1)))'\n    const transformer = new JqTransformer({ value })\n    const out = await transformer.transform(usersIn)\n    expect(out).to.equal(groupsOut)\n  })\n})"
  },
  {
    "path": "packages/transformer-jq/package.json",
    "content": "{\n  \"name\": \"@vudash/transformer-jq\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Vudash data transformer which uses jq to make complex json transformations\",\n  \"main\": \"transformer.js\",\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"lint\": \"../../node_modules/.bin/standard\",\n    \"test\": \"../../node_modules/.bin/mocha ../../test/unit.lab.js\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"^3.5.1\",\n    \"jq.node\": \"^2.1.1\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/transformer-map/package.json",
    "content": "{\n  \"name\": \"@vudash/transformer-map\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Vudash data transformer which maps from one structure to another\",\n  \"main\": \"transformer.js\",\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"dependencies\": {\n    \"reorient\": \"^2.1.0\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/transformer-map/transformer.js",
    "content": "'use strict'\n\nconst { transform } = require('reorient')\n\nclass MapTransformer {\n  constructor (schema) {\n    this.schema = schema\n  }\n\n  transform (data) {\n    const { value } = transform(data, this.schema)\n    return value\n  }\n}\n\nmodule.exports = MapTransformer\n"
  },
  {
    "path": "packages/widget-chart/package.json",
    "content": "{\n  \"name\": \"@vudash/widget-chart\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Chart widget for Vudash\",\n  \"main\": \"./src/server\",\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"bluebird\": \"^3.5.0\",\n    \"chartist\": \"^0.11.0\"\n  },\n  \"vudash\": {\n    \"component\": \"./src/client/markup.html\"\n  },\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"travis\",\n    \"travis-ci\",\n    \"travisci\",\n    \"widget\",\n    \"dashboard\",\n    \"dashing\",\n    \"build\"\n  ],\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-chart/src/client/chart-options.js",
    "content": "'use strict'\n\nconst ChartTypes = {\n  'line': {\n    constructorName: 'Line',\n    options: {\n      chartPadding: {\n        right: 60\n      }\n    }\n  },\n  'pie': {\n    constructorName: 'Pie'\n  },\n  'bar': {\n    constructorName: 'Bar'\n  },\n  'donut': {\n    constructorName: 'Pie',\n    options: {\n      donut: true,\n      donutWidth: 60,\n      donutSolid: true\n    }\n  }\n}\n\nexports.get = function (chartType) {\n  return ChartTypes[chartType] || ChartTypes.line\n}\n"
  },
  {
    "path": "packages/widget-chart/src/client/markup.html",
    "content": "<div class=\"vudash-chart\">\n  <div ref:chart class=\"value\">\n  </div>\n  <div class=\"label\">\n    {{ config.description }}\n  </div>\n</div>\n\n<style>\n  .value {\n    width: 100%;\n    height: 80%;\n  }\n\n  .label {\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n    font-weight: bold;\n    width: 100%;\n    text-align: center;    \n  }\n\n  .ct-label.ct-horizontal,\n  .ct-label.ct-vertical,\n  .ct-grids > .ct-grid.ct-horizontal,\n  .ct-grids > .ct-grid.ct-vertical {\n    color: dimgrey;\n    stroke: dimgrey;\n  }\n</style>\n\n<script>\n  import Chartist from 'chartist'\n  import 'chartist/dist/chartist.css'\n  import ChartOptions from './chart-options'\n\n  export default {\n    oncreate () {\n      const config = this.get('config')\n      this.set({ labels: config.labels })\n\n      const { constructorName, options } = ChartOptions.get(config.type)\n      const chartConfig = Object.assign({ fullWidth: true }, options)\n      const ChartConstructor = Chartist[constructorName]\n\n      this.chart = new ChartConstructor(this.refs.chart, {\n        labels: this.get('labels'),\n        series: this.get('series')\n      }, chartConfig)\n    },\n\n    methods: {\n      update ({ meta, data }) {\n        const updates = { labels: this.get('labels'), series: data.series }\n        this.set(updates)\n        this.chart.update(updates)\n      }\n    }\n  }\n</script>"
  },
  {
    "path": "packages/widget-chart/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-chart/src/server/widget.js",
    "content": "'use strict'\n\nconst defaults = {\n  description: '',\n  labels: [],\n  type: 'line'\n}\n\nclass ChartWidget {\n  constructor (options) {\n    this.config = Object.assign({}, defaults, options)\n  }\n\n  update (data) {\n    const names = Object.keys(data)\n    const series = names.map(names => {\n      return data[names]\n    })\n    return { series }\n  }\n}\n\nexports.register = function (options) {\n  return new ChartWidget(options)\n}\n"
  },
  {
    "path": "packages/widget-chart/src/server/widget.spec.js",
    "content": "'use strict'\n\nconst { register } = require('.')\nconst { expect } = require('code')\n\ndescribe('widget', () => {\n  context('Default configuration', () => {\n    const scenarios = [\n      { attribute: 'description', defaultValue: '', override: 'Time taken' },\n      { attribute: 'labels', defaultValue: [], override: ['a', 'b', 'c'] },\n      { attribute: 'type', defaultValue: 'line', override: 'bar' }\n    ]\n\n    scenarios.forEach(({ attribute, defaultValue, override }) => {\n      it(`Default ${attribute}`, () => {\n        const model = register({})\n        expect(model.config[attribute]).to.equal(defaultValue)\n      })\n\n      it(`Override ${attribute}`, () => {\n        const model = register({ [attribute]: override })\n        expect(model.config[attribute]).to.equal(override)\n      })\n    })\n  })\n\n  context('Renders data series', () => {\n    let model\n\n    before(() => {\n      model = register({})\n    })\n\n    it('Renders multiple datasets', () => {\n      const data = {\n        incremental: [1, 2, 3, 4, 5, 6],\n        exponential: [1, 4, 9, 16, 25, 36],\n        incidental: [1, 2, 6, 12, 20, 30]\n      }\n\n      const result = model.update(data)\n\n      expect(result.series).to.equal([\n        data.incremental,\n        data.exponential,\n        data.incidental\n      ])\n    })\n\n    it('Renders simple array dataset', () => {\n      const data = [1, 4, 9, 16, 25, 36]\n\n      const result = model.update(data)\n\n      expect(result.series).to.equal(data)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-ci/.gitignore",
    "content": "node_modules\n*.log\n"
  },
  {
    "path": "packages/widget-ci/README.MD",
    "content": "# Vudash CI Widget\n\nShows the status of CI builds on a [Vudash](https://npmjs.org/vudash) Dashboard\n\nIndicates project build status for a number of CI providers.\n\nDocumentation has moved [to github](https://vudash.com#ci-widget)"
  },
  {
    "path": "packages/widget-ci/package.json",
    "content": "{\n  \"name\": \"vudash-widget-ci\",\n  \"version\": \"9.9.0\",\n  \"description\": \"A CI Widget for Vudash\",\n  \"main\": \"./src/server\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"travis\",\n    \"travis-ci\",\n    \"travisci\",\n    \"widget\",\n    \"dashboard\",\n    \"dashing\",\n    \"build\"\n  ],\n  \"vudash\": {\n    \"component\": \"./src/client/markup.html\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.6\",\n    \"circleci\": \"^0.3.2\",\n    \"hoek\": \"^4.1.0\",\n    \"joi\": \"^9.2.0\",\n    \"moment\": \"^2.22.0\",\n    \"travis-ci\": \"^2.1.1\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-ci/src/build-status.enum.js",
    "content": "class BuildStatus {\n  static get passed () {\n    return 'passed'\n  }\n\n  static get failed () {\n    return 'failed'\n  }\n\n  static get unknown () {\n    return 'unknown'\n  }\n\n  static get queued () {\n    return 'queued'\n  }\n\n  static get running () {\n    return 'running'\n  }\n}\n\nmodule.exports = BuildStatus\n"
  },
  {
    "path": "packages/widget-ci/src/client/markup.html",
    "content": "<div class=\"ci-widget {{ icon }}\">\n  <div class=\"value\">\n    {{#if icon === 'running'}}\n    <svg width=\"54%\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n        viewBox=\"0 0 50 50\" xml:space=\"preserve\">\n      <path id=\"running\" d=\"M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z\">\n      <animateTransform attributeType=\"xml\"\n        attributeName=\"transform\"\n        type=\"rotate\"\n        from=\"0 25 25\"\n        to=\"360 25 25\"\n        dur=\"1s\"\n        repeatCount=\"indefinite\"/>\n      </path>\n    </svg>\n    {{/if}}\n    {{#if icon === 'passed'}}\n      <svg width=\"58%\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"50 0 100 100\" x=\"0px\" y=\"0px\">\n        <circle cx=\"100\" cy=\"50\" r=\"32\" stroke-width=\"8\" fill=\"none\" />\n      </svg>\n    {{/if}}\n    {{#if icon === 'failed'}}\n      <svg width=\"40%\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" viewBox=\"0 0 1000 1000\"  xml:space=\"preserve\">\n        <path d=\"M962.6,830.4L632.1,500l330.5-330.5c36.5-36.5,36.5-95.6,0-132.1c-36.5-36.4-95.6-36.5-132.2,0L500,367.8L169.5,37.4c-36.4-36.4-95.6-36.5-132.1,0c-36.5,36.5-36.4,95.7,0,132.1L367.8,500L37.4,830.4c-36.4,36.5-36.4,95.7,0,132.2c36.5,36.5,95.7,36.4,132.1,0L500,632.1l330.4,330.5c36.6,36.5,95.6,36.5,132.2,0C999.1,926.1,999.1,867,962.6,830.4z\" />\n      </svg>\n    {{/if}}\n    {{#if icon === 'queued'}}\n      <svg width=\"40%\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" viewBox=\"0 -200 1600 1600\"  xml:space=\"preserve\">\n        <path id=\"pause\"\n          d=\"M 1536,1344 V -64 q 0,-26 -19,-45 -19,-19 -45,-19 H 960 q -26,0 -45,19 -19,19 -19,45 v 1408 q 0,26 19,45 19,19 45,19 h 512 q 26,0 45,-19 19,-19 19,-45 z m -896,0 V -64 q 0,-26 -19,-45 -19,-19 -45,-19 H 64 q -26,0 -45,19 -19,19 -19,45 v 1408 q 0,26 19,45 19,19 45,19 h 512 q 26,0 45,-19 19,-19 19,-45 z\" />   \n      </svg>\n    {{/if}}\n    {{#if ['error', 'unknown'].includes(icon)}}\n      <svg width=\"60%\" viewBox=\"0 0 1792 1792\" xmlns=\"http://www.w3.org/2000/svg\">\n        <path id=\"error\" d=\"M1088 1256v240q0 16-12 28t-28 12h-240q-16 0-28-12t-12-28v-240q0-16 12-28t28-12h240q16 0 28 12t12 28zm316-600q0 54-15.5 101t-35 76.5-55 59.5-57.5 43.5-61 35.5q-41 23-68.5 65t-27.5 67q0 17-12 32.5t-28 15.5h-240q-15 0-25.5-18.5t-10.5-37.5v-45q0-83 65-156.5t143-108.5q59-27 84-56t25-76q0-42-46.5-74t-107.5-32q-65 0-108 29-35 25-107 115-13 16-31 16-12 0-25-8l-164-125q-13-10-15.5-25t5.5-28q160-266 464-266 80 0 161 31t146 83 106 127.5 41 158.5z\"/>\n      </svg>\n    {{/if}}\n  </div>\n  <div class=\"label\">\n    {{#if !config.hideOwner}}<span class=\"ci-user\">{{ config.user }}</span> / {{/if}}<span class=\"ci-repo\">{{ config.repo }}</span>\n  </div>\n</div>\n\n<style>\n  .ci-widget > * {\n    width: 100%;\n    text-align: center;\n  }\n\n  .label {\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n    font-weight: bold;\n  }\n\n  svg {\n\t\tdisplay: block;\n\t\tmargin: auto;\n\t}\n\n  svg path#running {\n    fill: #fff;\n  }\n\n\tsvg path#pause {\n\t  fill: #ffd700;\n\t}\n\n  svg path#error {\n\t  fill: #000;\n\t}\n\n  svg circle {\n    stroke: #0f0;\n  }\n\n  @keyframes failed-pulse {\n    0% { background-color: rgb(159,20,255); }\n    25% { background-color: rgb(232,18,205) }\n    50% { background-color: rgb(255,32,33); }\n    75% { background-color: rgb(232,83,32); }\n    100% { background-color: rgb(255,139,18); }\n  }\n\n  .ci-widget.error, .ci-widget.failed {\n    background-color: rgb(255,32,33);\n    animation: failed-pulse 4s infinite alternate;\n    border-radius: 10px;\n  }\n</style>\n\n<script>\n'use strict'\n\nexport default {\n  methods: {\n    update ({ meta, data }) {\n      const icon = data.error ? 'error' : data.status\n\n      this.set({ icon })\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "packages/widget-ci/src/engines/circleci/circleci.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst CircleCI = require('circleci')\nconst BuildStatus = require('../../build-status.enum')\nconst Engine = require('.')\nconst { stub } = require('sinon')\n\ndescribe('engines.circleci', () => {\n  let getBranchBuildsStub\n\n  context('Build Passed', () => {\n    let status\n    const options = {\n      repo: 'repo',\n      user: 'user',\n      branch: 'branch',\n      options: {\n        auth: 'x'\n      }\n    }\n\n    before(() => {\n      const engine = new Engine(options)\n      const circleci = new CircleCI({ auth: 'x' })\n      engine.circleci = circleci\n\n      getBranchBuildsStub = stub(circleci, 'getBranchBuilds')\n      getBranchBuildsStub.resolves([{\n        status: 'success'\n      }])\n\n      return engine.fetchBuildStatus()\n        .then((result) => {\n          status = result\n        })\n    })\n\n    after(() => {\n      getBranchBuildsStub.restore()\n    })\n\n    it('has correct repo', () => {\n      expect(getBranchBuildsStub.firstCall.args[0].project).to.equal(options.repo)\n    })\n\n    it('has correct user', () => {\n      expect(getBranchBuildsStub.firstCall.args[0].username).to.equal(options.user)\n    })\n\n    it('has correct branch', () => {\n      expect(getBranchBuildsStub.firstCall.args[0].branch).to.equal(options.branch)\n    })\n\n    it('fetches correct status', () => {\n      expect(status).to.equal(BuildStatus.passed)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-ci/src/engines/circleci/index.js",
    "content": "'use strict'\n\nconst CircleCI = require('circleci')\nconst BuildStatus = require('../../build-status.enum')\n\nclass CircleCIEngine {\n  constructor (options) {\n    this.circleci = new CircleCI({ auth: options.options.auth })\n    this.user = options.user\n    this.repo = options.repo\n    this.branch = options.branch\n    this.mappings = {\n      success: BuildStatus.passed,\n      failed: BuildStatus.failed,\n      fixed: BuildStatus.passed,\n      canceled: BuildStatus.failed,\n      infrastructure_fail: BuildStatus.failed,\n      timedout: BuildStatus.failed,\n      running: BuildStatus.running,\n      queued: BuildStatus.queued,\n      scheduled: BuildStatus.queued\n    }\n  }\n\n  fetchBuildStatus () {\n    return this.circleci.getBranchBuilds({ username: this.user,\n      project: this.repo,\n      branch: this.branch,\n      limit: 1\n    })\n      .then((builds) => {\n        if (builds.length < 1) {\n          throw new Error('No builds found')\n        }\n\n        const latestBuild = builds[0]\n        return this.mappings[latestBuild.status] || BuildStatus.unknown\n      })\n  }\n}\n\nmodule.exports = CircleCIEngine\n"
  },
  {
    "path": "packages/widget-ci/src/engines/factory.js",
    "content": "const Travis = require('./travis')\nconst CircleCI = require('./circleci')\n\nclass Factory {\n  constructor () {\n    this.engines = {\n      travis: Travis,\n      circleci: CircleCI\n    }\n  }\n\n  getEngine (engine) {\n    return this.engines[engine]\n  }\n\n  get availableEngines () {\n    return Object.keys(this.engines)\n  }\n}\n\nmodule.exports = new Factory()\n"
  },
  {
    "path": "packages/widget-ci/src/engines/travis/index.js",
    "content": "'use strict'\n\nconst Travis = require('travis-ci')\nconst Promise = require('bluebird').Promise\nconst BuildStatus = require('../../build-status.enum')\n\nclass TravisEngine {\n  constructor (options) {\n    this.travis = new Travis({\n      version: '2.0.0'\n    })\n    this.user = options.user\n    this.repo = options.repo\n    this.branch = options.branch\n    this.mappings = {\n      started: BuildStatus.running,\n      created: BuildStatus.queued,\n      passed: BuildStatus.passed,\n      failed: BuildStatus.failed\n    }\n  }\n\n  fetchBuildStatus () {\n    return new Promise((resolve, reject) => {\n      this.travis.repos(this.user, this.repo).builds().get((err, res) => {\n        if (err) { throw err }\n        if (!res.builds || !res.builds.length) {\n          reject(new Error('No builds found'))\n        }\n\n        const latestBuild = res.builds[0]\n        resolve(this.mappings[latestBuild.state] || BuildStatus.unknown)\n      })\n    })\n  }\n}\n\nmodule.exports = TravisEngine\n"
  },
  {
    "path": "packages/widget-ci/src/engines/travis/travis.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst sinon = require('sinon')\n\nconst BuildStatus = require('../../build-status.enum')\n\ndescribe('engines.travis', () => {\n  const Engine = require('.')\n\n  it('Expects state to be returned', () => {\n    const engine = new Engine({})\n\n    engine.travis = {\n      repos: sinon.stub().returns({\n        builds: sinon.stub().returns({\n          get: sinon.stub().yields(null, { builds: [ { state: 'passed' } ] })\n        })\n      })\n    }\n\n    return engine.fetchBuildStatus()\n      .then((result) => {\n        expect(result).to.equal(BuildStatus.passed)\n      })\n  })\n})\n"
  },
  {
    "path": "packages/widget-ci/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-ci/src/server/validation.js",
    "content": "'use strict'\n\nconst engineFactory = require('../engines/factory')\nconst Joi = require('joi')\n\nmodule.exports = {\n  repo: Joi.string().required().description('Repository Name'),\n  user: Joi.string().required().description('Account/Organisation Name'),\n  branch: Joi.string().optional().description('Branch name'),\n  schedule: Joi.number().optional().default(60000).description('Update frequency (ms)'),\n  provider: Joi.string().required().only(engineFactory.availableEngines).description('CI Provider name'),\n  hideOwner: Joi.boolean().optional().default(false).description('Hide repo owner from display'),\n  sounds: Joi.object({\n    passed: Joi.string().optional().description('Sound to play on build pass'),\n    failed: Joi.string().optional().description('Sound to play on build fail'),\n    unknown: Joi.string().optional().description('Sound to play on unknown state')\n  }).optional().description('Sounds to play when build status changes'),\n  options: Joi.when('provider', {\n    is: 'circleci',\n    then: Joi.object({\n      auth: Joi.string().required().description('CircleCI auth token')\n    }).required(),\n    otherwise: Joi.forbidden()\n  })\n}\n"
  },
  {
    "path": "packages/widget-ci/src/server/widget.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst Hoek = require('hoek')\nconst engineFactory = require('../engines/factory')\nconst validation = require('./validation')\n\nclass CiWidget {\n  constructor (config, emitter) {\n    const { error, value: options } = Joi.validate(config, validation)\n\n    if (error) {\n      throw new Error(`Could not load CI widget, ${error.message}`)\n    }\n\n    this.emitter = emitter\n    this.config = Object.assign({ branch: 'master' }, options)\n\n    const Provider = engineFactory.getEngine(this.config.provider)\n    this.provider = new Provider(this.config)\n\n    this.timer = setInterval(function () {\n      this.run()\n    }.bind(this), this.config.schedule)\n    this.run()\n  }\n\n  run () {\n    return this.provider\n      .fetchBuildStatus()\n      .then((status) => {\n        const sound = Hoek.reach(this.config, `sounds.${status}`)\n        if (sound && this.previousState !== status) {\n          this.emitter.emit('plugin', 'audio:play', { data: sound })\n        }\n\n        this.previousState = status\n        this.emitter.emit('update', { status })\n      })\n  }\n\n  destroy () {\n    clearInterval(this.timer)\n  }\n}\n\nexports.register = function (options, emitter) {\n  return new CiWidget(options, emitter)\n}\n"
  },
  {
    "path": "packages/widget-ci/src/server/widget.spec.js",
    "content": "'use strict'\n\nconst { register } = require('.')\nconst Travis = require('../engines/travis')\nconst BuildStatus = require('../build-status.enum')\nconst engineFactory = require('../engines/factory')\nconst { expect } = require('code')\nconst sinon = require('sinon')\n\ndescribe('widget-ci/server', () => {\n  context('branch configuration', () => {\n    it('defaults to master branch', () => {\n      const config = { provider: 'travis', user: 'x', repo: 'y' }\n      const configuration = register(config)\n      expect(configuration.config.branch).to.equal('master')\n    })\n\n    it('can override branch', () => {\n      const config = { provider: 'travis', user: 'x', repo: 'y', branch: 'feature/xyz' }\n      const configuration = register(config)\n      expect(configuration.config.branch).to.equal('feature/xyz')\n    })\n  })\n\n  context('show owner', () => {\n    it('do not show repo owner', () => {\n      const config = { provider: 'travis', user: 'x', repo: 'y', hideOwner: true }\n      const configuration = register(config)\n      expect(configuration.config.hideOwner).to.be.true()\n    })\n\n    it('show repo owner', () => {\n      const config = { provider: 'travis', user: 'x', repo: 'y' }\n      const configuration = register(config)\n      expect(configuration.config.hideOwner).to.be.false()\n    })\n  })\n\n  context('provider is travis', () => {\n    it('No config for travis', () => {\n      const config = { provider: 'travis', user: 'x', repo: 'y' }\n      const configuration = register(config)\n      expect(configuration.config.provider).to.equal(config.provider)\n    })\n  })\n\n  context('provider is circleci', () => {\n    it('Auth for circleci', () => {\n      const config = { provider: 'circleci', user: 'x', repo: 'y', options: { auth: 'aaa' } }\n      const configuration = register(config)\n      expect(configuration.config.provider).to.equal(config.provider)\n    })\n  })\n\n  context.skip('Sound configuration', () => {\n    let instance\n    let sandbox\n    let emitStub\n\n    before(() => {\n      sandbox = sinon.sandbox.create()\n      const travisStub = sinon.createStubInstance(Travis)\n      travisStub.fetchBuildStatus.resolves(BuildStatus.passed)\n      const TravisClassStub = sandbox.stub(Travis.prototype, 'constructor').returns(travisStub)\n      sandbox.stub(engineFactory, 'getEngine').returns(TravisClassStub)\n      emitStub = sandbox.stub()\n\n      instance = register({\n        provider: 'travis',\n        user: 'x',\n        repo: 'y',\n        sounds: {\n          passed: 'recovery-sound'\n        }\n      }, emitStub)\n\n      return instance.update()\n    })\n\n    after(() => {\n      sandbox.restore()\n    })\n\n    it('Calls stub', () => {\n      expect(emitStub.callCount).to.equal(1)\n    })\n\n    it('Emits sound event', () => {\n      expect(emitStub.firstCall.args[0]).to.equal('audio:play')\n    })\n\n    it('Delivers sound payload', () => {\n      expect(emitStub.firstCall.args[1]).to.equal({ data: 'recovery-sound' })\n    })\n\n    it('Sound only plays on state change', () => {\n      return instance.update()\n        .then(() => {\n          expect(emitStub.callCount).to.equal(1)\n        })\n    })\n\n    const scenarios = [\n      { scenario: 'all specified', sounds: { passed: 'x', failed: 'y', unknown: 'z' } },\n      { scenario: 'passed specified', sounds: { passed: 'x' } },\n      { scenario: 'failed specified', sounds: { failed: 'y' } },\n      { scenario: 'unknown specified', sounds: { unknown: 'z' } }\n    ]\n\n    scenarios.forEach(({ scenario, sounds }) => {\n      it(`Sound config ${scenario}`, () => {\n        const config = { provider: 'travis', user: 'x', repo: 'y', sounds }\n        const configuration = register(config)\n        expect(configuration.config.provider).to.equal(config.provider)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-gauge/.gitignore",
    "content": "node_modules\n*.log\n"
  },
  {
    "path": "packages/widget-gauge/README.MD",
    "content": "# Vudash Gauge Widget\n\nDisplays a gauge, like a vu-meter, on a [Vudash](https://npmjs.org/vudash) Dashboard\n\nDocumentation has moved [to github](https://vudash.com#gauge-widget)"
  },
  {
    "path": "packages/widget-gauge/package.json",
    "content": "{\n  \"name\": \"vudash-widget-gauge\",\n  \"version\": \"9.9.0\",\n  \"main\": \"./src/server\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"vudash\": {\n    \"component\": \"./src/client/markup.html\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.7\",\n    \"hoek\": \"^4.1.0\",\n    \"joi\": \"^13.1.2\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-gauge/src/client/markup.html",
    "content": "<div class=\"widget-gauge\">\n  <div class=\"wrapper\">\n    <svg width=\"54%\" viewBox=\"0 0 {{dimension}} {{dimension}}\">\n      <circle r=\"{{radius}}\" id=\"mask\" \n              stroke-dasharray=\"{{semi}},{{cf}}\"\n              stroke-width=\"{{maskStroke}}\" \n              class=\"circle\" cx=\"50%\" cy=\"50%\">\n      </circle>\n      <circle r=\"{{radius}}\" id=\"outline-curves\" \n              stroke-dasharray=\"{{semi}},{{cf}}\"\n              stroke-width=\"{{maskStroke}}\" \n              class=\"circle outline\" cx=\"50%\" cy=\"50%\">\n      </circle>\n      <circle r=\"{{radius}}\" id=\"high\" \n              stroke-dasharray=\"{{max}},{{cf}}\" \n              stroke-width=\"{{rangeStroke}}\"\n              class=\"circle range\" cx=\"50%\" cy=\"50%\" \n              stroke=\"#E04644\">\n      </circle>\n      <circle r=\"{{radius}}\" id=\"avg\" \n              stroke-dasharray=\"{{current}},{{cf}}\" \n              stroke-width=\"{{rangeStroke}}\"\n              class=\"circle range\" cx=\"50%\" cy=\"50%\" \n              stroke=\"#50D050\">\n      </circle>\n      <circle r=\"{{radius}}\" id=\"outline-ends\" \n              stroke-dasharray=\"2,{{ends}}\" \n              stroke-width=\"{{maskStroke}}\" \n              class=\"circle outline\" cx=\"50%\" cy=\"50%\">\n      </circle>\n    </svg>\n  </div>\n  <div class=\"label\">\n    {{ config.description }}\n  </div>\n</div>\n\n<style>\n  .label {\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n    font-weight: bold;\n  }\n\t\n\t.wrapper {\n\t\tposition: relative;\n\t\tmargin: auto;\n\t\ttop: 16%;\n\t}\n\t\n\tsvg {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttransform: matrix(-1, 0, 0, 1, 0, 0) rotateX(180deg);\n\t}\n\t\n\t.circle {\n\t\tfill: none;\n\t}\n\t\n\t.outline, #mask {\n\t\tstroke: #F1F1F1;\n\t}\n</style>\n\n<script>\n  'use strict'\n\n  export default {\n\t\tdata () {\n\t\t\treturn {\n        config: {\n          maximum: 100\n        },\n        highest: 0,\n        value: 0,\n\t\t\t\tradius: 100\n\t\t\t}\n\t\t},\n\t\t\n\t\tcomputed: {\n\t\t\tdimension: radius => (radius * 2) + 100,\n\t\t\tcf: radius => 2 * Math.PI * radius,\n\t\t\tsemi: cf => cf / 2,\n\t\t\tcurrent: (semi, config, value) => semi / config.maximum * value,\n\t\t\tmax: (semi, config, highest) => semi / config.maximum * highest,\n\t\t\tends: semi => semi - 2,\n\t\t\tmaskStroke: radius => radius / 5 + 10,\n\t\t\trangeStroke: maskStroke => maskStroke - 5\n\t\t},\n\t\t\n    methods: {\n      update ({ data, meta, history }) {\n        const { value } = data\n        const highest = history.reduce((curr, h) => {\n          return h.data.value > curr ? h.data.value : curr\n        }, value)\n        this.set({ value, highest })\n      }\n    }\n  }\n</script>"
  },
  {
    "path": "packages/widget-gauge/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-gauge/src/server/widget.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst { applyToDefaults } = require('hoek')\n\nconst defaults = {\n  maximum: 100\n}\n\nclass GaugeWidget {\n  constructor (options) {\n    this.config = applyToDefaults(defaults, options, false)\n  }\n\n  update (value) {\n    const config = this.config\n    return { value, config }\n  }\n}\n\nexports.validation = Joi.object({\n  maximum: Joi.number().required().min(0).description('Maximum value')\n})\n\nexports.register = function (options) {\n  return new GaugeWidget(options)\n}\n"
  },
  {
    "path": "packages/widget-gauge/src/server/widget.spec.js",
    "content": "'use strict'\n\nconst { register, validation } = require('.')\nconst { expect } = require('code')\nconst Joi = require('joi')\n\ndescribe('widget-gauge/widget', () => {\n  context('Maximum', () => {\n    it('is passed', () => {\n      const config = { maximum: 46 }\n      const widget = register(config)\n      expect(widget.config.maximum).to.equal(config.maximum)\n    })\n  })\n\n  describe('Validation', () => {\n    context('Maximum', () => {\n      it('is valid', () => {\n        const config = { maximum: 46 }\n        const { value } = Joi.validate(config, validation)\n        expect(value.maximum).to.equal(config.maximum)\n      })\n\n      it('is a number', () => {\n        const config = { maximum: 'abc' }\n        const { error } = Joi.validate(config, validation)\n        expect(error.message).to.include('\"maximum\" must be a number')\n      })\n\n      it('is required', () => {\n        const config = {}\n        const { error } = Joi.validate(config, validation)\n        expect(error.message).to.include('\"maximum\" is required')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-health/README.MD",
    "content": "# Vudash Health Widget\n\nDisplays [Vudash](https://npmjs.org/vudash) Dashboard health\n\nDocumentation has moved [to github](https://vudash.com#health-widget)"
  },
  {
    "path": "packages/widget-health/package.json",
    "content": "{\n  \"name\": \"vudash-widget-health\",\n  \"main\": \"./src/server\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"vudash\": {\n    \"component\": \"./src/client/component.html\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"dashboard\",\n    \"dashing\",\n    \"health\",\n    \"monitoring\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"version\": \"9.9.0\",\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.7\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-health/src/client/component.html",
    "content": "<div class=\"vudash-health\">\n  <div class=\"value\">\n    <img alt=\"heart {{ heart }}\" ref:heartbeat src=\"{{ heart }}\" />\n  </div>\n  <div class=\"label\">\n    Health\n  </div>\n</div>\n\n<style>\n  .vudash-health > * {\n    text-align: center;\n    width: 100%;\n  }\n\n  .label {\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n    font-weight: bold;\n  }\n\n  .value {\n    height: 80px;\n    display: block;\n  }\n\n  .value > img {\n    transition: width 0.5s, height 0.5s;\n    color: rgb(255,105,94);\n  }\n</style>\n\n<script>\n  import heart from '../images/heart.svg'\n\n  export default {\n    data () {\n      return {\n        heart: heart,\n        size: '92'\n      }\n    },\n\n    methods: {\n      update ({ meta, data }) {\n        this.set({ size: data.on ? '92' : '56' })\n        this.refs.heartbeat.width = this.get('size')\n        this.refs.heartbeat.height = this.get('size')\n      }\n    }\n  }\n</script>"
  },
  {
    "path": "packages/widget-health/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-health/src/server/widget.js",
    "content": "'use strict'\n\nclass HealthWidget {\n  constructor (options, emitter) {\n    this.emitter = emitter\n    this.on = false\n\n    this.timer = setInterval(function () {\n      this.run()\n    }.bind(this), options.schedule || 1000)\n    this.run()\n  }\n\n  run () {\n    this.on = !this.on\n    this.emitter.emit('update', { on: this.on })\n  }\n\n  destroy () {\n    clearInterval(this.timer)\n  }\n}\n\nexports.register = function (options, emitter) {\n  return new HealthWidget(options, emitter)\n}\n"
  },
  {
    "path": "packages/widget-progress/README.MD",
    "content": "# Vudash Progress Widget\n\nDisplays a progress bar widget on a [Vudash](https://npmjs.org/vudash) dashboard\n\nDocumentation has moved [to github](https://vudash.com#gauge-widget)"
  },
  {
    "path": "packages/widget-progress/package.json",
    "content": "{\n  \"name\": \"vudash-widget-progress\",\n  \"main\": \"./src/server\",\n  \"version\": \"9.8.0\",\n  \"description\": \"Vudash Progress Widget\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"widget\",\n    \"progress\",\n    \"progress bar\",\n    \"completion\",\n    \"percentage\"\n  ],\n  \"vudash\": {\n    \"component\": \"./src/client/component.html\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-progress/src/client/component.html",
    "content": "<div class=\"vudash-progress\">\n   <div class=\"bar\">\n     <div ref:progress class=\"progress\">\n     </div>\n   </div>\n   <div class=\"label\">{{ config.description }}</div>\n</div>\n<style>\n  .bar {\n    width: 60%;\n    border: 4px solid rgb(255,255,255);\n    height: 40px;\n  }\n\n  .bar > .progress {\n    background-color: rgb(112,179,125);\n    display: block;\n    height: 100%;\n    transition: width 1s\n  }\n\n  .label {\n    text-align: center;\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n    font-weight: bold;\n    width: 100%;\n  }\n</style>\n<script>\n  export default {\n    data () {\n      return {\n        config: {\n          description: 'Progress'\n        }\n      }\n    },\n\n    methods: {\n      update ({ data, meta }) {\n        this.set(data)\n        const progress = this.get('percentage')\n        this.refs.progress.style.width = `${progress}%`\n      }\n    }\n  }\n</script>"
  },
  {
    "path": "packages/widget-progress/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-progress/src/server/widget.js",
    "content": "'use strict'\n\nclass ProgressWidget {\n  constructor (options) {\n    this.config = options\n  }\n\n  update (data) {\n    let percentage = data\n    if (data < 0) { percentage = 0 }\n    if (data > 100) { percentage = 100 }\n    return { percentage }\n  }\n}\n\nexports.register = function (options) {\n  return new ProgressWidget(options)\n}\n"
  },
  {
    "path": "packages/widget-statistic/.gitignore",
    "content": "node_modules\n*.log\n"
  },
  {
    "path": "packages/widget-statistic/README.MD",
    "content": "# Vudash Statistic Widget\n\nDisplays a statistic, such as a visitor count, on a [Vudash](https://npmjs.org/vudash) Dashboard\n\n![stats widget](https://cloud.githubusercontent.com/assets/218949/20489789/adb964ca-b003-11e6-917b-c07218625bd3.png)\n\nDocumentation has moved [to github](https://vudash.com#statistic-widget)"
  },
  {
    "path": "packages/widget-statistic/package.json",
    "content": "{\n  \"name\": \"vudash-widget-statistic\",\n  \"version\": \"9.9.0\",\n  \"main\": \"./src/server\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"dependencies\": {\n    \"chartist\": \"^0.11.0\",\n    \"fit-text\": \"^2.0.0\",\n    \"joi\": \"^13.1.2\",\n    \"sprintf-js\": \"^1.0.3\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"vudash\": {\n    \"component\": \"./src/client/markup.html\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-statistic/src/client/markup.html",
    "content": "<div class=\"vudash-statistic\">\n  <div class=\"value\" ref:value>\n    {{ displayValue }}\n  </div>\n  {{#if config.historyView === 'ticker'}}\n  <div class=\"ticker\">\n    <span class=\"indicator {{ ticker.up ? 'up' : 'down' }}\">\n      {{ ticker.difference }} ({{ ticker.percent }}%)\n    </span>\n  </div>\n  {{/if}}\n  <div class=\"label\">\n    {{ config.description }}\n  </div>\n  {{#if config.historyView === 'chart'}}\n    <div ref:chart class=\"chart\"></div>\n  {{/if}}\n</div>\n\n<style>\n  .vudash-statistic > .label,\n  .vudash-statistic > .value {\n    width: 100%;\n    text-align: center;\n    font-weight: bold;\n  }\n\n  .value, .label {\n    z-index: 1;\n  }\n\n  .value {\n    width: 90%;\n  }\n\n  .label {\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n  }\n\n  .chart {\n    bottom: 0;\n    height: 100%;\n    width: 100%;\n    position: absolute;\n    float: left;\n    z-index: 0;\n  }\n\n  .ticker {\n    font-size: 1.6vw;\n  }\n\n  .indicator.up {\n    color: green;\n  }\n\n  .indicator.up::before {\n    content: '\\25B2 '\n  }\n\n  .indicator.down {\n    color: red;\n  }\n\n  .indicator.down::before {\n    content: '\\25BC '\n  }\n</style>\n\n<script>\n  'use strict'\n\n  import Chartist from 'chartist'\n  import 'chartist/dist/chartist.css'\n  import FitText from 'fit-text'\n\n  const options = {\n    axisX: {\n      showLabel: false,\n      showGrid: false\n    },\n    axisY: {\n      showLabel: false,\n      showGrid: false\n    },\n    showPoint: false,\n    showArea: true,\n    fullWidth: true,\n    width: '100%',\n    chartPadding: { \n      left: -40,\n      right: 0,\n      bottom: -40\n    }\n  }\n\n  const colours = [\n    '#d70206',\n    '#f05b4f',\n    '#f4c63d',\n    '#d17905',\n    '#453d3f',\n    '#59922b',\n    '#0544d3',\n    '#6b0392',\n    '#f05b4f',\n    '#dda458',\n    '#eacf7d',\n    '#86797d',\n    '#b2c326',\n    '#6188e2',\n    '#a748ca'\n  ]\n\n  export default {\n    data () {\n      return {\n        displayValue: '-',\n        ticker: {\n          difference: 0,\n          percent: 0,\n          up: true\n        }\n      }\n    },\n\n    oncreate () {\n      const config = this.get('config')\n      new FitText(this.refs.value, { fontRatio: config['font-ratio'] || 4 })\n      const colour = config.colour || colours[Math.floor(Math.random() * colours.length)]\n      this.set({ colour })\n    },\n\n    methods: {\n      update ({ data, meta, history }) {\n        const { displayValue } = data\n        const config = this.get('config')\n\n        if (history) {\n          if (config.historyView === 'chart') {\n            this.buildChart(history)\n          } else {\n            this.buildTicker(history)\n          }\n        }\n        \n        const updates = { displayValue }\n        this.set(updates)\n      },\n\n      buildTicker (history) {\n        try {\n          const latest = history.pop()\n          const previous = history.pop()\n          const current = Number.parseFloat(latest.data.value)\n          const former = Number.parseFloat(previous.data.value)\n          const difference = (Math.abs(current - former)).toFixed(2)\n          const percent = ((100 / former) * difference).toFixed(2)\n          const up = current >= former\n          this.set({ ticker: { difference, percent, up } })\n        } catch (e) {\n          return\n        }\n      },\n\n      buildChart (history) {\n        const series = history.reduce((curr, item) => {\n          try {\n            curr.push(Number.parseFloat(item.data.value))\n            return curr\n          } catch (e) {\n            return curr\n          }\n        }, [])\n\n        const chartData = { series: [series] }\n        this.chart = this.chart || this.create(chartData)\n        this.chart.update(chartData)\n      },\n\n      create (chartData) {\n        const chart = new Chartist.Line(this.refs.chart, chartData, options)\n        chart.on('created', () => {\n          const colour = this.get('colour')\n          const lineSelector = '.ct-series-a .ct-line'\n          const areaSelector = '.ct-series-a .ct-area'\n\n          const svg = this.refs.chart\n          const line = svg.querySelectorAll(lineSelector)[0]\n          const area = svg.querySelectorAll(areaSelector)[0]\n\n          line.style.stroke = colour\n          if (area) {\n            area.style.fill = colour\n            area.style.opacity = 0.5\n          }\n        })\n        return chart\n      }\n    }\n  }\n</script>"
  },
  {
    "path": "packages/widget-statistic/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-statistic/src/server/widget.js",
    "content": "'use strict'\n\nconst { sprintf } = require('sprintf-js')\nconst Joi = require('joi')\n\nconst defaults = {\n  description: 'Statistics'\n}\n\nfunction format (format, value) {\n  return sprintf(format, value)\n}\n\nclass StatisticWidget {\n  constructor (options) {\n    this.config = Object.assign({}, defaults, options)\n  }\n\n  update (value) {\n    return {\n      value,\n      displayValue: format(this.config.format, value)\n    }\n  }\n}\n\nexports.validation = Joi.object({\n  format: Joi.string().optional().default('%s').description('Display format'),\n  colour: Joi.string().optional().default('#fff').description('Colour'),\n  description: Joi.string().optional().description('Description'),\n  'font-ratio': Joi.number().default(4).description('Font ratio for display value'),\n  historyView: Joi.string().only('chart', 'ticker').default('chart').description('History display format')\n})\n\nexports.register = function (options) {\n  return new StatisticWidget(options)\n}\n"
  },
  {
    "path": "packages/widget-statistic/src/server/widget.spec.js",
    "content": "'use strict'\n\nconst { expect } = require('code')\nconst { register, validation } = require('./widget')\nconst Joi = require('joi')\n\ndescribe('widget', () => {\n  context('#register()', () => {\n    it('Uses config', () => {\n      const config = { foo: 'bar' }\n      const widget = register(config, validation)\n      expect(widget.config.foo).to.equal(config.foo)\n    })\n\n    it('Has default config values', () => {\n      const config = {}\n      const widget = register(config)\n      expect(widget.config.description).to.equal('Statistics')\n    })\n\n    it('Merges config with default values', () => {\n      const config = { description: 'hello' }\n      const widget = register(config)\n      expect(widget.config.description).to.equal(config.description)\n    })\n  })\n\n  context('Updates', () => {\n    let output\n\n    beforeEach(() => {\n      const configuration = register({ format: '%d%%' })\n      output = configuration.update(34)\n    })\n\n    it('Will convert given value to string', () => {\n      const widget = register({ format: '%s' })\n      const { displayValue } = widget.update(2)\n      expect(displayValue).to.equal('2')\n    })\n\n    it('Will format according to format config', () => {\n      expect(output.displayValue).to.equal('34%')\n    })\n\n    it('Will retain original value for history', () => {\n      expect(output.value).to.equal(34)\n    })\n  })\n\n  context('Configuration', () => {\n    it('With provided colour', () => {\n      const conf = { colour: '#f00' }\n      const config = Joi.attempt(conf, validation)\n      expect(config.colour).to.equal('#f00')\n    })\n\n    it('No colour passed', () => {\n      const config = Joi.attempt({}, validation)\n      expect(config.colour).to.equal('#fff')\n    })\n\n    it('default format is set', () => {\n      const config = Joi.attempt({}, validation)\n      expect(config.format).to.equal('%s')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-status/README.MD",
    "content": "# Vudash Status Widget\n\nDisplays status for APIs, such as a Statuspage IO status page, on a [Vudash](https://npmjs.org/vudash) Dashboard\n\nDocumentation has moved [to github](https://vudash.com#status-widget)"
  },
  {
    "path": "packages/widget-status/package.json",
    "content": "{\n  \"name\": \"vudash-widget-status\",\n  \"main\": \"./src/server\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"vudash\": {\n    \"component\": \"./src/client/markup.html\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"statuspage\",\n    \"status\",\n    \"monitoring\",\n    \"github\",\n    \"api\",\n    \"widget\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"^3.4.7\",\n    \"export-dir\": \"^0.1.2\",\n    \"got\": \"^6.7.1\",\n    \"hoek\": \"^4.1.0\",\n    \"joi\": \"^13.0.1\",\n    \"moment\": \"^2.18.1\"\n  },\n  \"version\": \"9.9.0\",\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-status/src/client/markup.html",
    "content": "<div class=\"vudash-status\">\n  <span class=\"updated\">{{ when }}</span>\n  <ul>\n    {{#each components as cmp}}\n    <li class=\"{{ cmp.ligature }}\">{{ cmp.name }}</li>\n    {{/each}}\n  </ul>\n  <div class=\"label\">\n    {{ description }}\n  </div>\n</div>\n\n<style>\n  .vudash-status > * {\n    width: 100%;\n    text-align: center;\n  }\n\n  .updated {\n    position: absolute;\n    top: 1vw;\n    right: 1vw;\n    text-align: right;\n    font-size: 0.8vw;\n    color: rgb(128,128,128);\n  }\n\n  .label {\n    position: absolute;\n    bottom: 30px;\n    font-size: 14px;\n    font-weight: bold;\n  }\n\t\n\tul {\n    padding: 0;\n\t}\n\t\n\tul li {\n    margin: 0 4%;\n    list-style-type: none;\n    text-align: left;\n\t\tfont-size: 1.2vw;\n\t}\n\t\n\tul li::before {\n\t\tmargin: 0 10px 0 0;\n\t}\n\t\n\tul li.good::before {\n\t\tcolor: green;\n\t\tcontent: '\\25B2 ';\n\t}\n\t\n\tul li.minor::before {\n\t\tcolor: yellow;\n\t\tcontent: '\\25B2 ';\n\t}\n\t\n\tul li.major::before {\n\t\tcontent: '\\25BC ';\n\t\tanimation: failed-pulse 4s infinite alternate;\n\t}\n\t\n\tul li.waiting::before {\n\t\tcolor: grey;\n\t\tmargin: 0 10px 0 4px;\n\t\tcontent: '\\25CF ';\n\t}\n\t\n\t @keyframes failed-pulse {\n    0% { color: rgb(159,20,255); }\n    25% { color: rgb(232,18,205) }\n    50% { color: rgb(255,32,33); }\n    75% { color: rgb(232,83,32); }\n    100% { color: rgb(255,139,18); }\n  }\n</style>\n\n<script>\n  'use strict'\n  import moment from 'moment'\n\n  export default {\n    data () {\n      return {\n        description: '-',\n        components: {}\n      }\n    },\n\n    methods: {\n      update ({ meta, data }) {\n        this.set(data)\n      }\n    },\n\n    computed: {\n      when: _updated => {\n        return moment(_updated).fromNow(true)\n      }\n    }\n  }\n</script>"
  },
  {
    "path": "packages/widget-status/src/health-status.js",
    "content": "'use strict'\n\nmodule.exports = {\n  HEALTHY: { ligature: 'good' },\n  PARTIAL_OUTAGE: { ligature: 'minor' },\n  MAJOR_OUTAGE: { ligature: 'major' },\n  DEGRADED: { ligature: 'minor' },\n  UNKNOWN: { ligature: 'waiting' }\n}\n"
  },
  {
    "path": "packages/widget-status/src/providers/github/github.spec.js",
    "content": "'use strict'\n\nconst Provider = require('.')\nconst HealthStatus = require('../../health-status')\nconst nock = require('nock')\nconst { expect } = require('code')\n\ndescribe('providers.github', () => {\n  context('#configValidation', () => {\n    it('Returns an empty block', () => {\n      expect(Provider.configValidation).to.equal({})\n    })\n  })\n\n  context('#fetch()', () => {\n    const scenarios = [\n      { status: 'good', health: HealthStatus.HEALTHY },\n      { status: 'minor', health: HealthStatus.PARTIAL_OUTAGE },\n      { status: 'major', health: HealthStatus.MAJOR_OUTAGE },\n      { status: 'xyz', health: HealthStatus.UNKNOWN }\n    ]\n\n    afterEach(() => {\n      nock.cleanAll()\n    })\n\n    scenarios.forEach(({ status, health }) => {\n      it(`Returns ${health} when status is ${status}`, () => {\n        nock('https://status.github.com')\n          .get('/api/status.json')\n          .reply(200, { status })\n\n        const provider = new Provider()\n        return provider.fetch()\n          .then((output) => {\n            expect(output.description).to.equal('Github')\n            expect(output.components[0].ligature).to.equal(health)\n            expect(output.components[0].name).to.equal('github')\n          })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-status/src/providers/github/index.js",
    "content": "'use strict'\n\nconst got = require('got')\nconst HealthStatus = require('../../health-status')\nconst { reach } = require('hoek')\n\nclass Github {\n  static get configValidation () {\n    return {}\n  }\n\n  mapHealth (body) {\n    const mappings = {\n      good: HealthStatus.HEALTHY,\n      major: HealthStatus.MAJOR_OUTAGE,\n      minor: HealthStatus.PARTIAL_OUTAGE\n    }\n\n    const status = reach(body, 'status')\n    return mappings[status] || HealthStatus.UNKNOWN\n  }\n\n  fetch () {\n    return got('https://status.github.com/api/status.json', {\n      json: true\n    })\n      .then((response) => {\n        const body = response.body\n        return {\n          components: [\n            { name: 'github', ligature: this.mapHealth(body) }\n          ],\n          description: 'Github'\n        }\n      })\n  }\n}\n\nmodule.exports = Github\n"
  },
  {
    "path": "packages/widget-status/src/providers/index.js",
    "content": "'use strict'\n\nconst exportDir = require('export-dir')\nmodule.exports = exportDir(__dirname)\n"
  },
  {
    "path": "packages/widget-status/src/providers/statuspageio/index.js",
    "content": "'use strict'\n\nconst got = require('got')\nconst HealthStatus = require('../../health-status')\nconst { reach } = require('hoek')\nconst { assign } = Object\nconst Joi = require('joi')\n\nfunction mapHealthStatus (status) {\n  const mappings = {\n    'operational': HealthStatus.HEALTHY,\n    'partial_outage': HealthStatus.PARTIAL_OUTAGE,\n    'major_outage': HealthStatus.MAJOR_OUTAGE,\n    'degraded_performance': HealthStatus.DEGRADED\n  }\n  return mappings[status] || HealthStatus.UNKNOWN\n}\n\nclass StatuspageIo {\n  constructor (config) {\n    this.url = config.url\n    this.selectedComponents = config.components\n  }\n\n  static get configValidation () {\n    return {\n      url: Joi.string().uri().required().description('Status page url'),\n      components: Joi.array().items(Joi.string()).required().description('Component names to monitor')\n    }\n  }\n\n  filterComponentList (all, component) {\n    if (this.selectedComponents.includes(component.name)) {\n      const styles = mapHealthStatus(component.status)\n      const state = assign({ name: component.name }, styles)\n      all.push(state)\n    }\n    return all\n  }\n\n  fetch () {\n    return got(this.url, {\n      json: true\n    })\n      .then((response) => {\n        const body = response.body\n        const filteredComponents = body.components.reduce(this.filterComponentList.bind(this), [])\n\n        return {\n          description: reach(body, 'page.name'),\n          components: filteredComponents\n        }\n      })\n  }\n}\n\nmodule.exports = StatuspageIo\n"
  },
  {
    "path": "packages/widget-status/src/providers/statuspageio/statuspageio.spec.js",
    "content": "'use strict'\n\nconst Provider = require('.')\nconst HealthStatus = require('../../health-status')\nconst nock = require('nock')\nconst { assign } = Object\nconst Joi = require('joi')\nconst { expect } = require('code')\n\ndescribe('providers.statuspageio', () => {\n  context('#configValidation', () => {\n    it('Requires component list', () => {\n      Joi.validate({ url: 'http://www.example.com' }, Provider.configValidation, (err) => {\n        expect(err).to.exist()\n      })\n    })\n\n    it('Requires statuspage url', () => {\n      Joi.validate({ components: [] }, Provider.configValidation, (err) => {\n        expect(err).to.exist()\n      })\n    })\n\n    it('Statuspage url must be an url', () => {\n      Joi.validate({ url: 'xxx', components: [] }, Provider.configValidation, (err) => {\n        expect(err).to.exist()\n      })\n    })\n  })\n\n  context('#fetch()', () => {\n    const url = 'http://example.org/'\n    const statuspageJson = {\n      page: {\n        name: 'Some Page'\n      },\n      status: {\n        indicator: 'none'\n      },\n      components: [\n        { name: 'status', indicator: 'none' },\n        { name: 'Component A', status: 'operational' },\n        { name: 'Component B', status: 'major_outage' },\n        { name: 'Component C', status: 'partial_outage' },\n        { name: 'Component D', status: 'degraded_performance' },\n        { name: 'Component E', status: 'xxx' },\n        { name: 'Component F', status: 'operational' }\n      ]\n    }\n    const config = {\n      url,\n      components: [\n        'Component A',\n        'Component B',\n        'Component C',\n        'Component D',\n        'Component E'\n      ]\n    }\n    let results\n\n    beforeEach(() => {\n      nock(url, {\n        reqheaders: {\n          accept: 'application/json'\n        }\n      })\n        .get('/')\n        .reply(200, statuspageJson)\n\n      const provider = new Provider(config)\n      return provider.fetch()\n        .then((output) => {\n          results = output\n        })\n    })\n\n    it('Has status page name', () => {\n      expect(results.description).to.equal('Some Page')\n    })\n\n    it('Fetches components', () => {\n      expect(results.components).to.equal([\n        assign({ name: 'Component A' }, HealthStatus.HEALTHY),\n        assign({ name: 'Component B' }, HealthStatus.MAJOR_OUTAGE),\n        assign({ name: 'Component C' }, HealthStatus.PARTIAL_OUTAGE),\n        assign({ name: 'Component D' }, HealthStatus.DEGRADED),\n        assign({ name: 'Component E' }, HealthStatus.UNKNOWN)\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-status/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-status/src/server/validator.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst providers = require('../providers')\n\nfunction getBasicValidation () {\n  const availableProviders = Object.keys(providers)\n\n  return {\n    type: Joi.string().required().only(availableProviders).description('Status page type'),\n    schedule: Joi.number().optional().default(60000).description('CI Refresh schedule'),\n    config: Joi.object().optional().default({}).description('Provider configuration')\n  }\n}\n\nfunction validate (schema, config) {\n  const { error, value } = Joi.validate(config, schema)\n\n  if (error) {\n    throw new Error(`Unable to configure status widget: ${error.message}`)\n  }\n\n  return value\n}\n\nexports.validateConfig = function (options) {\n  return validate(getBasicValidation(), options)\n}\n\nexports.validateProvider = function (ProviderClass, config) {\n  const { configValidation } = ProviderClass\n  return validate(configValidation, config)\n}\n"
  },
  {
    "path": "packages/widget-status/src/server/widget.js",
    "content": "'use strict'\n\nconst providers = require('../providers')\nconst { validateConfig, validateProvider } = require('./validator')\n\nclass StatusWidget {\n  constructor (options, emitter) {\n    this.emitter = emitter\n\n    const { type, schedule, config } = validateConfig(options)\n    const ProviderClass = providers[type]\n\n    const providerConfig = validateProvider(ProviderClass, config)\n    this.provider = new ProviderClass(providerConfig)\n\n    this.timer = setInterval(function () {\n      this.run()\n    }.bind(this), schedule)\n    this.run()\n  }\n\n  run () {\n    return this\n      .provider\n      .fetch()\n      .then(data => {\n        this.emitter.emit('update', data)\n      })\n  }\n\n  destroy () {\n    clearInterval(this.timer)\n  }\n}\n\nexports.register = function (options, emitter) {\n  return new StatusWidget(options, emitter)\n}\n"
  },
  {
    "path": "packages/widget-time/.gitignore",
    "content": "node_modules\n*.log\n"
  },
  {
    "path": "packages/widget-time/README.md",
    "content": "# Vudash Time Widget\nShows the current time and date in your [Vudash](https://npmjs.org/vudash) Dashboard.\n\nAlso plays custom sounds as alarms.\n\nDocumentation has moved [to vudash.com](https://vudash.com#time-widget)"
  },
  {
    "path": "packages/widget-time/package.json",
    "content": "{\n  \"name\": \"vudash-widget-time\",\n  \"version\": \"9.9.0\",\n  \"description\": \"Vudash time widget\",\n  \"main\": \"./src/server\",\n  \"scripts\": {\n    \"test\": \"NODE_PATH=src:test ../../node_modules/.bin/mocha ../../test/unit.lab.js\",\n    \"link\": \"npm link\",\n    \"lint\": \"../../node_modules/.bin/standard\"\n  },\n  \"keywords\": [\n    \"vudash\",\n    \"dashboard\",\n    \"dashing\",\n    \"time\",\n    \"alarm\",\n    \"cron\",\n    \"schedule\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vudash/vudash\"\n  },\n  \"author\": \"Antony Jones\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"bluebird\": \"^3.5.1\",\n    \"cron\": \"^1.3.0\",\n    \"hoek\": \"^4.2.1\",\n    \"joi\": \"^9.2.0\",\n    \"moment\": \"^2.19.1\",\n    \"moment-timezone\": \"^0.5.13\"\n  },\n  \"vudash\": {\n    \"component\": \"./src/client/markup.html\"\n  },\n  \"standard\": {\n    \"globals\": [\n      \"describe\",\n      \"context\",\n      \"it\",\n      \"before\",\n      \"after\",\n      \"beforeEach\",\n      \"afterEach\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/widget-time/src/client/markup.html",
    "content": "<div class=\"vudash-time\">\n  <div class=\"value\">\n    {{ time }}\n  </div>\n  <div class=\"label\">\n    {{ date }}\n  </div>\n</div>\n\n<style>\n  .vudash-time > * {\n    text-align: center;\n  }\n\n  .vudash-time > .value {\n    color: rgb(84,200,255);\n    font-size: 76px;\n  }\n\n  .vudash-time > .label {\n    color: rgb(217,231,120);\n    font-size: 36px;\n  }\n</style>\n\n<script>\n  export default {\n  data () {\n    return {\n      time: '--:--:--',\n      date: '--- -- --- ----'\n    }\n  },\n\n  methods: {\n    update ({ meta, data }) {\n      this.set(data)\n    }\n  }\n}\n</script>"
  },
  {
    "path": "packages/widget-time/src/server/alarms.js",
    "content": "'use strict'\n\nconst { reach } = require('hoek')\nconst { CronJob } = require('cron')\n\nexports.parseAlarms = function (config, emitter) {\n  const alarms = reach(config, 'alarms', { default: [] })\n  return alarms.map(alarm => {\n    return alarm.actions.map(action => {\n      const context = {\n        options: action.options,\n        emitter\n      }\n\n      return new CronJob({\n        cronTime: alarm.expression,\n        onTick: function () {\n          this.emitter.emit('plugin', 'audio:play', {\n            data: this.options.data\n          })\n        },\n        context,\n        start: true,\n        timeZone: config.timezone,\n        runOnInit: false\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "packages/widget-time/src/server/index.js",
    "content": "'use strict'\n\nmodule.exports = require('./widget')\n"
  },
  {
    "path": "packages/widget-time/src/server/validation.js",
    "content": "'use strict'\n\nconst Joi = require('joi')\nconst moment = require('moment')\n\nconst action = Joi.object({\n  action: Joi.string().only('sound').description('Action name'),\n  options: Joi.object({\n    data: Joi.string().description('Data uri for sound file')\n  }).description('Action configuration')\n}).description('Action')\n\nconst alarm = Joi.object({\n  expression: Joi.string().description('Cron Expression'),\n  actions: Joi.array().items(action).description('Actions')\n}).required().description('Alarm entry')\n\nexports.schema = Joi.object({\n  timezone: Joi.string().only(moment.tz.names()).optional().default('UTC').description('A momentjs timezone'),\n  alarms: Joi.array().items(alarm).optional().description('List of alarms')\n}).optional()\n"
  },
  {
    "path": "packages/widget-time/src/server/widget.js",
    "content": "'use strict'\n\nconst { time } = require('../time')\nconst { schema } = require('./validation')\nconst { parseAlarms } = require('./alarms')\n\nclass TimeWidget {\n  constructor (options, emitter) {\n    this.config = options\n    this.emitter = emitter\n    this.alarms = parseAlarms(this.config, this.emitter)\n\n    this.timer = setInterval(function () {\n      this.run()\n    }.bind(this), 1000)\n    this.run()\n  }\n\n  run () {\n    const data = time(this.config.timezone)\n    this.emitter.emit('update', data)\n  }\n\n  destroy () {\n    clearInterval(this.timer)\n    this.alarms.forEach(actions => {\n      actions.forEach(action => action.stop())\n    })\n  }\n}\n\nexports.validation = schema\n\nexports.register = function (options, emitter) {\n  return new TimeWidget(options, emitter)\n}\n"
  },
  {
    "path": "packages/widget-time/src/server/widget.spec.js",
    "content": "'use strict'\n\nconst { register } = require('.')\nconst { expect } = require('code')\nconst { stub } = require('sinon')\n\ndescribe('widget', () => {\n  context('Alarms', () => {\n    let widget\n\n    const config = {\n      alarms: [\n        {\n          expression: '* * * * * *',\n          actions: [\n            {\n              action: 'sound',\n              options: {\n                data: 'abcde'\n              }\n            }\n          ]\n        }\n      ],\n      timezone: 'UTC'\n    }\n\n    it('Allows alarm config', () => {\n      expect(() => {\n        widget = register(config, { emit: stub() })\n      }).not.to.throw()\n    })\n\n    afterEach(() => {\n      widget.destroy()\n    })\n  })\n\n  context('No alarms', () => {\n    let widget\n\n    it('Allows config', () => {\n      expect(() => {\n        widget = register({\n          timezone: 'UTC'\n        }, { emit: stub() })\n      }).not.to.throw()\n    })\n\n    afterEach(() => {\n      widget.destroy()\n    })\n  })\n\n  context('#destroy()', () => {\n    let widget\n\n    const action1 = { stop: stub() }\n    const action2 = { stop: stub() }\n\n    beforeEach(() => {\n      widget = register({\n        timezone: 'UTC'\n      }, { emit: stub() })\n      widget.alarms = [\n        [\n          action1,\n          action2\n        ]\n      ]\n    })\n\n    it('calls stop on all actions', () => {\n      widget.destroy()\n      expect(action1.stop.callCount).to.equal(1)\n      expect(action2.stop.callCount).to.equal(1)\n    })\n\n    afterEach(() => {\n      widget.destroy()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/widget-time/src/time/index.js",
    "content": "'use strict'\n\nconst moment = require('moment-timezone')\n\nexports.time = locale => {\n  const now = moment().tz(locale)\n  return {\n    time: now.format('HH:mm:ss'),\n    date: now.format('MMMM Do YYYY')\n  }\n}\n"
  },
  {
    "path": "packages/widget-time/src/time/time.spec.js",
    "content": "'use strict'\n\nconst moment = require('moment-timezone')\nconst service = require('.')\nconst { expect } = require('code')\n\ndescribe('time', () => {\n  it('with UTC locale', async () => {\n    const timezone = 'UTC'\n    const current = moment().tz(timezone).format('HH:mm:ss')\n    const { time } = await service.time(timezone)\n    expect(time).to.equal(current)\n  })\n\n  it('with locale', async () => {\n    const timezone = 'America/Los_Angeles'\n    const current = moment().tz(timezone).format('HH:mm:ss')\n    const { time } = await service.time(timezone)\n    expect(time).to.equal(current)\n  })\n})\n"
  },
  {
    "path": "test/unit.lab.js",
    "content": "'use strict'\n\nconst path = require('path')\nconst glob = require('glob')\n\nrequire('sinon-as-promised')\n\nconst tests = glob.sync(path.join(process.cwd(), './{,!(node_modules)/**/}*.spec.js'))\ntests.forEach(fullPath => require(fullPath))\n"
  }
]