├── .gitignore ├── .travis.yml ├── CONTRIBUTORS.md ├── COPYRIGHT ├── DEPENDENCIES.md ├── LICENSE ├── README.md ├── demo ├── demo-billboard.json ├── demo-gallery-charts.json ├── demo-gallery-presentation-options.json ├── demo-gallery-text-presentations.json ├── demo-overview-demo.json ├── release-notes-what-s-new-in-0-10.json ├── release-notes-what-s-new-in-0-11.json ├── release-notes-what-s-new-in-0-6.json ├── release-notes-what-s-new-in-0-7.json ├── release-notes-what-s-new-in-0-8.json ├── release-notes-what-s-new-in-0-9.json ├── render-bar-charts.json ├── render-donut-charts.json ├── render-legends.json ├── render-singlegraphs.json ├── render-standard-time-series.json ├── test-charts-all.json ├── test-charts-bar-discrete-bar.json ├── test-charts-donut-pie.json ├── test-charts-legends.json ├── test-charts-line-area.json ├── test-charts-singlegraphs.json ├── test-comprehensive.json ├── test-test-cell-styles.json └── test-test-layout.json ├── docs ├── dashboard-items.png ├── dashboard-items.t2d └── screenshots │ └── color-themes-small.png ├── extras └── ExpandLatency.js ├── script ├── bootstrap ├── cibuild ├── console ├── server ├── setup ├── test └── update ├── tessera-frontend ├── .gitignore ├── Gruntfile.js ├── package.json ├── screenshots.js └── src │ ├── 3rd-Party │ ├── css │ │ ├── bootstrap-callouts.css │ │ ├── bootstrap-datetimepicker.css │ │ ├── bootstrap-editable.css │ │ ├── bootstrapValidator.min.css │ │ ├── dataTables.bootstrap.css │ │ ├── font-awesome.css │ │ ├── font-awesome.min.css │ │ ├── highlight-styles │ │ │ └── github.css │ │ ├── jquery.flot.valuelabels.css │ │ ├── select2-bootstrap.css │ │ ├── select2-spinner.gif │ │ ├── select2.css │ │ └── select2.png │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── img │ │ ├── back_disabled.png │ │ ├── back_enabled.png │ │ ├── back_enabled_hover.png │ │ ├── clear.png │ │ ├── forward_disabled.png │ │ ├── forward_enabled.png │ │ ├── forward_enabled_hover.png │ │ ├── loading.gif │ │ ├── sort_asc.png │ │ ├── sort_asc_disabled.png │ │ ├── sort_both.png │ │ ├── sort_desc.png │ │ └── sort_desc_disabled.png │ └── js │ │ ├── bootstrap-datetimepicker.min.js │ │ ├── bootstrap-editable.min.js │ │ ├── bootstrapValidator.min.js │ │ ├── equalize.min.js │ │ ├── flot │ │ ├── jquery.flot.barnumbers.enhanced.js │ │ ├── jquery.flot.crosshair.js │ │ ├── jquery.flot.downsample.js │ │ ├── jquery.flot.multihighlight.js │ │ └── jquery.flot.valuelabels.js │ │ └── tagmanager.js │ ├── css │ ├── bootstrap │ │ ├── dark.scss │ │ ├── light.scss │ │ ├── snow.scss │ │ ├── solarized-dark.scss │ │ └── solarized-light.scss │ ├── fonts.scss │ ├── tessera │ │ ├── _3rd-party.scss │ │ ├── _alerts.scss │ │ ├── _app.scss │ │ ├── _charts.scss │ │ ├── _edit.scss │ │ ├── _typography.scss │ │ ├── dark.scss │ │ ├── light.scss │ │ ├── snow.scss │ │ ├── solarized-dark.scss │ │ └── solarized-light.scss │ └── themes │ │ ├── dark.scss │ │ ├── light.scss │ │ ├── snow.scss │ │ ├── solarized-dark.scss │ │ ├── solarized-light.scss │ │ └── solarized.scss │ ├── js │ └── dependencies.js │ ├── templates │ ├── _action-menu-button.hbs │ ├── _ds-action-menu.hbs │ ├── _ds-dashboard-listing-action-menu.hbs │ ├── _ds-dashboard-listing-entry.hbs │ ├── _ds-dashboard-tag-with-link.hbs │ ├── _ds-dashboard-tag.hbs │ ├── _ds-edit-bar-cell.hbs │ ├── _ds-edit-bar-definition.hbs │ ├── _ds-edit-bar-item-details.hbs │ ├── _ds-edit-bar-item.hbs │ ├── _ds-edit-bar-row.hbs │ ├── _ds-edit-bar-section.hbs │ ├── _ds-edit-bar.hbs │ ├── _ds-edit-menu.hbs │ ├── _ds-preferences-renderer-entry.hbs │ ├── _ds-row-edit-bar.hbs │ ├── _ds-title-bar.hbs │ ├── action-menu-button.hbs │ ├── action.hbs │ ├── action_button.hbs │ ├── edit │ │ ├── _dashboard-metadata-panel.hbs │ │ ├── _dashboard-query-panel.hbs │ │ ├── _dashboard-query-row.hbs │ │ ├── _ds-item-property-sheet.hbs │ │ ├── dashboard_panel.hbs │ │ ├── item_source.hbs │ │ └── view_query.hbs │ ├── flot │ │ ├── discrete_bar_tooltip.hbs │ │ ├── donut_tooltip.hbs │ │ ├── table_legend.hbs │ │ └── tooltip.hbs │ ├── listing │ │ ├── dashboard_list.hbs │ │ └── dashboard_tag_list.hbs │ └── models │ │ ├── bar_chart.hbs │ │ ├── cell.hbs │ │ ├── comparison_summation_table.hbs │ │ ├── comparison_summation_table_body.hbs │ │ ├── definition.hbs │ │ ├── discrete_bar_chart.hbs │ │ ├── donut_chart.hbs │ │ ├── heading.hbs │ │ ├── jumbotron_singlestat.hbs │ │ ├── markdown.hbs │ │ ├── percentage_table.hbs │ │ ├── percentage_table_data.hbs │ │ ├── row.hbs │ │ ├── scatter_plot.hbs │ │ ├── section.hbs │ │ ├── separator.hbs │ │ ├── simple_time_series.hbs │ │ ├── singlegraph.hbs │ │ ├── singlegraph_grid.hbs │ │ ├── singlegraph_grid_item.hbs │ │ ├── singlestat.hbs │ │ ├── singlestat_grid.hbs │ │ ├── singlestat_grid_item.hbs │ │ ├── standard_time_series.hbs │ │ ├── summation_table.hbs │ │ ├── summation_table_row.hbs │ │ ├── timerstat.hbs │ │ ├── timerstat_body.hbs │ │ ├── timeshift_summation_table.hbs │ │ └── timeshift_summation_table_body.hbs │ └── ts │ ├── app │ ├── app.ts │ ├── config.ts │ ├── handlers │ │ ├── check-dirty.ts │ │ ├── dashboard-create.ts │ │ ├── dashboard-toolbar.ts │ │ ├── display-mode.ts │ │ ├── menu-dashboard-actions.ts │ │ ├── menu-dashboard-sort.ts │ │ ├── menu-presentation-actions.ts │ │ ├── menu-refresh.ts │ │ ├── menu-rejigger.ts │ │ ├── menu-theme.ts │ │ └── range-picker.ts │ ├── helpers.ts │ ├── index.ts │ ├── keybindings.ts │ └── manager.ts │ ├── charts │ ├── core.ts │ ├── flot.tickformat.ts │ ├── flot.ts │ ├── graphite.ts │ ├── index.ts │ ├── legend.ts │ ├── palettes.ts │ ├── placeholder.ts │ └── util.ts │ ├── client.ts │ ├── core │ ├── action.ts │ ├── index.ts │ └── property.ts │ ├── data │ └── graphite.ts │ ├── edit │ ├── edit.ts │ ├── property-sheets.ts │ └── queries.ts │ ├── importer │ ├── graphite.ts │ └── index.ts │ ├── index.ts │ ├── models │ ├── axis.ts │ ├── dashboard.ts │ ├── data │ │ ├── query.ts │ │ └── summation.ts │ ├── index.ts │ ├── items.ts │ ├── items │ │ ├── bar_chart.ts │ │ ├── cell.ts │ │ ├── chart.ts │ │ ├── comparison_jumbotron_singlestat.ts │ │ ├── comparison_singlestat.ts │ │ ├── comparison_summation_table.ts │ │ ├── container.ts │ │ ├── dashboard_definition.ts │ │ ├── discrete_bar_chart.ts │ │ ├── donut_chart.ts │ │ ├── factory.ts │ │ ├── heading.ts │ │ ├── item.ts │ │ ├── jumbotron_singlestat.ts │ │ ├── markdown.ts │ │ ├── percentage_table.ts │ │ ├── presentation.ts │ │ ├── row.ts │ │ ├── scatter_plot.ts │ │ ├── section.ts │ │ ├── separator.ts │ │ ├── simple_time_series.ts │ │ ├── singlegraph.ts │ │ ├── singlegraph_grid.ts │ │ ├── singlestat.ts │ │ ├── singlestat_grid.ts │ │ ├── standard_time_series.ts │ │ ├── summation_table.ts │ │ ├── table_presentation.ts │ │ ├── timerstat.ts │ │ ├── timeshift_jumbotron_singlestat.ts │ │ ├── timeshift_singlestat.ts │ │ ├── timeshift_summation_table.ts │ │ └── xychart.ts │ ├── model.ts │ ├── preferences.ts │ ├── tag.ts │ ├── thresholds.ts │ ├── transform │ │ ├── HighlightAverages.ts │ │ ├── Isolate.ts │ │ ├── SimpleGrid.ts │ │ ├── TimeShift.ts │ │ ├── TimeSpans.ts │ │ └── transform.ts │ └── user.ts │ └── util │ ├── event-source.ts │ ├── event.ts │ ├── index.ts │ ├── log.ts │ ├── registry.ts │ ├── template.ts │ └── util.ts └── tessera-server ├── MANIFEST.in ├── dev-requirements.txt ├── integration └── main.py ├── migrations ├── README ├── alembic.ini ├── env.py └── script.py.mako ├── requirements.txt ├── setup.py ├── tasks.py └── tessera ├── __init__.py ├── _version.py ├── application.py ├── client ├── __init__.py └── api │ ├── __init__.py │ ├── client.py │ └── model.py ├── config.py ├── database.py ├── helpers.py ├── importer ├── __init__.py ├── graphite.py └── json_importer.py ├── main.py ├── templates ├── base.html ├── dashboard-create.html ├── dashboard-embed.html ├── dashboard-list.html ├── dashboard.html ├── favorites.html ├── import.html ├── index.html ├── preferences.html ├── snippets │ ├── breadcrumbs.html │ ├── dashboard-info-edit-panel.html │ ├── dashboard-toolbar.html │ ├── range-picker.html │ ├── refresh-button.html │ ├── rejigger-button.html │ ├── site-footer.html │ ├── site-header.html │ └── theme-button.html └── standard.html ├── views_api.py └── views_ui.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | tessera-server/tessera/static/ 3 | tessera-server/dist 4 | 5 | # Miscellany 6 | .DS_Store 7 | *~ 8 | *# 9 | .#* 10 | 11 | # TypeScript 12 | tscommand* 13 | 14 | # Node, etc... 15 | npm-debug.log 16 | 17 | # Assorted Python 18 | *.pyc 19 | *.egg-info/ 20 | .Python 21 | 22 | # Python virtualenv dirs 23 | tessera-server/env* 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install virtualenv 6 | - ./script/bootstrap 7 | script: ./script/cibuild 8 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | 2 | ### Development Lead 3 | 4 | * Adam Alpern 5 | 6 | ### Contributors 7 | 8 | * Jeff Forcier 9 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2014, Urban Airship and Contributors 2 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | While Tessera has a lot of custom code on the front end, it's been 2 | built with as many "off-the-shelf" components as possible. 3 | 4 | ### Third-Party Code ### 5 | 6 | **Server Side** 7 | 8 | * [Flask](http://flask.pocoo.org/) 9 | * [Flask-SQLAlchemy](http://pythonhosted.org/Flask-SQLAlchemy/) 10 | * [Flask-Migrate](https://github.com/miguelgrinberg/Flask-Migrate) 11 | * [SLQAlchemy](http://www.sqlalchemy.org/) 12 | * [requests](https://github.com/kennethreitz/requests) 13 | * [inflection](https://github.com/jpvanhal/inflection) 14 | * [invoke](https://github.com/pyinvoke/invoke) 15 | 16 | **Client Side** 17 | 18 | * [Bootstrap](http://getbootstrap.com/) 19 | * [Font Awesome](http://fortawesome.github.com/Font-Awesome/) 20 | * [darkstrap](https://github.com/danneu/darkstrap) 21 | * The extracted 22 | [callouts](https://gist.github.com/matthiasg/6153853) from 23 | bootstrap's documentation site. 24 | * [bootbox](http://bootboxjs.com/) simplifies modal dialog interactions. 25 | * [bootstrap-validator](http://bootstrapvalidator.com/). This is now 26 | [formvalidation.io](https://github.com/formvalidation/), and 27 | appears to no longer have an OSS compatible license (the version 28 | included here is MIT licensed). 29 | * [bootstrap-growl](https://github.com/mouse0270/bootstrap-growl) 30 | * [bootstrap-datetimepicker](https://github.com/Eonasdan/bootstrap-datetimepicker) 31 | * [jQuery](http://jquery.com/) 32 | * [d3](http://d3js.org). d3 is used for value formatting in text and 33 | for stacked graph layout. 34 | * [flot](http://www.flotcharts.org/) is used for interactive chart 35 | rendering. 36 | * [flot-axislabels](https://github.com/mikeslim7/flot-axislabels) 37 | * [flot-valuelabels](https://github.com/winne27/flot-valuelabels) 38 | * [flot.multihighlight](https://github.com/eugenijusr/flot.multihighlight) 39 | * [flot-barnumbers-enhanced](https://github.com/jasonroman/flot-barnumbers-enhanced) 40 | * [flot-d3-stack](https://github.com/aalpern/flot-d3-stack/) 41 | * [flot-downsample](https://github.com/sveinn-steinarsson/flot-downsample/) 42 | * [DataTables](http://datatables.net/) 43 | * [moment.js](http://momentjs.com/) for time parsing & formatting. 44 | * [moment-timezone.js](http://momentjs.com/timezone/) 45 | * [bean.js](https://github.com/fat/bean) for events. 46 | * [handlebars.js](http://handlebarsjs.com/) for client side templating. 47 | * [marked](https://github.com/chjj/marked) for Markdown support. 48 | * [highlight.js](http://highlightjs.org/) 49 | * [URI.js](https://github.com/medialize/URI.js) for URL manipulation. 50 | * [limivorous](https://github.com/aalpern/limivorous) 51 | * [tagmanager](https://github.com/max-favilli/tagmanager) 52 | * [x-editable](http://vitalets.github.io/x-editable/) 53 | * [color](https://github.com/harthur/color) 54 | * [mousetrap](https://github.com/ccampbell/mousetrap) 55 | * [simple-statistics](https://github.com/tmcw/simple-statistics) 56 | * [equalize.js](https://github.com/tsvensen/equalize.js/) 57 | * [usertiming.js](https://github.com/nicjansma/usertiming.js) provides 58 | polyfill of the W3 [User Timing](http://www.w3.org/TR/user-timing/) 59 | API for browsers that don't support it natively (i.e. Safari 8). 60 | * [node-inflection](https://github.com/dreamerslab/node.inflection) 61 | * [HumanizeDuration](https://github.com/EvanHahn/HumanizeDuration.js) 62 | * [filejaver.js](https://www.npmjs.com/package/filesaver.js) 63 | -------------------------------------------------------------------------------- /demo/demo-billboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": "Demo", 3 | "definition": { 4 | "items": [ 5 | { 6 | "layout": "fixed", 7 | "level": 1, 8 | "items": [ 9 | { 10 | "item_id": "d5", 11 | "item_type": "row", 12 | "items": [ 13 | { 14 | "item_id": "d6", 15 | "item_type": "cell", 16 | "style": "well", 17 | "span": 12, 18 | "items": [ 19 | { 20 | "stack_mode": "stream", 21 | "height": 2, 22 | "item_type": "stacked_area_chart", 23 | "hide_zero_series": false, 24 | "item_id": "d7", 25 | "query": "query1", 26 | "options": { 27 | "palette": "d3category20c" 28 | }, 29 | "legend": "none", 30 | "interactive": true 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ], 37 | "item_type": "section", 38 | "horizontal_rule": false, 39 | "item_id": "d4" 40 | } 41 | ], 42 | "item_type": "dashboard_definition", 43 | "href": "/api/dashboard/1/definition", 44 | "queries": { 45 | "query1": { 46 | "name": "query1", 47 | "targets": [ 48 | "group(\n absolute(randomWalkFunction(\"query1\")),\n absolute(randomWalkFunction(\"query2\")),\n absolute(randomWalkFunction(\"query3\")),\n absolute(randomWalkFunction(\"query4\")),\n absolute(randomWalkFunction(\"query5\")),\n absolute(randomWalkFunction(\"query6\"))\n)" 49 | ] 50 | } 51 | }, 52 | "item_id": "d3", 53 | "options": { 54 | "from": "-3h" 55 | } 56 | }, 57 | "view_href": "/dashboards/1/billboard", 58 | "description": "", 59 | "tags": [ 60 | { 61 | "count": 1, 62 | "id": 1, 63 | "name": "featured-billboard" 64 | } 65 | ], 66 | "title": "Billboard", 67 | "summary": "", 68 | "definition_href": "/api/dashboard/1/definition", 69 | "href": "/api/dashboard/1", 70 | "id": 1, 71 | "imported_from": null 72 | } -------------------------------------------------------------------------------- /docs/dashboard-items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/docs/dashboard-items.png -------------------------------------------------------------------------------- /docs/screenshots/color-themes-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/docs/screenshots/color-themes-small.png -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/bootstrap: Resolve all dependencies that the application 4 | # requires to run. 5 | 6 | set -e 7 | 8 | cd "$(dirname "$0")/.." 9 | 10 | echo "==> Installing Javascript dependencies..." 11 | cd tessera-frontend/ 12 | npm install 13 | 14 | cd ../tessera-server/ 15 | if [ ! -d "env" ]; then 16 | echo "==> Creating python virtualenv..." 17 | python3 -m venv ./env 18 | fi 19 | 20 | echo "==> Installing Python dependencies..." 21 | . env/bin/activate 22 | pip install -r requirements.txt 23 | pip install -r dev-requirements.txt 24 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/cibuild: Setup environment for CI to run tests. This is primarily 4 | # designed to run on the continuous integration server. 5 | 6 | set -e 7 | 8 | cd "$(dirname "$0")/.." 9 | 10 | script/setup 11 | script/test 12 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/console: Launch a console for the application. Optionally allow an 4 | # environment to be passed in to let the script handle the 5 | # specific requirements for connecting to a console for that 6 | # environment. 7 | 8 | set -e 9 | 10 | cd "$(dirname "$0")/../tessera-server" 11 | . env/bin/activate 12 | 13 | python 14 | -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/server: Launch the application and any extra required 4 | # processes locally. 5 | 6 | set -e 7 | 8 | cd "$(dirname "$0")/.." 9 | 10 | # boot the app and any other necessary processes. 11 | cd tessera-server/ 12 | . env/bin/activate 13 | inv run 14 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/setup: Set up application for the first time after cloning, 4 | # or set it back to the initial first unused state. 5 | 6 | set -e 7 | 8 | cd "$(dirname "$0")/.." 9 | 10 | script/bootstrap 11 | 12 | cd "$(dirname "$0")/.." 13 | 14 | echo "==> Compiling frontend..." 15 | cd tessera-frontend/ 16 | ./node_modules/.bin/grunt 17 | 18 | echo "==> Initializing database..." 19 | cd ../tessera-server/ 20 | . env/bin/activate 21 | 22 | # Create a fresh database 23 | inv db.init 24 | 25 | echo "==> Ready to run!" 26 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/test: Run test suite for application. Optionallly pass in a path to an 4 | # individual test file to run a single test. 5 | 6 | 7 | set -e 8 | 9 | cd "$(dirname "$0")/.." 10 | -------------------------------------------------------------------------------- /script/update: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/update: Update application to run for its current checkout. 4 | 5 | set -e 6 | 7 | cd "$(dirname "$0")/.." 8 | 9 | script/bootstrap 10 | 11 | cd tessera-frontend/ 12 | ./node_modules/.bin/grunt 13 | cd .. 14 | -------------------------------------------------------------------------------- /tessera-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | package-lock.json 3 | .tsCache 4 | src/ts/.baseDir.ts 5 | bower_components/ 6 | node_modules/ 7 | screenshots/ 8 | -------------------------------------------------------------------------------- /tessera-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tessera", 3 | "version": "0.11.0-beta", 4 | "description": "A dashboard front-end for Graphite.", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/tessera-metrics/tessera" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/tessera-metrics/tessera/issues" 20 | }, 21 | "homepage": "https://github.com/tessera-metrics/tessera", 22 | "dependencies": { 23 | "axios": "0.18.1", 24 | "bean": "1.0.15", 25 | "bluebird": "3.4.6", 26 | "bootbox": "5.1.3", 27 | "bootstrap": "3.4.1", 28 | "bootstrap-notify": "3.1.3", 29 | "color": "3.1.1", 30 | "d3-format": "1.3.2", 31 | "d3-shape": "1.3.5", 32 | "datatables": "1.10.18", 33 | "extend": "3.0.2", 34 | "file-saver": "2.0.1", 35 | "flot": "3.2.9", 36 | "flot-axislabels": "1.0.0", 37 | "flot-d3-stack": "2.1.1", 38 | "handlebars": "4.1.2", 39 | "highlight.js": "9.15.6", 40 | "holderjs": "2.9.6", 41 | "humanize-duration": "3.18.0", 42 | "inflection": "1.12.0", 43 | "jquery": "2.2.4", 44 | "marked": "0.7.0", 45 | "moment": "2.24.0", 46 | "moment-timezone": "0.5.25", 47 | "mousetrap": "1.6.3", 48 | "select2": "3.5.2-browserify", 49 | "simple-statistics": "7.0.2", 50 | "store": "2.0.12", 51 | "urijs": "1.19.1" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "7.4.4", 55 | "@babel/polyfill": "7.4.4", 56 | "@babel/preset-env": "7.4.4", 57 | "babelify": "10.0.0", 58 | "bootstrap-sass": "^3.4.1", 59 | "dart-sass": "^1.22.10", 60 | "grunt": "1.0.4", 61 | "grunt-autoprefixer": "^3.0.4", 62 | "grunt-browserify": "5.3.0", 63 | "grunt-cli": "1.3.2", 64 | "grunt-contrib-clean": "2.0.0", 65 | "grunt-contrib-concat": "1.0.1", 66 | "grunt-contrib-copy": "1.0.0", 67 | "grunt-contrib-handlebars": "1.0.0", 68 | "grunt-contrib-uglify": "4.0.0", 69 | "grunt-contrib-watch": "1.1.0", 70 | "grunt-run": "0.8.1", 71 | "grunt-sass": "3.0.2", 72 | "grunt-ts": "6.0.0-beta.22", 73 | "mkdirp": "0.5.1", 74 | "puppeteer": "1.14.0", 75 | "typescript": "3.4.4" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tessera-frontend/screenshots.js: -------------------------------------------------------------------------------- 1 | // -*- mode:javascript -*- 2 | 3 | const { URL } = require ('url') 4 | const path = require('path') 5 | const axios = require('axios') 6 | const puppeteer = require('puppeteer') 7 | const mkdirp = require('mkdirp') 8 | const moment = require('moment') 9 | 10 | const outputdir = 'screenshots' 11 | const rooturl = 'http://localhost:5000' 12 | const defaultViewport = { width: 1920, height: 1080, isLandscape: true, deviceScaleFactor: 2 } 13 | const GRAPHITE_TIME_FORMAT = 'hh:mm_YYYYMMDD' 14 | 15 | function msleep(n) { 16 | Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, n); 17 | } 18 | 19 | function sleep(n) { 20 | msleep(n*1000); 21 | } 22 | 23 | // Fetch the list of dashboards from the API, so we can get all the 24 | // URLs for the dashboard pages 25 | async function list_dashboards(tag) { 26 | var url = rooturl + '/api/dashboard/' 27 | if (tag) { 28 | url += 'tagged/' + tag 29 | } 30 | return axios.get(url) 31 | .then(resp => { 32 | return resp.data 33 | }) 34 | .catch(err => { 35 | console.log(err) 36 | }); 37 | } 38 | 39 | // Navigate to a dashboard and take a screenshot to the output 40 | // directory 41 | async function screenshot(browser, dashboard, viewport) { 42 | var tab = await browser.newPage() 43 | var url = rooturl + dashboard.view_href + '/embed' 44 | var u = new URL(url) 45 | var name = path.basename(dashboard.view_href) 46 | var day = moment().utc().startOf('day').subtract(1, 'day') 47 | var from = day.clone().hour(13).minute(0) 48 | var until = day.clone().hour(13).minute(30) 49 | var theme = 'light' 50 | 51 | await tab.setViewport(viewport || defaultViewport) 52 | 53 | u.searchParams.set('theme', 'light') 54 | u.searchParams.set('from', from.format(GRAPHITE_TIME_FORMAT)) 55 | u.searchParams.set('until', until.format(GRAPHITE_TIME_FORMAT)) 56 | console.log('Fetching ' + u) 57 | await tab.goto(u) 58 | await sleep.sleep(1) 59 | 60 | var rootElement = await tab.$('html') 61 | var boundingBox = await rootElement.boundingBox() 62 | boundingBox.height += 12 63 | 64 | var outpath = outputdir + '/' + dashboard.category + '-' + name + '-' + theme + '.png' 65 | console.log('Saving ' + outpath) 66 | await tab.screenshot({ 67 | clip: boundingBox, 68 | path: outpath 69 | }) 70 | await tab.close() 71 | } 72 | 73 | (async () => { 74 | mkdirp.sync(outputdir) 75 | 76 | const browser = await puppeteer.launch({ 77 | headless: true, 78 | args: [ '--allow-running-insecure-content' ] 79 | }) 80 | 81 | var dashboards = await list_dashboards('render-test') 82 | for (var d of dashboards) { 83 | await screenshot(browser, d) 84 | } 85 | await browser.close() 86 | })() 87 | 88 | -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/css/bootstrap-callouts.css: -------------------------------------------------------------------------------- 1 | /* Side notes for calling out things 2 | -------------------------------------------------- */ 3 | 4 | /* Base styles (regardless of theme) */ 5 | .bs-callout { 6 | margin: 0; 7 | padding: 15px 30px 15px 15px; 8 | border-left: 5px solid #eee; 9 | } 10 | .bs-callout h1, 11 | .bs-callout h2, 12 | .bs-callout h3, 13 | .bs-callout h4, 14 | .bs-callout h5, 15 | .bs-callout h6 { 16 | margin-top: 0; 17 | } 18 | 19 | .bs-callout-danger h1, 20 | .bs-callout-danger h2, 21 | .bs-callout-danger h3, 22 | .bs-callout-danger h4, 23 | .bs-callout-danger h5, 24 | .bs-callout-danger h6 { 25 | color: #B94A48; 26 | } 27 | 28 | .bs-callout-warning h1, 29 | .bs-callout-warning h2, 30 | .bs-callout-warning h3, 31 | .bs-callout-warning h4, 32 | .bs-callout-warning h5, 33 | .bs-callout-warning h6 { 34 | color: #C09853; 35 | } 36 | 37 | .bs-callout-info h1, 38 | .bs-callout-info h2, 39 | .bs-callout-info h3, 40 | .bs-callout-info h4, 41 | .bs-callout-info h5, 42 | .bs-callout-info h6 { 43 | color: #3A87AD; 44 | } 45 | 46 | .bs-callout-success h1, 47 | .bs-callout-success h2, 48 | .bs-callout-success h3, 49 | .bs-callout-success h4, 50 | .bs-callout-success h5, 51 | .bs-callout-success h6 { 52 | color: #3C763D; 53 | } 54 | 55 | .bs-callout p:last-child { 56 | margin-bottom: 0; 57 | } 58 | 59 | .bs-callout code, 60 | .bs-callout .highlight { 61 | background-color: #fff; 62 | } 63 | 64 | /* Themes for different contexts */ 65 | .bs-callout-danger { 66 | background-color: #fcf2f2; 67 | border-color: #dFb5b4; 68 | } 69 | .bs-callout-warning { 70 | background-color: #fefbed; 71 | border-color: #f1e7bc; 72 | } 73 | .bs-callout-info { 74 | background-color: #f0f7fd; 75 | border-color: #d0e3f0; 76 | } 77 | .bs-callout-success { 78 | background-color: #dff0d8; 79 | border-color: #d6e9c6; 80 | } 81 | .bs-callout-neutral { 82 | background-color: #f0f0f0; 83 | border-color: #d0d0d0; 84 | } 85 | -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/css/bootstrapValidator.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * BootstrapValidator (http://bootstrapvalidator.com) 3 | * 4 | * A jQuery plugin to validate form fields. Use with Bootstrap 3 5 | * 6 | * @version v0.4.4 7 | * @author https://twitter.com/nghuuphuoc 8 | * @copyright (c) 2013 - 2014 Nguyen Huu Phuoc 9 | * @license MIT 10 | */ 11 | 12 | 13 | .bv-form .help-block{margin-bottom:0}.nav-tabs li.bv-tab-success>a{color:#3c763d}.nav-tabs li.bv-tab-error>a{color:#a94442} -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/css/highlight-styles/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-template_comment, 15 | .diff .hljs-header, 16 | .hljs-javadoc { 17 | color: #998; 18 | font-style: italic 19 | } 20 | 21 | .hljs-keyword, 22 | .css .rule .hljs-keyword, 23 | .hljs-winutils, 24 | .javascript .hljs-title, 25 | .nginx .hljs-title, 26 | .hljs-subst, 27 | .hljs-request, 28 | .hljs-status { 29 | color: #333; 30 | font-weight: bold 31 | } 32 | 33 | .hljs-number, 34 | .hljs-hexcolor, 35 | .ruby .hljs-constant { 36 | color: #099; 37 | } 38 | 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-phpdoc, 42 | .tex .hljs-formula { 43 | color: #d14 44 | } 45 | 46 | .hljs-title, 47 | .hljs-id, 48 | .coffeescript .hljs-params, 49 | .scss .hljs-preprocessor { 50 | color: #900; 51 | font-weight: bold 52 | } 53 | 54 | .javascript .hljs-title, 55 | .lisp .hljs-title, 56 | .clojure .hljs-title, 57 | .hljs-subst { 58 | font-weight: normal 59 | } 60 | 61 | .hljs-class .hljs-title, 62 | .haskell .hljs-type, 63 | .vhdl .hljs-literal, 64 | .tex .hljs-command { 65 | color: #458; 66 | font-weight: bold 67 | } 68 | 69 | .hljs-tag, 70 | .hljs-tag .hljs-title, 71 | .hljs-rules .hljs-property, 72 | .django .hljs-tag .hljs-keyword { 73 | color: #000080; 74 | font-weight: normal 75 | } 76 | 77 | .hljs-attribute, 78 | .hljs-variable, 79 | .lisp .hljs-body { 80 | color: #008080 81 | } 82 | 83 | .hljs-regexp { 84 | color: #009926 85 | } 86 | 87 | .hljs-symbol, 88 | .ruby .hljs-symbol .hljs-string, 89 | .lisp .hljs-keyword, 90 | .tex .hljs-special, 91 | .hljs-prompt { 92 | color: #990073 93 | } 94 | 95 | .hljs-built_in, 96 | .lisp .hljs-title, 97 | .clojure .hljs-built_in { 98 | color: #0086b3 99 | } 100 | 101 | .hljs-preprocessor, 102 | .hljs-pragma, 103 | .hljs-pi, 104 | .hljs-doctype, 105 | .hljs-shebang, 106 | .hljs-cdata { 107 | color: #999; 108 | font-weight: bold 109 | } 110 | 111 | .hljs-deletion { 112 | background: #fdd 113 | } 114 | 115 | .hljs-addition { 116 | background: #dfd 117 | } 118 | 119 | .diff .hljs-change { 120 | background: #0086b3 121 | } 122 | 123 | .hljs-chunk { 124 | color: #aaa 125 | } 126 | -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/css/jquery.flot.valuelabels.css: -------------------------------------------------------------------------------- 1 | .flotValueLabels { 2 | color:black; 3 | } 4 | .flotValueLabelLight { 5 | opacity:0.5; 6 | background-color: white; 7 | border:none; 8 | position:absolute; 9 | } 10 | .flotValueLabel { 11 | position:absolute; 12 | border:none; 13 | } 14 | -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/css/select2-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/css/select2-spinner.gif -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/css/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/css/select2.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/back_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/back_disabled.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/back_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/back_enabled.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/back_enabled_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/back_enabled_hover.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/clear.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/forward_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/forward_disabled.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/forward_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/forward_enabled.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/forward_enabled_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/forward_enabled_hover.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/loading.gif -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/sort_asc.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/sort_asc_disabled.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/sort_both.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/sort_desc.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/img/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-frontend/src/3rd-Party/img/sort_desc_disabled.png -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/js/equalize.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * equalize.js 3 | * Author & copyright (c) 2012: Tim Svensen 4 | * Dual MIT & GPL license 5 | * 6 | * Page: http://tsvensen.github.com/equalize.js 7 | * Repo: https://github.com/tsvensen/equalize.js/ 8 | */ 9 | !function(i){i.fn.equalize=function(e){var n,t,h=!1,c=!1;return i.isPlainObject(e)?(n=e.equalize||"height",h=e.children||!1,c=e.reset||!1):n=e||"height",i.isFunction(i.fn[n])?(t=0s&&(s=e)}),e.css(t,s+"px")})):!1}}(jQuery); -------------------------------------------------------------------------------- /tessera-frontend/src/3rd-Party/js/flot/jquery.flot.multihighlight.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | var options = { 4 | multihighlight: { 5 | mode: null, // null, x or y. 6 | linkedPlots: null, // null or array of plots. 7 | } 8 | }; 9 | 10 | function init(plot) { 11 | 12 | // Builds an array of plots to highlight - the current plot and all the linked plots if any. 13 | function getPlotsToHighlight() { 14 | var plotsToHighlight = [ plot ]; 15 | if (plot.getOptions().multihighlight.linkedPlots != null) { 16 | $.each(plot.getOptions().multihighlight.linkedPlots, function(index, linkedPlot){ 17 | plotsToHighlight.push(linkedPlot); 18 | }); 19 | } 20 | return plotsToHighlight; 21 | } 22 | 23 | function onPlotHover(event, position, item) { 24 | 25 | $.each(getPlotsToHighlight(), function(index, plotToHighlight) { 26 | if (!plotToHighlight.getOptions().multihighlight.mode != null) { 27 | plotToHighlight.unhighlight(); 28 | 29 | var axisPosition = plotToHighlight.getOptions().multihighlight.mode == 'x' ? position.x : position.y; 30 | var dataIndex = plotToHighlight.getOptions().multihighlight.mode == 'x' ? 0 : 1; 31 | 32 | var highlightedItems = new Array(); 33 | $.each(plotToHighlight.getData(), function(i, serie) { 34 | var j; 35 | for (j = 0; j < serie.data.length; j++) { 36 | if (serie.data[j] == null || serie.data[j][dataIndex] > axisPosition) { 37 | break; 38 | } 39 | } 40 | 41 | if (j != 0 && serie.data[j] !=null) { 42 | var highlighted = j-1; 43 | // Checking which one is closer. 44 | if (axisPosition-serie.data[j-1][dataIndex] > Math.abs(axisPosition-serie.data[j][dataIndex])) { 45 | highlighted = j; 46 | } 47 | 48 | plotToHighlight.highlight(i, highlighted); 49 | highlightedItems.push({ serieIndex: i, dataIndex: highlighted }); 50 | } 51 | }); 52 | 53 | if (highlightedItems.length > 0) { 54 | plotToHighlight.getPlaceholder().trigger("multihighlighted", [ position, highlightedItems ]); 55 | } 56 | else { 57 | plotToHighlight.getPlaceholder().trigger("unmultihighlighted"); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | function onMouseOut(event) { 64 | $.each(getPlotsToHighlight(), function(index, plotToHighlight) { 65 | if (!plotToHighlight.getOptions().multihighlight.mode != null) { 66 | plotToHighlight.unhighlight(); 67 | plotToHighlight.getPlaceholder().trigger("unmultihighlighted"); 68 | } 69 | }); 70 | } 71 | 72 | // Hook up the events. 73 | plot.hooks.bindEvents.push(function(plot, eventHolder) { 74 | if (!plot.getOptions().multihighlight.mode) 75 | return; 76 | 77 | plot.getPlaceholder().bind('plothover', onPlotHover); 78 | plot.getPlaceholder().bind('mouseout', onMouseOut); 79 | }); 80 | plot.hooks.shutdown.push(function(plot, eventHolder) { 81 | plot.getPlaceholder().unbind('plothover', onPlotHover); 82 | plot.getPlaceholder().unbind('mouseout', onMouseOut); 83 | }); 84 | } 85 | 86 | $.plot.plugins.push({ 87 | init: init, 88 | options: options, 89 | name: 'multihighlight', 90 | version: '1.0' 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/bootstrap/light.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '../fonts/'; 2 | @import "../themes/light.scss"; 3 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss"; 4 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/bootstrap/snow.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '../fonts/'; 2 | @import "../themes/snow.scss"; 3 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss"; 4 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/bootstrap/solarized-dark.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '../fonts/'; 2 | @import "../themes/solarized-dark.scss"; 3 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss"; 4 | 5 | .page-header { 6 | border-bottom-color: $background-dark-0 !important; 7 | box-shadow: rgba(#fff, .07) 0 1px 0; 8 | border-bottom: 1px solid #121212; 9 | } 10 | 11 | hr { 12 | border: none; 13 | border-bottom-color: $background-dark-0 !important; 14 | box-shadow: rgba(#fff, .07) 0 1px 0; 15 | border-bottom: 1px solid #121212; 16 | } 17 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/bootstrap/solarized-light.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '../fonts/'; 2 | @import "../themes/solarized-light.scss"; 3 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss"; 4 | 5 | .page-header { 6 | border-bottom-color: $base2 !important; 7 | } 8 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/fonts.scss: -------------------------------------------------------------------------------- 1 | $font-stack-condensed: "HelveticaNeue-CondensedBold", "Helvetica Neue", "Arial Narrow", Arial, sans-serif; 2 | $font-stack-code: Hack, Consolas, "Andale Mono", "Courier New", monospace; 3 | $font-stack-sans-serif: system, -apple-system, ".SFNSDisplay-Regular", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Arial Narrow", "Arial", sans-serif; 4 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/_alerts.scss: -------------------------------------------------------------------------------- 1 | /* Alerts */ 2 | 3 | // Bootstrap 3 defaults 4 | $alert-neutral-border: gray !default; 5 | $alert-neutral-bg: #f0f0f0 !default; 6 | $alert-neutral-text: darken(darkgray, 20%) !default; 7 | 8 | .alert-neutral, 9 | .bs-callout-neutral { 10 | background-color: $alert-neutral-bg; 11 | border-color: $alert-neutral-border; 12 | h1, h2, h3, h4, h5, h6 { 13 | color: $alert-neutral-text; 14 | } 15 | } 16 | 17 | .alert-info, 18 | .bs-callout-info { 19 | background-color: $alert-info-bg; 20 | border-color: $alert-info-border; 21 | h1, h2, h3, h4, h5, h6 { 22 | color: $alert-info-text; 23 | } 24 | } 25 | 26 | .alert-success, 27 | .bs-callout-success { 28 | background-color: $alert-success-bg; 29 | border-color: $alert-success-border; 30 | h1, h2, h3, h4, h5, h6 { 31 | color: $alert-success-text; 32 | } 33 | } 34 | 35 | .alert-warning, 36 | .bs-callout-warning { 37 | background-color: $alert-warning-bg; 38 | border-color: $alert-warning-border; 39 | h1, h2, h3, h4, h5, h6 { 40 | color: $alert-warning-text; 41 | } 42 | } 43 | 44 | .alert-danger, 45 | .bs-callout-danger { 46 | background-color: $alert-danger-bg; 47 | border-color: $alert-danger-border; 48 | h1, h2, h3, h4, h5, h6 { 49 | color: $alert-danger-text; 50 | } 51 | } 52 | 53 | .ds-warning { 54 | h3, .value, .unit { 55 | color: $alert-warning-text; 56 | } 57 | } 58 | 59 | .ds-danger { 60 | h3, .value, .unit { 61 | color: $alert-danger-text; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/_charts.scss: -------------------------------------------------------------------------------- 1 | $ds-tooltip-bg: white !default; 2 | $ds-tooltip-fg: black !default; 3 | $ds-tooltip-border-color: $gray-dark !default; 4 | $ds-tooltip-border: 1px solid $ds-tooltip-border-color !default; 5 | 6 | #ds-tooltip { 7 | color: $ds-tooltip-fg; 8 | background-color: $ds-tooltip-bg; 9 | border: $ds-tooltip-border; 10 | } 11 | 12 | $ds-value-label-fg: $gray !default; 13 | $ds-value-label-border-color: #a95353 !default; 14 | 15 | .flotValueLabel { 16 | color: $ds-value-label-fg; 17 | border-left-color: $ds-value-label-border-color; 18 | } 19 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/_edit.scss: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | Edit mode headers 3 | ----------------------------------------------------------------------------- */ 4 | 5 | $ds-badge-section: #387AA3 !default; 6 | $ds-badge-row: #649EB9 !default; 7 | $ds-badge-cell: #649EB9 !default; 8 | $ds-badge-item: #D9EDF7 !default; 9 | 10 | $ds-badge-text: black !default; 11 | 12 | $ds-edit-bar-section-background: #9DC2D3 !default; 13 | $ds-edit-bar-section-border: $ds-badge-section !default; 14 | $ds-edit-bar-row-border: $ds-badge-row !default; 15 | $ds-edit-bar-row-background: #D9EDF7 !default; 16 | $ds-badge-cell-border: #9DC2D3 !default; 17 | $ds-edit-bar-cell-background: #E3F7FF !default; 18 | $ds-edit-bar-cell-border: $ds-badge-cell-border !default; 19 | $ds-badge-item-text: #387AA3 !default; 20 | $ds-badge-item-border: #649EB9 !default; 21 | $ds-edit-bar-item-background: #FAFAFA !default; 22 | $ds-edit-bar-item-border: $ds-badge-item-border !default; 23 | 24 | .ds-badge-section, .ds-badge-row, .ds-badge-cell { 25 | color: $ds-badge-text; 26 | } 27 | 28 | /* 29 | * Section 30 | */ 31 | 32 | .ds-badge-section { 33 | background-color: $ds-badge-section; 34 | } 35 | 36 | .ds-edit-bar-section { 37 | background-color: $ds-edit-bar-section-background; 38 | border: 1px solid $ds-edit-bar-section-border; 39 | } 40 | 41 | /* 42 | * Row 43 | */ 44 | 45 | .ds-badge-row { 46 | background-color: $ds-badge-row; 47 | } 48 | 49 | .ds-edit-bar-row { 50 | border: 1px solid $ds-edit-bar-row-border; 51 | background-color: $ds-edit-bar-row-background; 52 | } 53 | 54 | .ds-row.ds-edit { 55 | border: 1px solid $ds-badge-row; 56 | } 57 | 58 | /* 59 | * Cell 60 | */ 61 | 62 | .ds-badge-cell { 63 | background-color: $ds-badge-cell; 64 | border: 1px solid $ds-badge-cell-border; 65 | } 66 | 67 | .ds-edit-bar-cell { 68 | background-color: $ds-edit-bar-cell-background; 69 | border: 1px solid $ds-edit-bar-cell-border; 70 | } 71 | 72 | /* 73 | * Item 74 | */ 75 | 76 | .ds-badge-item { 77 | background-color: $ds-badge-item; 78 | color: $ds-badge-item-text; 79 | border: 1px solid $ds-badge-item-border; 80 | } 81 | 82 | .ds-edit-bar-item { 83 | background-color: $ds-edit-bar-item-background; 84 | border: 1px solid $ds-edit-bar-item-border; 85 | } 86 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/_typography.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | body p { 4 | font-family: $font-stack-sans-serif !important; 5 | } 6 | 7 | h1, h2, h3, h4 { 8 | font-family: $font-stack-condensed !important; 9 | } 10 | 11 | .ds-source { 12 | font-family: $font-stack-code !important; 13 | } 14 | 15 | .ds-query-target { 16 | font-family: $font-stack-code !important; 17 | } 18 | 19 | /* Badges are just more readable when slightly less bold */ 20 | .badge { 21 | font-weight: 400; 22 | } 23 | 24 | .ds-timerstat h3, 25 | .ds-singlestat h3 { 26 | font-stretch: condensed; 27 | font-size: 1.2em; 28 | font-weight: normal; 29 | text-transform:uppercase; 30 | } 31 | 32 | .ds-timerstat p, 33 | .ds-singlestat p { 34 | font-size: 2.5em; 35 | font-weight: bold; 36 | } 37 | 38 | .ds-timerstat .unit, 39 | .ds-singlestat .unit, 40 | .ds-singlestat .diff { 41 | font-size: .5em; 42 | text-transform: lowercase; 43 | } 44 | 45 | .ds-jumbotron-singlestat .diff, 46 | .ds-jumbotron-singlestat .unit { 47 | font-family: $font-stack-condensed; 48 | text-transform: lowercase; 49 | font-size: 2em; 50 | font-weight: bold; 51 | } 52 | 53 | .ds-jumbotron-singlestat .value { 54 | font-size: 8em; 55 | font-weight: bold; 56 | } 57 | 58 | .ds-jumbotron-singlestat h3 { 59 | text-transform: lowercase; 60 | font-size: 2.5em; 61 | } 62 | 63 | .name-overlay h5 { 64 | font-size:12px; 65 | } 66 | 67 | .ds-singlegraph span.ds-label { 68 | font-size: 1.1em; 69 | } 70 | .ds-singlegraph h3 { 71 | font-size: 1.4em; 72 | } 73 | 74 | .ds-singlegraph span.value { 75 | font-size: 3em; 76 | font-weight: bold; 77 | } 78 | 79 | .ds-dashboard-listing-category { 80 | font-weight: 600; 81 | } 82 | .ds-dashboard-listing-title { 83 | font-weight: 600; 84 | font-size: 1.25em; 85 | } 86 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/light.scss: -------------------------------------------------------------------------------- 1 | @import "../themes/light.scss"; 2 | @import "../fonts.scss"; 3 | @import "_3rd-party.scss"; 4 | @import "_typography.scss"; 5 | @import "_alerts.scss"; 6 | @import "_charts.scss"; 7 | @import "_edit.scss"; 8 | @import "_app.scss"; 9 | 10 | h1 .ds-primary-title { 11 | color: gray; 12 | } 13 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/snow.scss: -------------------------------------------------------------------------------- 1 | @import "../themes/snow.scss"; 2 | @import "../fonts.scss"; 3 | @import "_3rd-party.scss"; 4 | @import "_typography.scss"; 5 | @import "_alerts.scss"; 6 | @import "_charts.scss"; 7 | @import "_edit.scss"; 8 | @import "_app.scss"; 9 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/solarized-dark.scss: -------------------------------------------------------------------------------- 1 | @import "../themes/solarized-dark.scss"; 2 | @import "../fonts.scss"; 3 | @import "_3rd-party.scss"; 4 | @import "_typography.scss"; 5 | @import "_alerts.scss"; 6 | @import "_charts.scss"; 7 | @import "_edit.scss"; 8 | @import "_app.scss"; 9 | 10 | .ds-heading small { 11 | color: $base2; 12 | } 13 | 14 | h1 .ds-secondary-title { 15 | color: $base2; 16 | } 17 | 18 | /* ----------------------------------------------------------------------------- 19 | Select2 20 | ----------------------------------------------------------------------------- */ 21 | 22 | .select2-container .select2-choices .select2-search-field input, 23 | .select2-container .select2-choice, 24 | .select2-container .select2-choices, 25 | .select2-search input, 26 | .select2-drop 27 | { 28 | background: $background-dark-2; 29 | border-color: $base01; 30 | color: white; 31 | } 32 | 33 | .select2-arrow { 34 | border-left: 1px solid #777; 35 | color: white; 36 | } 37 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/tessera/solarized-light.scss: -------------------------------------------------------------------------------- 1 | @import "../themes/solarized-light.scss"; 2 | @import "../fonts.scss"; 3 | @import "_3rd-party.scss"; 4 | @import "_typography.scss"; 5 | @import "_alerts.scss"; 6 | @import "_charts.scss"; 7 | @import "_edit.scss"; 8 | @import "_app.scss"; 9 | 10 | h1 .ds-primary-title { 11 | color: $base1; 12 | } 13 | 14 | /* ----------------------------------------------------------------------------- 15 | Select2 16 | ----------------------------------------------------------------------------- */ 17 | 18 | .select2-container .select2-choices .select2-search-field input, 19 | .select2-container .select2-choice, 20 | .select2-container .select2-choices, 21 | .select2-search input, 22 | .select2-drop 23 | { 24 | border-color: $content-4; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /tessera-frontend/src/css/themes/solarized.scss: -------------------------------------------------------------------------------- 1 | $base03: #002b36; 2 | $base02: #073642; 3 | $base01: #586e75; 4 | $base00: #657b83; 5 | $base0: #839496; 6 | $base1: #93a1a1; 7 | $base2: #eee8d5; 8 | $base3: #fdf6e3; 9 | 10 | $yellow: #b58900; 11 | $orange: #cb4b16; 12 | $red: #dc322f; 13 | $magenta: #d33682; 14 | $violet: #6c71c4; 15 | $blue: #268bd2; 16 | $cyan: #2aa198; 17 | $green: #859900; 18 | 19 | $background-dark-0: darken($base03, 3%); 20 | $background-dark-1: $base03; 21 | $background-dark-2: $base02; 22 | $background-light-1: $base2; 23 | $background-light-2: $base3; 24 | 25 | $content-1: $base01; 26 | $content-2: $base00; 27 | $content-3: $base0; 28 | $content-4: $base1; 29 | -------------------------------------------------------------------------------- /tessera-frontend/src/js/dependencies.js: -------------------------------------------------------------------------------- 1 | $ = jQuery = require('jquery') 2 | flot = require('flot') 3 | moment = require('moment') 4 | momenttz = require('moment-timezone') 5 | Handlebars = require('handlebars') 6 | URI = require('urijs') 7 | ss = require('simple-statistics') 8 | bootbox = require('bootbox') 9 | Color = require('color') 10 | Holder = require('holderjs') 11 | marked = require('marked') 12 | hljs = require('highlight.js') 13 | Mousetrap = require('mousetrap') 14 | bean = require('bean') 15 | bootstrap = require('bootstrap') 16 | select2 = require('select2') 17 | store = require('store') 18 | growl = require('bootstrap-notify') 19 | datatables = require('datatables')(window, $) 20 | saveAs = require('file-saver').saveAs 21 | Promise = require('bluebird') 22 | humanize_duration = require('humanize-duration') 23 | 24 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_action-menu-button.hbs: -------------------------------------------------------------------------------- 1 | {{#each actions}} 2 |
  • 7 | {{action.display}} 8 | {{/each}} 9 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-action-menu.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 7 | 12 |
    13 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-dashboard-listing-action-menu.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 8 | 12 |
    13 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-dashboard-listing-entry.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
    5 | 6 |
    7 | 8 | 9 | {{category}} 10 | 11 | 12 | {{title}} 13 | 14 | {{#if summary}} 15 |
    16 | {{summary}} 17 | {{/if}} 18 |

    19 | 20 | Last modified {{moment 'fromNow' last_modified_date}}. 21 | 22 |
    23 | 24 |
    25 | {{> ds-dashboard-listing-action-menu}} 26 |
    27 | 28 |
    29 | 30 |
    31 | 32 |
    33 | {{#each tags}} 34 | {{> ds-dashboard-tag-with-link this}} 35 | {{/each}} 36 |
    37 | {{#if imported_from}} 38 |
    39 | {{/if}} 40 |
    41 | 42 |
    43 | 44 |
    45 | 46 | 47 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-dashboard-tag-with-link.hbs: -------------------------------------------------------------------------------- 1 | {{> ds-dashboard-tag this}} 2 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-dashboard-tag.hbs: -------------------------------------------------------------------------------- 1 | 10 | {{name}} 11 | 12 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar-cell.hbs: -------------------------------------------------------------------------------- 1 |
    2 | cell {{item.item_id}} 3 | 6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar-definition.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | definition {{item.item_id}} 4 |
    5 |
    6 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar-item-details.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{> ds-item-property-sheet}} 4 |
    5 |
    6 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar-item.hbs: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar-row.hbs: -------------------------------------------------------------------------------- 1 |
    2 | row {{item.item_id}} 3 | 6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar-section.hbs: -------------------------------------------------------------------------------- 1 |
    2 | section {{item.item_id}} 3 | 6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-bar.hbs: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-edit-menu.hbs: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-preferences-renderer-entry.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 11 |

    {{renderer.description}}

    12 |
    13 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-row-edit-bar.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 6 | 7 | 8 | row 9 | 10 | 11 | 12 |
    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/_ds-title-bar.hbs: -------------------------------------------------------------------------------- 1 | {{ds-edit-bar item}} 2 |
    3 |
    4 | {{#if item.title}}

    {{item.title}}

    {{/if}} 5 |
    6 |
    7 | {{> ds-action-menu}} 8 |
    9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/action-menu-button.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 8 | 24 |
    25 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/action.hbs: -------------------------------------------------------------------------------- 1 | {{#if action.divider}} 2 |
  • 3 | {{else}} 4 |
  • 12 | {{/if}} 13 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/action_button.hbs: -------------------------------------------------------------------------------- 1 | {{#if action.divider}} 2 | {{else}} 3 | 12 | {{/if}} 13 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/_dashboard-metadata-panel.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |

    Properties

    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{#if imported_from}} 42 | 43 | 44 | 45 | {{/if}} 46 | 47 | 48 |
    Title{{title}}
    Category{{category}}
    Summary{{summary}}
    Tags 23 | 28 | 29 | 30 | 31 |
    Created{{moment 'MMMM Do YYYY, h:mm:ss a' creation_date}}
    Last Modified{{moment 'MMMM Do YYYY, h:mm:ss a' last_modified_date}}
    Imported FromLink
    49 |
    50 | 51 | 52 |
    53 |

    Description

    54 |
    61 | {{markdown description}} 62 |
    63 |
    64 | 65 |
    66 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/_dashboard-query-panel.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 |
    6 | {{actions 'dashboard-queries' 'button'}} 7 |
    8 |
    9 |
    10 |
    11 | 12 |
    13 |
    14 | 15 | {{#each definition.queries}} 16 | {{> dashboard-query-row}} 17 | {{/each}} 18 |
    19 |
    20 |
    21 | 22 |
    23 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/_dashboard-query-row.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{name}} 3 | {{targets}} 4 | 5 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/_ds-item-property-sheet.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{#each item.meta.interactive_properties}} 5 | 6 | 7 | 8 | 9 | {{/each}} 10 | 11 |
    PropertyValue
    {{#if category}}{{category}} / {{/if}}{{property_name}}{{interactive_property this ../item}}
    12 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/dashboard_panel.hbs: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 |
    8 | {{> dashboard-metadata-panel}} 9 |
    10 |
    11 | {{> dashboard-query-panel}} 12 |
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/item_source.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    {{json item}}
    3 |
    4 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/edit/view_query.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{markdown source}} 3 |
    4 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/flot/discrete_bar_tooltip.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 11 | 12 | 13 |
    5 | 6 | {{series.label}} 7 | 9 | {{value}} 10 |
    14 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/flot/donut_tooltip.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 11 | 12 | 13 |
    5 | 6 | {{series.label}} 7 | 9 | {{value}} / {{percent}} 10 |
    14 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/flot/table_legend.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{#each data}} 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{/each}} 27 | 28 |
     currentminmaxmeanmediansum
    17 | {{target}} 18 | {{format ',.3s' summation.last}}{{format ',.3s' summation.min}}{{format ',.3s' summation.max}}{{format ',.3s' summation.mean}}{{format ',.3s' summation.median}}{{format ',.3s' summation.sum}}
    29 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/flot/tooltip.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | {{#each items}} 9 | 10 | 14 | 17 | 18 | {{/each}} 19 | 20 |
    5 | {{moment "dd, M-D-YYYY, h:mm A" time}} 6 |
    11 | 12 | {{series.label}} 13 | 15 | {{value}} 16 |
    21 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/listing/dashboard_list.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#each dashboards}} 4 | {{> ds-dashboard-listing-entry}} 5 | {{else}} 6 | 7 | {{/each}} 8 | 9 |

    No dashboards defined

    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/listing/dashboard_tag_list.hbs: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/bar_chart.hbs: -------------------------------------------------------------------------------- 1 |
    3 | {{> ds-title-bar}} 4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/cell.hbs: -------------------------------------------------------------------------------- 1 |
    4 | {{> ds-edit-bar}} 5 | {{#if item.style}}
    {{/if}} 6 | 7 | {{#each item.items}} 8 | {{item this}} 9 | {{/each}} 10 | 11 | {{#if item.style}}
    {{/if}} 12 | 17 |
    18 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/comparison_summation_table.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
     {{#if item.query_other}}{{item.query_other.name}}{{/if}}{{#if item.query}}{{item.query.name}}{{/if}}Delta%
    16 |
    17 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/comparison_summation_table_body.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Average 4 | {{format item.format then.mean}} 5 | {{format item.format now.mean}} 6 | 7 | {{format item.format diff.mean}} 8 | 9 | 10 | {{diff.mean_pct}} 11 | 12 | 13 | 14 | Min 15 | {{format item.format then.min}} 16 | {{format item.format now.min}} 17 | 18 | {{format item.format diff.min}} 19 | 20 | 21 | {{diff.min_pct}} 22 | 23 | 24 | 25 | Max 26 | {{format item.format then.max}} 27 | {{format item.format now.max}} 28 | 29 | {{format item.format diff.max}} 30 | 31 | 32 | {{diff.max_pct}} 33 | 34 | 35 | 36 | Sum 37 | {{format item.format then.sum}} 38 | {{format item.format now.sum}} 39 | 40 | {{format item.format diff.sum}} 41 | 42 | 43 | {{diff.sum_pct}} 44 | 45 | 46 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/definition.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-edit-bar}} 3 | {{#each item.items}} 4 | {{item this}} 5 | {{/each}} 6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/discrete_bar_chart.hbs: -------------------------------------------------------------------------------- 1 |
    3 | {{> ds-title-bar}} 4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/donut_chart.hbs: -------------------------------------------------------------------------------- 1 |
    3 | {{> ds-title-bar}} 4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/heading.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{ds-edit-bar item}} 3 | 4 | {{item.text}} {{#if item.description}}{{item.description}}{{/if}} 5 | 6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/jumbotron_singlestat.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{ds-edit-bar item}} 3 |
    4 | 5 |
    6 |
    7 | 8 | {{item.units}} 9 |
    10 |
    11 |

    {{item.title}}

    12 |
    13 |
    14 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/markdown.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{ds-edit-bar item}} 3 | {{#if item.raw}} 4 |
    {{item.text}}
    5 | {{else}} 6 | {{#if item.expanded_text}}{{markdown item.expanded_text}}{{else}}{{markdown item.text}}{{/if}} 7 | {{/if}} 8 |
    9 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/percentage_table.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 |
    4 |

    No data

    5 |
    6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/percentage_table_data.hbs: -------------------------------------------------------------------------------- 1 | 3 | 4 | {{#if item.invert_axes}} 5 | 6 | 7 | 8 | 9 | {{#if item.include_sums}} 10 | 11 | {{/if}} 12 | 13 | 14 | 15 | 16 | {{#each query.data}} 17 | 18 | 19 | 20 | {{#if ../item.include_sums}} 21 | 22 | {{/if}} 23 | 24 | {{/each}} 25 | 26 | {{#if item.include_sums}} 27 | 28 | 29 | 30 | 31 | 32 | {{/if}} 33 | 34 | {{else}} 35 | 36 | 37 | {{#if item.include_sums}} 38 | 39 | 40 | {{/if}} 41 | {{#each query.data}} 42 | 43 | {{/each}} 44 | 45 | 46 | 47 | 48 | 49 | {{#if item.include_sums}} 50 | 51 | 52 | {{/if}} 53 | {{#each query.data}} 54 | 55 | {{/each}} 56 | 57 | 58 | {{#if item.include_sums}} 59 | 60 | 61 | 62 | {{#each query.data}} 63 | 64 | {{/each}} 65 | 66 | {{/if}} 67 | 68 | {{/if}} 69 | 70 |
     %{{item.transform}}
    {{target}}{{format ",.2%" summation.percent}}{{format ",.0f" summation.percent_value}}
    Total{{format ",.0f" query.summation.percent_value}}
     Total{{target}}
    %{{format ",.2%" summation.percent}}
    {{item.transform}}{{format item.format query.summation.percent_value}}{{format ../item.format summation.percent_value}}
    71 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/row.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#if item.style}}
    {{/if}} 3 | 4 | {{> ds-edit-bar}} 5 |
    6 | {{#each item.items}} 7 | {{item this}} 8 | {{/each}} 9 |
    10 | {{#if item.style}}
    {{/if}} 11 |
    12 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/scatter_plot.hbs: -------------------------------------------------------------------------------- 1 |
    3 | {{> ds-title-bar}} 4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/section.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-edit-bar}} 3 | {{#if item.style}}
    {{/if}} 4 |
    5 | {{#if item.title}} 6 | {{item.title}} 7 | {{#if item.description}}{{item.description}}{{/if}} 8 | 9 | {{/if}} 10 | {{#if item.horizontal_rule}} 11 |
    12 | {{/if}} 13 |
    14 | 15 | {{#each item.items}} 16 | {{item this}} 17 | {{/each}} 18 | 19 | {{#if item.style}}
    {{/if}} 20 |
    21 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/separator.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{ds-edit-bar item}} 3 |
    4 |
    5 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/simple_time_series.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 |
    4 | 5 |
    6 |
    7 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/singlegraph.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 | 4 |
    5 | 6 |
    7 | {{item.units}} 8 | 9 | {{#if item.display_transform}} 10 | {{item.transform}} 11 | {{/if}} 12 |
    13 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/singlegraph_grid.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 |
    4 |
    5 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/singlegraph_grid_item.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{#if item.style}}
    {{/if}} 4 | {{label}} 5 |
    6 | {{item.units}} 7 | {{value}} 8 | {{#if item.display_transform}}{{item.transform}}{{/if}} 9 | {{#if item.style}}
    {{/if}} 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/singlestat.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{ds-edit-bar item}} 3 |
    4 |

    {{item.title}}

    5 |

    {{item.units}}

    6 |
    7 |
    8 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/singlestat_grid.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 |
    4 |
    5 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/singlestat_grid_item.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{#if item.style}}
    {{/if}} 4 |

    {{label}}

    5 |

    {{value}}{{item.units}}

    6 | {{#if item.style}}
    {{/if}} 7 |
    8 | 9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/standard_time_series.hbs: -------------------------------------------------------------------------------- 1 |
    3 | {{> ds-title-bar}} 4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/summation_table.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 | 5 | 6 | 7 | {{#if item.show_current}}{{/if}} 8 | {{#if item.show_last_non_zero}}{{/if}} 9 | {{#if item.show_min}}{{/if}} 10 | {{#if item.show_max}}{{/if}} 11 | {{#if item.show_mean}}{{/if}} 12 | {{#if item.show_median}}{{/if}} 13 | {{#if item.show_sum}}{{/if}} 14 | {{#if item.show_sparkline}}{{/if}} 15 | 16 | 17 | 18 | 19 | {{#if item.show_current}}{{/if}} 20 | {{#if item.show_last_non_zero}}{{/if}} 21 | {{#if item.show_min}}{{/if}} 22 | {{#if item.show_max}}{{/if}} 23 | {{#if item.show_mean}}{{/if}} 24 | {{#if item.show_median}}{{/if}} 25 | {{#if item.show_sum}}{{/if}} 26 | {{#if item.show_sparkline}}{{/if}} 27 | 28 | 29 | 30 | 31 |
     currentLast > 0minmaxmeanmediansum 
    32 |
    33 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/summation_table_row.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#if item.show_color}} 4 | {{#unless item.show_sparkline}} 5 | 6 | {{/unless}} 7 | {{/if}} 8 | {{series.target}} 9 | 10 | {{#if item.show_current}}{{format item.format series.summation.last}}{{/if}} 11 | {{#if item.show_last_non_zero}}{{format item.format series.summation.last_non_zero}}{{/if}} 12 | {{#if item.show_min}}{{format item.format series.summation.min}}{{/if}} 13 | {{#if item.show_max}}{{format item.format series.summation.max}}{{/if}} 14 | {{#if item.show_mean}}{{format item.format series.summation.mean}}{{/if}} 15 | {{#if item.show_median}}{{format item.format series.summation.median}}{{/if}} 16 | {{#if item.show_sum}}{{format item.format series.summation.sum}}{{/if}} 17 | {{#if item.show_sparkline}} 18 | 19 |
    20 | 21 | {{/if}} 22 | 23 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/timerstat.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{ds-edit-bar item}} 3 |
    4 |

    {{item.title}}

    5 |

    6 |
    7 |
    8 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/timerstat_body.hbs: -------------------------------------------------------------------------------- 1 | {{#each parts}} 2 | {{value}}{{unit}} 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/timeshift_summation_table.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> ds-title-bar}} 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    Now{{item.shift}} AgoDelta%
    16 |
    17 | -------------------------------------------------------------------------------- /tessera-frontend/src/templates/models/timeshift_summation_table_body.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Average 4 | {{format item.format now.mean}} 5 | {{format item.format then.mean}} 6 | 7 | {{format item.format diff.mean}} 8 | 9 | 10 | {{diff.mean_pct}} 11 | 12 | 13 | 14 | Median 15 | {{format item.format now.median}} 16 | {{format item.format then.median}} 17 | 18 | {{format item.format diff.median}} 19 | 20 | 21 | {{diff.median_pct}} 22 | 23 | 24 | 25 | Min 26 | {{format item.format now.min}} 27 | {{format item.format then.min}} 28 | 29 | {{format item.format diff.min}} 30 | 31 | 32 | {{diff.min_pct}} 33 | 34 | 35 | 36 | Max 37 | {{format item.format now.max}} 38 | {{format item.format then.max}} 39 | 40 | {{format item.format diff.max}} 41 | 42 | 43 | {{diff.max_pct}} 44 | 45 | 46 | 47 | Sum 48 | {{format item.format now.sum}} 49 | {{format item.format then.sum}} 50 | 51 | {{format item.format diff.sum}} 52 | 53 | 54 | {{diff.sum_pct}} 55 | 56 | 57 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/config.ts: -------------------------------------------------------------------------------- 1 | export default class Config { 2 | 3 | PROPSHEET_AUTOCLOSE_SECONDS: number = 3 4 | APPLICATION_ROOT: string = null 5 | DEFAULT_FROM_TIME: string = '-3h' 6 | DISPLAY_TIMEZONE: string = 'Etc/UTC' 7 | GRAPHITE_AUTH: string = null 8 | GRAPHITE_URL: string = 'http://localhost:8080' 9 | 10 | constructor(data?: any) { 11 | if (data) { 12 | this.PROPSHEET_AUTOCLOSE_SECONDS = data.PROPSHEET_AUTOCLOSE_SECONDS || this.PROPSHEET_AUTOCLOSE_SECONDS 13 | this.APPLICATION_ROOT = data.APPLICATION_ROOT 14 | this.DEFAULT_FROM_TIME = data.DEFAULT_FROM_TIME || this.DEFAULT_FROM_TIME 15 | this.DISPLAY_TIMEZONE = data.DISPLAY_TIMEZONE || this.DISPLAY_TIMEZONE 16 | this.GRAPHITE_AUTH = data.GRAPHITE_AUTH 17 | this.GRAPHITE_URL = data.GRAPHITE_URL || this.GRAPHITE_URL 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/check-dirty.ts: -------------------------------------------------------------------------------- 1 | declare var ts 2 | 3 | window.onbeforeunload = (e) => { 4 | if (ts.manager.current && ts.manager.current.dashboard.dirty) { 5 | let msg = 'Dashboard has unsaved changes. Are you sure you want to leave?' 6 | e.returnValue = msg 7 | return msg 8 | } 9 | return null 10 | } 11 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/dashboard-create.ts: -------------------------------------------------------------------------------- 1 | import Dashboard from '../../models/dashboard' 2 | import manager from '../manager' 3 | import { make } from '../../models/items/factory' 4 | 5 | declare var $, window 6 | 7 | /* 8 | * Logic for the dashboard-create.html template. 9 | */ 10 | 11 | $(document).ready(function() { 12 | let tags = $('#ds-dashboard-tags') 13 | if (tags.length) { 14 | tags .tagsManager({ 15 | tagsContainer: '.ds-tag-holder', 16 | tagClass: 'badge badge-primary' 17 | }) 18 | } 19 | }) 20 | 21 | $(document).ready(function() { 22 | let form = $('#ds-dashboard-create-form') 23 | if (form.length) { 24 | form.bootstrapValidator() 25 | } 26 | }) 27 | 28 | $(document).on('click', '#ds-new-dashboard-create', function(e) { 29 | let title = $('#ds-dashboard-title')[0].value 30 | let category = $('#ds-dashboard-category')[0].value 31 | let summary = $('#ds-dashboard-summary')[0].value 32 | let description = $('#ds-dashboard-description')[0].value 33 | let tags = $('#ds-dashboard-tags').tagsManager('tags') 34 | 35 | if (!$('#ds-dashboard-create-form').data('bootstrapValidator').validate().isValid()) { 36 | return 37 | } 38 | 39 | let dashboard = new Dashboard({ 40 | title: title, 41 | category: category, 42 | summary: summary, 43 | description: description, 44 | tags: tags, 45 | definition: make('dashboard_definition') 46 | }) 47 | manager.create(dashboard, function(rsp) { 48 | window.location = rsp.data.view_href 49 | }) 50 | }) 51 | 52 | $(document).on('click', '#ds-new-dashboard-cancel', function(e) { 53 | window.history.back() 54 | }) 55 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/display-mode.ts: -------------------------------------------------------------------------------- 1 | import manager from '../manager' 2 | import * as app from '../app' 3 | 4 | declare var $ 5 | 6 | app.add_mode_handler('display', { 7 | enter: function() { 8 | /* Make sure the fullscreen range indicator is correct */ 9 | let range = app.context() 10 | let description = manager.getRangeDescription(range.from); 11 | if ( description ) { 12 | $("a.ds-fullscreen-range-indicator").text(description); 13 | } 14 | /* Update the header to match the dashboard if it's fluid */ 15 | let fluid = false 16 | manager.current.dashboard.definition.visit(function(item) { 17 | if (item.layout === 'fluid') 18 | fluid = true 19 | }) 20 | if (fluid) { 21 | $('.ds-header-container').removeClass('container') 22 | $('.ds-header-container').addClass('container-fluid') 23 | } 24 | }, 25 | exit: function() { 26 | $('.ds-header-container').removeClass('container-fluid') 27 | $('.ds-header-container').addClass('container') 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/menu-dashboard-actions.ts: -------------------------------------------------------------------------------- 1 | import * as util from '../../util' 2 | import Action, { actions } from '../../core/action' 3 | import manager from '../manager' 4 | import * as app from '../app' 5 | import * as core from '../../core' 6 | 7 | declare var $, URI, moment, window 8 | 9 | /** 10 | * Actions that operate on dashboards in the listing page, and the 11 | * handler to invoke them from the dashboard-list.html template. 12 | */ 13 | 14 | actions.register('dashboard-list-actions', [ 15 | new Action({ 16 | name: 'open', 17 | display: 'Open', 18 | icon: 'fa fa-external-link', 19 | handler: function(action, context) { 20 | window.location = context.view_href 21 | } 22 | }), 23 | new Action({ 24 | name: 'edit', 25 | display: 'Edit...', 26 | icon: 'fa fa-edit', 27 | handler: function(action, context) { 28 | let url = new URI(context.view_href).setQuery('mode', app.Mode.EDIT).href() 29 | window.location = url 30 | } 31 | }), 32 | new Action({ 33 | name: 'duplicate', 34 | display: 'Duplicate...', 35 | icon: 'fa fa-copy', 36 | handler: function(action, context) { 37 | manager.duplicate(context.href, function() { 38 | window.location.reload() 39 | }) 40 | } 41 | }), 42 | new Action({ 43 | name: 'export', 44 | display: 'Export...', 45 | icon: 'fa fa-download', 46 | handler: function(action, context) { 47 | manager.client.dashboard_get(context.href, { definition: true }) 48 | .then((data) => { 49 | let json = JSON.stringify(util.json(data), null, ' ') 50 | let blob = new Blob([json], { type: 'application/json;charset=utf-8' }) 51 | let now = moment().format() 52 | window.saveAs(blob, `${data.title} ${now}`) 53 | }) 54 | } 55 | }), 56 | Action.DIVIDER, 57 | new Action({ 58 | name: 'delete', 59 | display: 'Delete...', 60 | icon: 'fa fa-trash-o', 61 | handler: function(action, context) { 62 | manager.delete_with_confirmation(context.href, function() { 63 | $('tr[data-ds-href="' + context.href + '"]').remove() 64 | manager.success('Succesfully deleted dashboard ' + context.href) 65 | }) 66 | } 67 | }) 68 | ]) 69 | 70 | $(document).on('click', 'ul.ds-dashboard-action-menu li', function(event) { 71 | let element = $(event.target).parent().parent()[0] 72 | let context = { 73 | href: element.getAttribute('data-ds-href'), 74 | view_href: element.getAttribute('data-ds-view-href') 75 | } 76 | let action = actions.get('dashboard-list-actions', this.getAttribute('data-ds-action')) 77 | action.handler(action, context) 78 | }) 79 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/menu-dashboard-sort.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Handlers for the sort menu in the dashboard-list.html template.a 3 | */ 4 | 5 | declare var $, URI 6 | 7 | $(document).on('click', 'ul.ds-dashboard-sort-menu li', function(e) { 8 | var column = e.target.getAttribute('data-ds-sort-col') 9 | var order = e.target.getAttribute('data-ds-sort-order') 10 | var url = new URI(window.location) 11 | if (column) { 12 | url.setQuery('sort', column) 13 | } 14 | if (order) { 15 | url.setQuery('order', order) 16 | } 17 | window.location.href = url.href() 18 | }) 19 | 20 | 21 | $(document).ready(function() { 22 | var params = new URI(window.location).search(true) 23 | if (params.sort !== 'default' ) { 24 | $('ul.ds-dashboard-sort-menu li').removeClass('active') 25 | $('[data-ds-sort-col="' + params.sort + '"][data-ds-sort-order="' + (params.order || 'asc') + '"]') 26 | .parent() 27 | .addClass('active') 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/menu-presentation-actions.ts: -------------------------------------------------------------------------------- 1 | declare var ts, bootbox 2 | import manager from '../manager' 3 | import Action, { actions } from '../../core/action' 4 | import * as graphite from '../../charts/graphite' 5 | 6 | actions.register('presentation-actions', [ 7 | new Action({ 8 | name: 'view-query', 9 | display: 'View query...', 10 | icon: 'fa-question', 11 | handler: (action, item) => { 12 | let query = item.query_override || item.query 13 | let expr = query.targets[0] 14 | let source = `### Query: ${item.query.name} 15 | 16 | \`\`\` 17 | ${expr} 18 | \`\`\` 19 | ` 20 | bootbox.alert(ts.templates.edit.view_query({ source: source })) 21 | } 22 | }), 23 | new Action({ 24 | name: 'open-in-graphite', 25 | display: 'Open in Graphite...', 26 | icon: 'fa-bar-chart-o', 27 | handler: function(action, item) { 28 | let composer_url = graphite.composer_url(item, item.query_override || item.query, { 29 | showTitle: true 30 | }) 31 | window.open(composer_url.href()) 32 | } 33 | }), 34 | new Action({ 35 | name: 'export-png', 36 | display: 'Export PNG...', 37 | icon: 'fa-file-image-o', 38 | handler: function(action, item) { 39 | let image_url = graphite.chart_url(item, item.query_override || item.query, { 40 | showTitle: true 41 | }) 42 | window.open(image_url.href()) 43 | } 44 | }), 45 | new Action({ 46 | name: 'export-svg', 47 | display: 'Export SVG...', 48 | icon: 'fa-file-code-o', 49 | handler: function(action, item) { 50 | let image_url = graphite.chart_url(item, item.query_override || item.query, { 51 | showTitle: true, 52 | format: 'svg' 53 | }) 54 | window.open(image_url.href()) 55 | } 56 | }), 57 | new Action({ 58 | name: 'export-csv', 59 | display: 'Export CSV...', 60 | icon: 'fa-file-excel-o', 61 | handler: function(action, item) { 62 | let image_url = graphite.chart_url(item, item.query_override || item.query, { 63 | showTitle: true, 64 | format: 'csv' 65 | }) 66 | window.open(image_url.href()) 67 | } 68 | }) 69 | ]) 70 | 71 | $(document).on('click', 'ul.ds-action-menu li', function(event) { 72 | 73 | let presentation_id = $(this).parent().parent().parent().parent().parent()[0].id 74 | let item = manager.current.dashboard.get_item(presentation_id) 75 | 76 | let action = actions.get(this.getAttribute('data-ds-category'), this.getAttribute('data-ds-action')) 77 | action.handler(action, item) 78 | 79 | /* prevents resetting scroll position */ 80 | return false 81 | }) 82 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/menu-refresh.ts: -------------------------------------------------------------------------------- 1 | import manager from '../manager' 2 | 3 | $(document).on('click', 'button.ds-refresh-button', function(e) { 4 | manager.refresh() 5 | }) 6 | 7 | $(document).on('click', 'ul.ds-refresh-menu li', function(e) { 8 | let target = $(e.target).parent() 9 | if (target.attr('data-ds-range')) { 10 | let range = target.attr('data-ds-range') 11 | manager.set_time_range(range, null) 12 | $("ul.ds-refresh-menu li[data-ds-range]").removeClass('active') 13 | $("ul.ds-refresh-menu li[data-ds-range=" + range + "]").addClass('active') 14 | } else if (target.attr('data-ds-interval')) { 15 | let interval = target.attr('data-ds-interval') 16 | manager.set_refresh_interval(interval) 17 | $("ul.ds-refresh-menu li[data-ds-interval]").removeClass('active') 18 | $("ul.ds-refresh-menu li[data-ds-interval=" + interval + "]").addClass('active') 19 | } 20 | return false 21 | }) 22 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/menu-rejigger.ts: -------------------------------------------------------------------------------- 1 | import manager from '../manager' 2 | import * as app from '../app' 3 | import SimpleGrid from '../../models/transform/SimpleGrid' 4 | 5 | $(document).on('click', 'ul.ds-rejigger-menu li a', function(e) { 6 | let target = $(e.target).parent() 7 | let cols = target.attr('data-ds-cols') 8 | let section_type = target.attr('data-ds-section-type') 9 | 10 | let layout = new SimpleGrid({ 11 | section_type: section_type, 12 | columns: cols 13 | }) 14 | 15 | manager.apply_transform(layout, manager.current.dashboard, false) 16 | app.refresh_mode() 17 | return false 18 | }) 19 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/handlers/menu-theme.ts: -------------------------------------------------------------------------------- 1 | declare var URI, window 2 | 3 | import manager from '../manager' 4 | import * as app from '../app' 5 | import SimpleGrid from '../../models/transform/SimpleGrid' 6 | 7 | $(document).on('click', 'ul.ds-theme-menu li a', function(e) { 8 | let target = $(e.target).parent() 9 | let theme = target.attr('data-ds-theme-name') 10 | 11 | let u = new URI(window.location) 12 | u.setSearch('theme', theme) 13 | window.location = u.toString() 14 | 15 | 16 | app.refresh_mode() 17 | return false 18 | }) 19 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Event, Mode, Application, 3 | instance, uri, context, config, 4 | refresh_mode, switch_to_mode, toggle_mode, add_mode_handler 5 | } from './app' 6 | 7 | export { 8 | default as manager 9 | } from './manager' 10 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/app/keybindings.ts: -------------------------------------------------------------------------------- 1 | import * as app from './app' 2 | import manager from './manager' 3 | 4 | declare var Mousetrap 5 | 6 | /* ============================================================================= 7 | Keyboard Shortcuts 8 | ============================================================================= */ 9 | 10 | Mousetrap.bind('ctrl+shift+d', function(e) { 11 | app.toggle_mode(app.Mode.DISPLAY) 12 | }) 13 | 14 | Mousetrap.bind('ctrl+shift+e', function(e) { 15 | app.toggle_mode(app.Mode.EDIT) 16 | }) 17 | 18 | Mousetrap.bind('ctrl+shift+s', function(e) { 19 | manager.update_definition(manager.current.dashboard, function() { 20 | manager.success('Dashboard saved') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/charts/flot.tickformat.ts: -------------------------------------------------------------------------------- 1 | import * as app from '../app/app' 2 | 3 | declare var $, moment 4 | 5 | function tick_formatter(value, axis) { 6 | // Take care of time series axis 7 | if (axis.tickSize && axis.tickSize.length === 2) { 8 | if (axis.tickSize[1] === 'year' && axis.tickSize[0] >= 1) 9 | return moment(value).tz(app.config.DISPLAY_TIMEZONE).format('YYYY') 10 | if (axis.tickSize[1] === 'month' && axis.tickSize[0] >= 1 || axis.tickSize[1] === 'year') 11 | return moment(value).tz(app.config.DISPLAY_TIMEZONE).format('MM-\'YY') 12 | if (axis.tickSize[1] === 'day' && axis.tickSize[0] >= 1 || axis.tickSize[1] === 'month') 13 | return moment(value).tz(app.config.DISPLAY_TIMEZONE).format('MM/DD') 14 | if (axis.tickSize[1] === 'hour' && axis.tickSize[0] >= 12) 15 | return moment(value).tz(app.config.DISPLAY_TIMEZONE).format('MM/DD hA') 16 | } 17 | return moment(value).tz(app.config.DISPLAY_TIMEZONE).format('h:mm A') 18 | } 19 | 20 | function time_format_init(plot) { 21 | plot.hooks.processOptions.push(function(plot, options) { 22 | $.each(plot.getAxes(), function(axisName, axis) { 23 | let opts = axis.options 24 | if (opts.mode == "time") { 25 | axis.tickFormatter = tick_formatter 26 | } 27 | }) 28 | }) 29 | } 30 | 31 | $.plot.plugins.push({ 32 | init: time_format_init, 33 | options: {}, 34 | name: 'tessera-time-format', 35 | version: '1.0', 36 | }) 37 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/charts/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Chart 3 | } from '../models/items/chart' 4 | 5 | export { 6 | ChartRenderer, get_renderer, set_renderer, renderers, 7 | StackMode, 8 | simple_line_chart, standard_line_chart, simple_area_chart, stacked_area_chart, 9 | donut_chart, bar_chart, discrete_bar_chart, scatter_plot, 10 | process_series, process_data, 11 | cleanup 12 | } from './core' 13 | 14 | export { 15 | default as FlotChartRenderer 16 | } from './flot' 17 | 18 | export { 19 | composer_url, chart_url, default as GraphiteChartRenderer 20 | } from './graphite' 21 | 22 | export { 23 | default as PlaceholderChartRenderer 24 | } from './placeholder' 25 | 26 | export { 27 | default as palettes 28 | } from './palettes' 29 | 30 | export { 31 | get_palette, get_low_contrast_palette 32 | } from './util' 33 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/charts/legend.ts: -------------------------------------------------------------------------------- 1 | import Chart, { ChartLegendType } from '../models/items/chart' 2 | import Query from '../models/data/query' 3 | import { get_colors, get_palette } from './util' 4 | import { get_renderer } from './core' 5 | 6 | declare var $, ts 7 | 8 | function highlightSeries(item) { 9 | return function(e) { 10 | var index = e.currentTarget.dataset.seriesIndex 11 | get_renderer(item).highlight_series(item, index) 12 | } 13 | } 14 | 15 | function unhighlightSeries(item) { 16 | return function(e) { 17 | get_renderer(item).unhighlight_series(item) 18 | } 19 | } 20 | 21 | export function render_legend(item: Chart, query: Query, options?: any) { 22 | let legend_id = '#ds-legend-' + item.item_id 23 | if ( item.legend === ChartLegendType.SIMPLE ) { 24 | render_simple_legend(legend_id, item, query, options) 25 | } else if ( item.legend === ChartLegendType.TABLE ) { 26 | render_table_legend(legend_id, item, query, options) 27 | } 28 | } 29 | 30 | function render_simple_legend(legend_id: string, item: Chart, query: Query, options: any = {}) { 31 | let legend = '' 32 | let data = query.chart_data('flot') 33 | let theme_colors = get_colors() 34 | let colors = get_palette(options.palette || item.options.palette) 35 | for (let i = 0; i < data.length; i++) { 36 | let series = data[i] 37 | if (item.hide_zero_series && series.summation.sum === 0) { 38 | continue 39 | } 40 | let label = series.label 41 | let color = colors[i % colors.length] 42 | 43 | let cell = '
    ' 44 | + '' 45 | + '' + label + '' 46 | + '
    ' 47 | legend += cell 48 | } 49 | let elt = $(legend_id) 50 | elt.html(legend) 51 | elt.equalize({equalize: 'outerWidth', reset: true }) 52 | if (options.interactive_legend) { 53 | let elt = $(legend_id + ' .ds-legend-cell') 54 | elt.on('mouseenter', highlightSeries(item)) 55 | elt.on('mouseenter', (e) => { 56 | $(e.currentTarget).addClass('highlighted') 57 | }) 58 | elt.on('mouseleave', unhighlightSeries(item)) 59 | elt.on('mouseleave', (e) => { 60 | $(e.currentTarget).removeClass('highlighted') 61 | }) 62 | } 63 | } 64 | 65 | function render_table_legend(legend_id: string, item: Chart, query: Query, options: any = {}) { 66 | let palette = get_palette(item.options.palette || options.palette) 67 | query.data.forEach((series, i) => { 68 | series['color'] = palette[i % palette.length] 69 | }) 70 | let data = query.data 71 | if (item.hide_zero_series) 72 | data = data.filter(s => s.summation.sum != 0) 73 | let legend = ts.templates.flot.table_legend({ 74 | item: item, 75 | data: data, 76 | opt: options 77 | }) 78 | $(legend_id).html(legend) 79 | 80 | if (options.interactive_legend) { 81 | $(legend_id + ' > table > tbody > tr').on('mouseenter', highlightSeries(item)) 82 | $(legend_id + ' > table > tbody > tr').on('mouseleave', unhighlightSeries(item)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/charts/placeholder.ts: -------------------------------------------------------------------------------- 1 | import * as charts from './core' 2 | import Chart from '../models/items/chart' 3 | import Query from '../models/data/query' 4 | import { extend } from '../util' 5 | 6 | declare var $ 7 | 8 | function render(element, item) { 9 | element.html($('') 10 | .attr('data-src', 'holder.js/100px100p') 11 | .height(element.height()) 12 | .width(element.width())) 13 | } 14 | 15 | /** 16 | * A chart renderer that uses Holder.js to render placeholder images. 17 | */ 18 | export default class PlaceholderChartRenderer extends charts.ChartRenderer { 19 | 20 | constructor(data?: any) { 21 | super(extend({}, data, { 22 | name: 'placeholder', 23 | description: 'Render placeholder images in place of actual charts.', 24 | is_interactive: false 25 | })) 26 | } 27 | 28 | simple_line_chart(element: any, item: Chart, query: Query) : void { 29 | render(element, item) 30 | } 31 | 32 | standard_line_chart(element: any, item: Chart, query: Query) : void { 33 | render(element, item) 34 | } 35 | 36 | simple_area_chart(element: any, item: Chart, query: Query) : void { 37 | render(element, item) 38 | } 39 | 40 | stacked_area_chart(element: any, item: Chart, query: Query) : void { 41 | render(element, item) 42 | } 43 | 44 | donut_chart(element: any, item: Chart, query: Query) : void { 45 | render(element, item) 46 | } 47 | 48 | bar_chart(element: any, item: Chart, query: Query) : void { 49 | render(element, item) 50 | } 51 | 52 | discrete_bar_chart(element: any, item: Chart, query: Query) : void { 53 | render(element, item) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/charts/util.ts: -------------------------------------------------------------------------------- 1 | import PALETTES from './palettes' 2 | 3 | declare var $, Color 4 | 5 | export const DEFAULT_PALETTE = 'd3category20' 6 | 7 | export function get_palette(name_or_palette?: string|string[]) : string[] { 8 | if (name_or_palette instanceof Array) { 9 | return name_or_palette 10 | } else if (typeof name_or_palette === 'string') { 11 | return PALETTES[name_or_palette] || PALETTES[DEFAULT_PALETTE] 12 | } else { 13 | return PALETTES[DEFAULT_PALETTE] 14 | } 15 | } 16 | 17 | /** 18 | * Return a set of colors for rendering graphs that are tuned to 19 | * the current UI color theme. Colors are derived from the 20 | * background color of the 'body' element. 21 | * 22 | * TODO: cache the results keyed by background color. 23 | * 24 | * TODO: if the model had back links to containers, we could walk 25 | * up the containment hierarchy to see if the chart is contained 26 | * in something that has a background style set (i.e. well, alert, 27 | * etc...) and get the colors based on that. 28 | * 29 | * Or we could just pre-compute them all based on the background 30 | * colors from the CSS. 31 | */ 32 | export function get_colors() { 33 | let color = Color(window.getComputedStyle($('body')[0]).backgroundColor) 34 | if (color.isDark()) { 35 | return { 36 | majorGridLineColor: color.lighten(0.75).hex(), 37 | minorGridLineColor: color.lighten(0.5).hex(), 38 | fgcolor: color.lighten(3.0).hex() 39 | } 40 | } else { 41 | return { 42 | majorGridLineColor: color.darken(0.15).hex(), 43 | minorGridLineColor: color.darken(0.05).hex(), 44 | fgcolor: color.darken(0.75).hex() 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Return a low contrast monochromatic color palette for 51 | * transforms like HighlightAverage, which de-emphasize a mass of 52 | * raw metrics in order to highlight computed series. 53 | */ 54 | export function get_low_contrast_palette() { 55 | /* TODO: get from options parameter */ 56 | let light_step = 0.1 57 | , dark_step = 0.05 58 | , count = 6 59 | , bg = Color(window.getComputedStyle($('body')[0]).backgroundColor) 60 | , color = bg.isDark() ? bg.lighten(0.25) : bg.darken(0.1) 61 | 62 | let palette = [] 63 | for (var i = 0; i < count; i++) { 64 | palette.push(color.hex()) 65 | bg.isDark() ? color.lighten(light_step) : color.darken(dark_step) 66 | } 67 | 68 | return palette 69 | } 70 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/core/action.ts: -------------------------------------------------------------------------------- 1 | import { NamedObject, Registry } from '../util' 2 | 3 | export type ActionList = Action[] 4 | 5 | /** 6 | * Type of a function which returns an action list, intended for late 7 | * binding lists of sub-actions by looking up categories in the 8 | * actions registry at runtime. 9 | * 10 | * @see actions 11 | */ 12 | export interface ActionListFunction { 13 | () : ActionList 14 | } 15 | 16 | /** 17 | * An object describing a user-interface action. Actions may be 18 | * rendered as either a menu item in a dropdown or a button in a 19 | * button bar. 20 | */ 21 | export default class Action implements NamedObject { 22 | 23 | /** NamedObject - identity of the action */ 24 | name: string 25 | 26 | /** NamedObject - category to register the action under */ 27 | category: string 28 | 29 | /* Display name of the action */ 30 | display: string 31 | 32 | /** A CSS icon class string to identify this action visually */ 33 | icon: string 34 | 35 | /** Application mode to show this action in. */ 36 | show: string 37 | 38 | /** Application mode to hide this action in. */ 39 | hide: string 40 | 41 | /** Additional CSS classes to apply to the rendered element */ 42 | css: string 43 | 44 | /* Is this even used? */ 45 | options: any 46 | 47 | /** A callback that is run when the action is invoked. */ 48 | handler: (action: Action, data: any) => void 49 | 50 | /** If true, just render a divider */ 51 | divider: boolean 52 | 53 | /** A list of additional sub-actions, which will cause the action to 54 | * be rendered as a dropdown button or a sub-menu. This can be 55 | * supplied as an immediate list of actions, a string naming an 56 | * action category, or a function returning a list of actions. */ 57 | private _actions: string|ActionList|ActionListFunction 58 | 59 | static DIVIDER = new Action({divider: true, name: 'DIVIDER'}) 60 | 61 | constructor(data?: any) { 62 | if (data) { 63 | this.name = data.name 64 | this.display = data.display 65 | this.icon = data.icon 66 | this.options = data.options 67 | this.handler = data.handler 68 | this.show = data.show 69 | this.hide = data.hide 70 | this.divider = data.divider 71 | this.css = data.css 72 | this._actions = data.actions 73 | this.category = data.category 74 | } 75 | } 76 | 77 | get actions() : ActionList { 78 | if (typeof this._actions === 'undefined' ) { 79 | return undefined 80 | 81 | } else if (typeof this._actions === 'string') { 82 | return actions.list(this._actions) 83 | 84 | } else if (this._actions instanceof Array) { 85 | return this._actions 86 | 87 | } else { 88 | let fn = this._actions 89 | return fn() 90 | } 91 | } 92 | } 93 | 94 | export const actions = new Registry({ 95 | name: 'actions', 96 | process: function(data) : Action { 97 | if (data instanceof Action) 98 | return data 99 | return new Action(data) 100 | } 101 | }) 102 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/core/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ActionList, actions, default as Action 3 | } from './action' 4 | 5 | export { 6 | PropertyList, properties, property, default as Property 7 | } from './property' 8 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/data/graphite.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A TypeScript description of the graphite-web JSON data format. 3 | * 4 | * @see http://graphite.readthedocs.org/en/latest/render_api.html#data-display-formats 5 | */ 6 | 7 | import Summation from '../models/data/summation' 8 | 9 | /** 10 | * Each individual datapoint is a tuple of [value, timestamp]. 11 | */ 12 | export type Datapoint = [number, number] 13 | 14 | export type DatapointList = Datapoint[] 15 | 16 | export interface DataSeries { 17 | target: string 18 | datapoints: DatapointList 19 | summation?: Summation 20 | } 21 | 22 | export type DataSeriesList = DataSeries[] 23 | 24 | /** 25 | * Parse Graphite's `raw` data format, which is a more compact 26 | * on-the-wire representation than JSON. 27 | * 28 | * ``` 29 | * Targets are output one per line and are of the format ,,,|[data]* 31 | * ``` 32 | */ 33 | export function parse_raw(raw: string) : DataSeriesList { 34 | let lines : string[] = raw.split(/\r?\n/) 35 | return lines.map((line) : DataSeries => { 36 | return parse_raw_line(line) 37 | }) 38 | } 39 | 40 | /** 41 | * Parse a single line of Graphite's `raw` data format, which 42 | * represents a single data series. 43 | */ 44 | export function parse_raw_line(line: string) : DataSeries { 45 | let [meta_string, values_string] = line.split('|') 46 | let [target, _start, _end, _step] = meta_string.split(',') 47 | let start = Number(_start) 48 | let end = Number(_end) 49 | let step = Number(_step) 50 | 51 | let series : DataSeries = { 52 | target: target, 53 | datapoints: [] 54 | } 55 | 56 | let values = values_string.split(',') 57 | let timestamp = start 58 | 59 | for (let v of values) { 60 | let value = Number(v) 61 | if (Number.isNaN(value)) { 62 | value = null 63 | } 64 | series.datapoints.push([value, timestamp]) 65 | timestamp += step 66 | } 67 | 68 | return series 69 | } 70 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/importer/index.ts: -------------------------------------------------------------------------------- 1 | import * as _graphite from './graphite' 2 | export const graphite = _graphite 3 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/index.ts: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | 3 | import { logger, extend } from './util' 4 | import * as core from './core' 5 | import * as charts from './charts/core' 6 | import * as factory from './models/items/factory' 7 | import * as app from './app/app' 8 | import * as edit from './edit/edit' 9 | import * as importer from './importer' 10 | import Client from './client' 11 | import { actions } from './core/action' 12 | import { transforms } from './models/transform/transform' 13 | import User from './models/user' 14 | import manager from './app/manager' 15 | import Config from './app/config' 16 | import GraphiteChartRenderer from './charts/graphite' 17 | import FlotChartRenderer from './charts/flot' 18 | import PlaceholderChartRenderer from './charts/placeholder' 19 | import { register_helpers } from './app/helpers' 20 | import { register_dashboard_item } from './models/items/factory' 21 | import * as items from './models/items' 22 | 23 | declare var window, $ 24 | 25 | var log = logger('main') 26 | 27 | window.ts.init = function() { 28 | let config = window.ts.config 29 | 30 | extend(window.ts, { 31 | core: core, 32 | app: app, 33 | manager: manager, 34 | charts: charts, 35 | factory: factory, 36 | actions: actions, 37 | edit: edit, 38 | transforms: transforms, 39 | importer: importer, 40 | user: new User() 41 | }) 42 | app.set_config(window.ts.config) 43 | 44 | /* Set up the API client */ 45 | window.ts.client 46 | = manager.client 47 | = new Client({ prefix: config.APPLICATION_ROOT }) 48 | 49 | /* Register all dashboard items */ 50 | register_dashboard_item(items.DashboardDefinition) 51 | register_dashboard_item(items.Section) 52 | register_dashboard_item(items.Row) 53 | register_dashboard_item(items.Cell) 54 | register_dashboard_item(items.Markdown) 55 | register_dashboard_item(items.Heading) 56 | register_dashboard_item(items.Separator) 57 | register_dashboard_item(items.SummationTable) 58 | register_dashboard_item(items.PercentageTable) 59 | register_dashboard_item(items.TimeshiftSummationTable) 60 | register_dashboard_item(items.ComparisonSummationTable) 61 | register_dashboard_item(items.Singlestat) 62 | register_dashboard_item(items.SinglestatGrid) 63 | register_dashboard_item(items.TimeshiftSinglestat) 64 | register_dashboard_item(items.ComparisonSinglestat) 65 | register_dashboard_item(items.JumbotronSinglestat) 66 | register_dashboard_item(items.TimeshiftJumbotronSinglestat) 67 | register_dashboard_item(items.ComparisonJumbotronSinglestat) 68 | register_dashboard_item(items.Timerstat) 69 | register_dashboard_item(items.DonutChart) 70 | register_dashboard_item(items.BarChart) 71 | register_dashboard_item(items.DiscreteBarChart) 72 | register_dashboard_item(items.SimpleTimeSeries) 73 | register_dashboard_item(items.StandardTimeSeries) 74 | register_dashboard_item(items.Singlegraph) 75 | register_dashboard_item(items.SinglegraphGrid) 76 | register_dashboard_item(items.ScatterPlot) 77 | 78 | /* Register Handlebars helper functions */ 79 | register_helpers() 80 | 81 | /* Register chart renderers */ 82 | charts.renderers.register(new GraphiteChartRenderer({ 83 | graphite_url: config.GRAPHITE_URL 84 | })) 85 | charts.renderers.register(new FlotChartRenderer()) 86 | } 87 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/axis.ts: -------------------------------------------------------------------------------- 1 | import Model from './model' 2 | 3 | export const AxisScale = { 4 | LINEAR: 'linear', 5 | LOG: 'log' 6 | } 7 | 8 | export default class Axis extends Model { 9 | visible: boolean 10 | label: string 11 | label_distance: number 12 | format: string 13 | min: number 14 | max: number 15 | scale: string 16 | 17 | constructor(data?: any) { 18 | super(data) 19 | if (data) { 20 | this.visible = data.visible 21 | this.label = data.label 22 | this.label_distance = data.label_distance 23 | this.format = data.format 24 | this.min = data.min 25 | this.max = data.max 26 | this.scale = data.scale 27 | } 28 | } 29 | 30 | toJSON() : any { 31 | return { 32 | visible: this.visible, 33 | label: this.label, 34 | label_distance: this.label_distance, 35 | format: this.format, 36 | min: this.min, 37 | max: this.max, 38 | scale: this.scale 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Model } from './model' 2 | export { AxisScale, default as Axis } from './axis' 3 | export { default as Thresholds } from './thresholds' 4 | export { default as Tag } from './tag' 5 | export { default as Dashboard, DashboardTuple } from './dashboard' 6 | 7 | export { default as Query } from './data/query' 8 | export { default as Summation } from './data/summation' 9 | export { default as Preferences } from './preferences' 10 | 11 | export * from './items' 12 | 13 | export interface DashboardCategory { 14 | name: string 15 | count: number 16 | } 17 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | DashboardItemStyle as ItemStyle, 4 | Transform, 5 | default as DashboardItem 6 | } from './items/item' 7 | 8 | export { default as DashboardDefinition } from './items/dashboard_definition' 9 | 10 | // Structural 11 | export { default as Container } from './items/container' 12 | export { default as Section } from './items/section' 13 | export { default as Row } from './items/row' 14 | export { default as Cell } from './items/cell' 15 | 16 | // Informational 17 | export { default as Markdown } from './items/markdown' 18 | export { default as Heading } from './items/heading' 19 | export { default as Separator } from './items/separator' 20 | 21 | // Presentation base classes 22 | export { default as Presentation } from './items/presentation' 23 | export { default as TablePresentation } from './items/table_presentation' 24 | 25 | // Text 26 | export { default as SummationTable } from './items/summation_table' 27 | export { default as PercentageTable } from './items/percentage_table' 28 | export { default as TimeshiftSummationTable } from './items/timeshift_summation_table' 29 | export { default as ComparisonSummationTable } from './items/comparison_summation_table' 30 | export { default as Singlestat } from './items/singlestat' 31 | export { default as SinglestatGrid } from './items/singlestat_grid' 32 | export { default as TimeshiftSinglestat } from './items/timeshift_singlestat' 33 | export { default as ComparisonSinglestat } from './items/comparison_singlestat' 34 | export { default as JumbotronSinglestat } from './items/jumbotron_singlestat' 35 | export { default as TimeshiftJumbotronSinglestat } from './items/timeshift_jumbotron_singlestat' 36 | export { default as ComparisonJumbotronSinglestat } from './items/comparison_jumbotron_singlestat' 37 | export { default as Timerstat } from './items/timerstat' 38 | 39 | // Charts 40 | export { default as Chart, ChartLegendType } from './items/chart' 41 | export { default as DonutChart } from './items/donut_chart' 42 | export { default as BarChart } from './items/bar_chart' 43 | export { default as DiscreteBarChart } from './items/discrete_bar_chart' 44 | export { default as SimpleTimeSeries } from './items/simple_time_series' 45 | export { default as StandardTimeSeries } from './items/standard_time_series' 46 | export { default as Singlegraph } from './items/singlegraph' 47 | export { default as SinglegraphGrid } from './items/singlegraph_grid' 48 | export { default as ScatterPlot } from './items/scatter_plot' 49 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/bar_chart.ts: -------------------------------------------------------------------------------- 1 | import XYChart from './xychart' 2 | import Query from '../data/query' 3 | import { DashboardItemMetadata } from './item' 4 | import * as charts from '../../charts/core' 5 | import { extend } from '../../util' 6 | import { PropertyList } from '../../core/property' 7 | 8 | declare var $ 9 | 10 | export default class BarChart extends XYChart { 11 | static meta: DashboardItemMetadata = { 12 | icon: 'fa fa-bar-chart' 13 | } 14 | 15 | stack_mode: string = charts.StackMode.NORMAL 16 | 17 | constructor(data?: any) { 18 | super(data) 19 | if (data) { 20 | this.stack_mode = data.stack_mode || this.stack_mode 21 | } 22 | } 23 | 24 | toJSON() : any { 25 | return extend(super.toJSON(), { 26 | stack_mode: this.stack_mode 27 | }) 28 | } 29 | 30 | data_handler(query: Query) : void { 31 | charts.bar_chart(`#${this.item_id} .ds-graph-holder`, this, query) 32 | } 33 | 34 | interactive_properties() : PropertyList { 35 | return super.interactive_properties().concat([ 36 | { 37 | name: 'stack_mode', 38 | type: 'select', 39 | edit_options: { 40 | source: [ 41 | charts.StackMode.NONE, 42 | charts.StackMode.NORMAL, 43 | charts.StackMode.PERCENT, 44 | charts.StackMode.STREAM 45 | ] 46 | } 47 | } 48 | ]) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/cell.ts: -------------------------------------------------------------------------------- 1 | import Container from './container' 2 | import { DashboardItemMetadata } from './item' 3 | import { extend } from '../../util' 4 | import { PropertyList } from '../../core/property' 5 | 6 | export default class Cell extends Container { 7 | static meta: DashboardItemMetadata = { 8 | category: 'structural' 9 | } 10 | 11 | span: number = 3 12 | offset: number 13 | align: string 14 | 15 | constructor(data?: any) { 16 | super(data) 17 | if (data) { 18 | this.span = data.span || this.span 19 | this.offset = data.offset 20 | this.align = data.align 21 | } 22 | } 23 | 24 | set_span(value: number) : Cell { 25 | this.span = value 26 | return this.updated() 27 | } 28 | 29 | set_offset(value: number) : Cell { 30 | this.offset = value 31 | return this.updated() 32 | } 33 | 34 | set_align(value: string) : Cell { 35 | this.align = value 36 | return this.updated() 37 | } 38 | 39 | toJSON() : any { 40 | return extend(super.toJSON(), { 41 | span: this.span, 42 | offset: this.offset, 43 | align: this.align 44 | }) 45 | } 46 | 47 | interactive_properties() : PropertyList { 48 | return super.interactive_properties().concat([ 49 | 'style', 50 | { 51 | name: 'span', 52 | edit_options: { 53 | type: 'select', 54 | source: [ 55 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 56 | ] 57 | } 58 | }, 59 | { 60 | name: 'offset', 61 | edit_options: { 62 | type: 'select', 63 | source: [ 64 | { value: undefined, text: 'none' }, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 65 | ] 66 | } 67 | }, 68 | { 69 | name: 'align', 70 | type: 'select', 71 | edit_options: { 72 | source: [ 73 | undefined, 74 | 'left', 75 | 'center', 76 | 'right' 77 | ] 78 | } 79 | } 80 | ]) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/comparison_jumbotron_singlestat.ts: -------------------------------------------------------------------------------- 1 | import ComparisonSinglestat from './comparison_singlestat' 2 | 3 | declare var ts 4 | 5 | export default class ComparisonJumbotronSinglestat extends ComparisonSinglestat { 6 | static meta = { 7 | template: ts.templates.models.jumbotron_singlestat 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/comparison_singlestat.ts: -------------------------------------------------------------------------------- 1 | import { logger, extend } from '../../util' 2 | import { PropertyList } from '../../core' 3 | import * as app from '../../app' 4 | import Singlestat from './singlestat' 5 | import Query from '../data/query' 6 | 7 | declare var $, d3, ts 8 | 9 | const log = logger('models.comparison_singlestat') 10 | const FORMAT_PERCENT = d3.format(',.1%') 11 | 12 | export default class ComparisonSinglestat extends Singlestat { 13 | static meta = { 14 | template: ts.templates.models.singlestat 15 | } 16 | 17 | private _query_other: string 18 | percent : boolean = false 19 | 20 | constructor(data?: any) { 21 | super(data) 22 | if (data) { 23 | if (this.query_other instanceof Query) { 24 | this._query_other = data.query_other.name 25 | } else { 26 | this._query_other = data.query_other 27 | } 28 | this.percent = !!data.percent 29 | } 30 | } 31 | 32 | _update_query() : void { 33 | if (this._query && this.query_other && this.dashboard) { 34 | let query = this.dashboard.definition.queries[this._query] 35 | this.query_override = 36 | query.join(this.query_other).set_name(`${this.item_id}_joined`) 37 | this.query_override.render_templates(app.context().variables) 38 | } 39 | } 40 | 41 | get query_other() : Query { 42 | if (!this.dashboard) { 43 | return null 44 | } 45 | return this.dashboard.definition.queries[this._query_other] 46 | } 47 | 48 | set query_other(value: Query) { 49 | this._query_other = value.name 50 | } 51 | 52 | set_query_other(value: string|Query) : ComparisonSinglestat { 53 | log.info(`set_query_other: ${value}`) 54 | if (typeof value === 'string') { 55 | this._query_other = value 56 | } else { 57 | this._query_other = value.name 58 | } 59 | return this.updated() 60 | } 61 | 62 | toJSON() : any { 63 | return extend(super.toJSON(), { 64 | query_other: this._query_other, 65 | percent: this.percent 66 | }) 67 | } 68 | 69 | render() : string { 70 | this._update_query() 71 | return super.render() 72 | } 73 | 74 | data_handler(query: Query) : void { 75 | if (query.data.length < 2) 76 | return 77 | let value = query.data[0].summation[this.transform] 78 | let base = query.data[1].summation[this.transform] 79 | let diff = value - base 80 | let pct = (value / base) - 1 81 | let float_margin = 0.000001 82 | let diff_elt = $(`#${this.item_id} span.diff`) 83 | 84 | $(`#${this.item_id} span.value`).text(d3.format(this.format)(value)) 85 | 86 | if (diff > float_margin) 87 | diff_elt.addClass('ds-diff-plus') 88 | else if (diff < float_margin) 89 | diff_elt.addClass('ds-diff-minus') 90 | 91 | let diff_formatted = this.percent 92 | ? FORMAT_PERCENT(Math.abs(pct)) 93 | : d3.format(this.format)(Math.abs(diff)) 94 | $(`#${this.item_id} span.diff`).text(diff_formatted) 95 | 96 | } 97 | 98 | interactive_properties(): PropertyList { 99 | return super.interactive_properties().concat([ 100 | 'query_other', 101 | { name: 'percent', type: 'boolean' } 102 | ]) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/discrete_bar_chart.ts: -------------------------------------------------------------------------------- 1 | import Chart from './chart' 2 | import Query from '../data/query' 3 | import * as charts from '../../charts/core' 4 | import { DashboardItemMetadata } from './item' 5 | import { extend } from '../../util' 6 | import { PropertyList } from '../../core/property' 7 | 8 | declare var $ 9 | 10 | export default class DiscreteBarChart extends Chart { 11 | static meta: DashboardItemMetadata = { 12 | display_name: 'Bar Chart (Discrete)', 13 | icon: 'fa fa-bar-chart' 14 | } 15 | 16 | transform: string = 'sum' 17 | orientation: string = 'vertical' 18 | format: string = ',.3s' 19 | show_grid: boolean = true 20 | show_numbers: boolean = true 21 | 22 | constructor(data?: any) { 23 | super(data) 24 | if (data) { 25 | this.legend = undefined 26 | this.transform = data.transform || this.transform 27 | this.orientation = data.orientation || this.orientation 28 | this.format = data.format || this.format 29 | if (typeof(data.show_grid) !== 'undefined') { 30 | this.show_grid = Boolean(data.show_grid) 31 | } 32 | if (typeof(data.show_numbers) !== 'undefined') { 33 | this.show_numbers = Boolean(data.show_numbers) 34 | } 35 | } 36 | } 37 | 38 | toJSON() : any { 39 | return extend(super.toJSON(), { 40 | orientation: this.orientation, 41 | transform: this.transform, 42 | format: this.format, 43 | show_grid: this.show_grid, 44 | show_numbers: this.show_numbers 45 | }) 46 | } 47 | 48 | data_handler(query: Query) : void { 49 | charts.discrete_bar_chart(`#${this.item_id} .ds-graph-holder`, this, query) 50 | } 51 | 52 | interactive_properties() : PropertyList { 53 | return super.interactive_properties().concat([ 54 | 'transform', 55 | 'format', 56 | 'chart.y-axis-label', 57 | { name: 'show_grid', type: 'boolean' }, 58 | { name: 'show_numbers', type: 'boolean' }, 59 | { 60 | name: 'orientation', 61 | type: 'select', 62 | edit_options: { 63 | source: [ 64 | 'horizontal', 65 | 'vertical' 66 | ] 67 | } 68 | } 69 | ]) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/donut_chart.ts: -------------------------------------------------------------------------------- 1 | import Chart from './chart' 2 | import Query from '../data/query' 3 | import { DashboardItemMetadata } from './item' 4 | import * as charts from '../../charts/core' 5 | import { extend } from '../../util' 6 | import { PropertyList } from '../../core/property' 7 | 8 | declare var $ 9 | 10 | export default class DonutChart extends Chart { 11 | static meta: DashboardItemMetadata = { 12 | icon: 'fa fa-pie-chart' 13 | } 14 | 15 | labels: boolean = false 16 | is_pie: boolean = false 17 | 18 | constructor(data?: any) { 19 | super(data) 20 | if (data) { 21 | if (typeof(data.labels) !== 'undefined') { 22 | this.labels = Boolean(data.labels) 23 | } 24 | if (typeof(data.is_pie) !== 'undefined') { 25 | this.is_pie = Boolean(data.is_pie) 26 | } 27 | } 28 | } 29 | 30 | toJSON() : any { 31 | return extend(super.toJSON(), { 32 | labels: this.labels, 33 | is_pie: this.is_pie 34 | }) 35 | } 36 | 37 | data_handler(query: Query) : void { 38 | charts.donut_chart(`#${this.item_id} .ds-graph-holder`, this, query) 39 | } 40 | 41 | interactive_properties(): PropertyList { 42 | return super.interactive_properties().concat([ 43 | { name: 'labels', type: 'boolean' }, 44 | { name: 'is_pie', type: 'boolean' } 45 | ]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/heading.ts: -------------------------------------------------------------------------------- 1 | import DashboardItem, { DashboardItemMetadata } from './item' 2 | import { extend } from '../../util' 3 | import { PropertyList } from '../../core/property' 4 | 5 | export default class Heading extends DashboardItem { 6 | static meta: DashboardItemMetadata = { 7 | icon: 'fa fa-header', 8 | category: 'display' 9 | } 10 | 11 | text: string 12 | level: number = 1 13 | description: string 14 | 15 | constructor(data?: any) { 16 | super(data) 17 | if (data) { 18 | this.text = data.text 19 | this.level = data.level || this.level 20 | this.description = data.description 21 | } 22 | } 23 | 24 | toJSON() : any { 25 | return extend(super.toJSON(), { 26 | text: this.text, 27 | level: this.level, 28 | description: this.description 29 | }) 30 | } 31 | 32 | interactive_properties() : PropertyList { 33 | return [ 34 | 'text', 35 | { 36 | name: 'level', 37 | edit_options: { 38 | type: 'select', 39 | source: [ 1, 2, 3, 4, 5, 6 ] 40 | } 41 | }, 42 | 'description' 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/jumbotron_singlestat.ts: -------------------------------------------------------------------------------- 1 | import Singlestat from './singlestat' 2 | 3 | export default class JumbotronSinglestat extends Singlestat { 4 | } 5 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/markdown.ts: -------------------------------------------------------------------------------- 1 | import DashboardItem, { DashboardItemMetadata } from './item' 2 | import { render_template } from '../../util' 3 | import { PropertyList } from '../../core/property' 4 | import { extend } from '../../util' 5 | 6 | export default class Markdown extends DashboardItem { 7 | static meta: DashboardItemMetadata = { 8 | icon: 'fa fa-code', 9 | category: 'display' 10 | } 11 | 12 | text: string 13 | expanded_text: string 14 | raw: boolean = false 15 | 16 | constructor(data?: any) { 17 | super(data) 18 | if (data) { 19 | this.text = data.text 20 | if (data.raw !== undefined) { 21 | this.raw = data.raw 22 | } 23 | } 24 | } 25 | 26 | set_text(value: string) : Markdown { 27 | this.text = value 28 | return this.updated() 29 | } 30 | 31 | render_templates(context?: any) : void { 32 | try { 33 | this.expanded_text = render_template(this.text, context) 34 | } catch (e) { 35 | this.expanded_text = e.toString() 36 | } 37 | } 38 | 39 | toJSON() : any { 40 | return extend(super.toJSON(), { 41 | text: this.text, 42 | raw: this.raw 43 | }) 44 | } 45 | 46 | interactive_properties(): PropertyList { 47 | return [ 48 | { 49 | name: 'markdown.text', 50 | type: 'textarea', 51 | property_name: 'text' 52 | }, 53 | 'css_class' 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/percentage_table.ts: -------------------------------------------------------------------------------- 1 | import TablePresentation from './table_presentation' 2 | import { DashboardItemMetadata } from './item' 3 | import Query from '../data/query' 4 | import { extend } from '../../util' 5 | import { PropertyList } from '../../core/property' 6 | 7 | declare var $, ts 8 | 9 | export default class PercentageTable extends TablePresentation { 10 | static meta: DashboardItemMetadata = { 11 | icon: 'fa fa-table', 12 | category: 'data-table', 13 | requires_data: true 14 | } 15 | 16 | include_sums: boolean = false 17 | invert_axes: boolean = false 18 | transform: string = 'sum' 19 | 20 | constructor(data?: any) { 21 | super(data) 22 | if (data) { 23 | this.include_sums = data.include_sums 24 | this.invert_axes = data.invert_axes 25 | this.transform = data.transform || this.transform 26 | } 27 | } 28 | 29 | toJSON() : any { 30 | return extend(super.toJSON(), { 31 | invert_axes: this.invert_axes, 32 | transform: this.transform, 33 | include_sums: this.include_sums 34 | }) 35 | } 36 | 37 | data_handler(query: Query) { 38 | query.summation.percent_value = query.summation[this.transform] 39 | 40 | query.data.forEach((series) => { 41 | series.summation.percent = 1 / (query.summation[this.transform] / series.summation[this.transform]) 42 | series.summation.percent_value = series.summation[this.transform] 43 | }) 44 | 45 | let holder = $(`#${this.item_id} .ds-percentage-table-holder`) 46 | holder.empty() 47 | holder.append(ts.templates.models.percentage_table_data({item:this, query:query})) 48 | if (this.sortable) { 49 | let table = $(`#${this.item_id} .ds-percentage-table-holder table`) 50 | if (!$.fn.dataTable.isDataTable(table)) { 51 | table.DataTable({ 52 | order: [[ 2, "desc" ]], 53 | paging: false, 54 | searching: false, 55 | oLanguage: { sSearch: "" }, 56 | info: true 57 | }) 58 | } 59 | } 60 | } 61 | 62 | interactive_properties(): PropertyList { 63 | return super.interactive_properties().concat([ 64 | { name: 'invert_axes', type: 'boolean' }, 65 | { name: 'include_sums', type: 'boolean' }, 66 | 'transform' 67 | ]) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/row.ts: -------------------------------------------------------------------------------- 1 | import Container from './container' 2 | import { DashboardItemMetadata } from './item' 3 | import { PropertyList } from '../../core/property' 4 | 5 | export default class Row extends Container { 6 | static meta: DashboardItemMetadata = { 7 | category: 'structural' 8 | } 9 | 10 | constructor(data?: any) { 11 | super(data) 12 | } 13 | 14 | interactive_properties() : PropertyList { 15 | return [ 16 | 'style', 'css_class' 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/section.ts: -------------------------------------------------------------------------------- 1 | import Container from './container' 2 | import { DashboardItemMetadata } from './item' 3 | import { extend } from '../../util' 4 | import { PropertyList } from '../../core/property' 5 | 6 | export default class Section extends Container { 7 | static meta: DashboardItemMetadata = { 8 | category: 'structural' 9 | } 10 | 11 | description: string 12 | level: number = 1 13 | horizontal_rule: boolean = false 14 | layout: string = 'fixed' 15 | 16 | constructor(data?: any) { 17 | super(data) 18 | if (data) { 19 | this.description = data.description 20 | this.level = data.level || this.level 21 | if (typeof data.horizontal_rule !== 'undefined') 22 | this.horizontal_rule = !!data.horizontal_rule 23 | this.layout = data.layout || this.layout 24 | } 25 | } 26 | 27 | set_layout(value: string) : Section { 28 | this.layout = value 29 | return
    this.updated() 30 | } 31 | 32 | toJSON() :any { 33 | return extend(super.toJSON(), { 34 | description: this.description, 35 | level: this.level, 36 | horizontal_rule: this.horizontal_rule, 37 | layout: this.layout 38 | }) 39 | } 40 | 41 | interactive_properties(): PropertyList { 42 | return [ 43 | { name: 'layout', 44 | type: 'select', 45 | edit_options: { 46 | source: [ 47 | 'fixed', 48 | 'fluid', 49 | 'none' 50 | ] 51 | } 52 | }, 53 | 'style', 54 | 'css_class', 55 | 'title', 56 | 'description', 57 | { 58 | name: 'level', 59 | edit_options: { 60 | type: 'select', 61 | source: [ 1, 2, 3, 4, 5, 6 ] 62 | } 63 | }, 64 | { name: 'horizontal_rule', type: 'boolean' } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/separator.ts: -------------------------------------------------------------------------------- 1 | import DashboardItem, { DashboardItemMetadata } from './item' 2 | import { PropertyList } from '../../core/property' 3 | 4 | export default class Separator extends DashboardItem { 5 | static meta: DashboardItemMetadata = { 6 | item_type: 'separator', 7 | display_name: 'Separator', 8 | icon: 'fa fa-arrows-h', 9 | category: 'display' 10 | } 11 | 12 | constructor(data?: any) { 13 | super(data) 14 | } 15 | 16 | interactive_properties() : PropertyList { 17 | return [ 'css_class' ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/simple_time_series.ts: -------------------------------------------------------------------------------- 1 | import XYChart from './xychart' 2 | import Query from '../data/query' 3 | import { DashboardItemMetadata } from './item' 4 | import { PropertyList } from '../../core/property' 5 | import { extend } from '../../util' 6 | import * as charts from '../../charts/core' 7 | 8 | declare var $ 9 | 10 | export default class SimpleTimeSeries extends XYChart { 11 | static meta: DashboardItemMetadata = { 12 | icon: 'fa fa-line-chart' 13 | } 14 | 15 | filled: boolean = false 16 | show_max_value: boolean = false 17 | show_min_value: boolean = false 18 | show_last_value: boolean = false 19 | 20 | constructor(data?: any) { 21 | super(data) 22 | if (data) { 23 | this.legend = data.legend 24 | this.filled = !!data.filled 25 | this.show_max_value = !!data.show_max_value 26 | this.show_min_value = !!data.show_min_value 27 | this.show_last_value = !!data.show_last_value 28 | if (!this.height) { 29 | this.height = 1 30 | } 31 | } 32 | } 33 | 34 | toJSON() : any { 35 | return extend(super.toJSON(), { 36 | filled: this.filled, 37 | show_max_value: this.show_max_value, 38 | show_min_value: this.show_min_value, 39 | show_last_value: this.show_last_value 40 | }) 41 | } 42 | 43 | data_handler(query: Query) : void { 44 | let selector = `#${this.item_id} .ds-graph-holder` 45 | if (this.filled) { 46 | charts.simple_area_chart(selector, this, query) 47 | } else { 48 | charts.simple_line_chart(selector, this, query) 49 | } 50 | } 51 | 52 | interactive_properties() : PropertyList { 53 | return super.interactive_properties().concat([ 54 | { name: 'filled', type: 'boolean' }, 55 | { name: 'show_max_value', type: 'boolean' }, 56 | { name: 'show_min_value', type: 'boolean' }, 57 | { name: 'show_last_value', type: 'boolean' } 58 | ]) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/singlegraph.ts: -------------------------------------------------------------------------------- 1 | import Chart from './chart' 2 | import Query from '../data/query' 3 | import { DashboardItemMetadata } from './item' 4 | import * as charts from '../../charts' 5 | import { extend } from '../../util' 6 | import { PropertyList } from '../../core/property' 7 | 8 | declare var $, d3 9 | 10 | export default class Singlegraph extends Chart { 11 | static meta: DashboardItemMetadata = { 12 | icon: 'fa fa-image', 13 | category: 'chart', 14 | requires_data: true 15 | } 16 | 17 | units: string 18 | format: string = ',.1s' 19 | transform: string = 'mean' 20 | display_transform: boolean = true 21 | index: number 22 | 23 | constructor(data?: any) { 24 | super(data) 25 | if (data) { 26 | this.units = data.units 27 | this.format = data.format || this.format 28 | this.transform = data.transform || this.transform 29 | if (typeof(data.display_transform) !== 'undefined') { 30 | this.display_transform = Boolean(data.display_transform) 31 | } 32 | this.index = data.index 33 | if (!this.height) 34 | this.height = 1 35 | } 36 | } 37 | 38 | toJSON() : any { 39 | return extend(super.toJSON(), { 40 | units: this.units, 41 | format: this.format, 42 | transform: this.transform, 43 | display_transform: this.display_transform, 44 | index: this.index 45 | }) 46 | } 47 | 48 | data_handler(query: Query) : void { 49 | if (!query.data) 50 | return 51 | let flot = charts.renderers.get('flot') 52 | let options = { 53 | colors: charts.get_palette(this.options.palette) 54 | } 55 | flot.sparkline(`#${this.item_id} .ds-graph-holder`, this, query, 0, options) 56 | this.options.margin = { top: 0, left: 0, bottom: 0, right: 0 } 57 | var label = query.data[this.index || 0].target 58 | var value = query.summation[this.transform] 59 | if (this.index) { 60 | value = query.data[this.index].summation[this.transform] 61 | } 62 | $(`#${this.item_id} span.value`).text(d3.format(this.format)(value)) 63 | $(`#${this.item_id} span.ds-label`).text(label) 64 | } 65 | 66 | interactive_properties(): PropertyList { 67 | return super.interactive_properties().concat([ 68 | 'units', 'format', 'transform', 69 | { 70 | name: 'display_transform', 71 | type: 'boolean', 72 | }, 73 | { name: 'index', type: 'number' } 74 | ]) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/singlegraph_grid.ts: -------------------------------------------------------------------------------- 1 | import Chart from './chart' 2 | import Query from '../data/query' 3 | import { DashboardItemMetadata } from './item' 4 | import * as charts from '../../charts' 5 | import { extend } from '../../util' 6 | import { PropertyList } from '../../core/property' 7 | 8 | declare var $, d3, ts 9 | 10 | export default class SinglegraphGrid extends Chart { 11 | static meta: DashboardItemMetadata = { 12 | icon: 'fa fa-image', 13 | category: 'chart', 14 | requires_data: true 15 | } 16 | 17 | units: string 18 | format: string = ',.1s' 19 | transform: string = 'mean' 20 | display_transform: boolean = true 21 | columns: number = 4 22 | 23 | constructor(data?: any) { 24 | super(data) 25 | if (data) { 26 | this.units = data.units 27 | this.format = data.format || this.format 28 | this.transform = data.transform || this.transform 29 | if (typeof(data.display_transform) !== 'undefined') { 30 | this.display_transform = Boolean(data.display_transform) 31 | } 32 | this.columns = data.columns || this.columns 33 | } 34 | if (!this.height) 35 | this.height = 1 36 | } 37 | 38 | toJSON() : any { 39 | return extend(super.toJSON(), { 40 | units: this.units, 41 | format: this.format, 42 | transform: this.transform, 43 | display_transform: this.display_transform, 44 | columns: this.columns 45 | }) 46 | } 47 | 48 | data_handler(query: Query) : void { 49 | if (!query.data) 50 | return 51 | let span = 12 / this.columns 52 | let format = d3.format(this.format) 53 | let flot = charts.renderers.get('flot') 54 | let holder = $(`#${this.item_id} .ds-singlegraph-grid-holder`) 55 | let options = { 56 | colors: charts.get_palette(this.options.palette) 57 | } 58 | holder.empty() 59 | query.data.forEach((series, i) => { 60 | let value = series.summation[this.transform] 61 | if (!(series.summation.sum === 0 && this.hide_zero_series)) { 62 | holder.append(ts.templates.models.singlegraph_grid_item({ 63 | item: this, 64 | index: i, 65 | colspan: span, 66 | value: format(value), 67 | label: series.target 68 | })) 69 | flot.sparkline(`#${this.item_id}-${i} .ds-graph-holder`, this, query, i, options) 70 | } 71 | }) 72 | } 73 | 74 | interactive_properties(): PropertyList { 75 | return super.interactive_properties().concat([ 76 | 'units', 'format', 'transform', 77 | { 78 | name: 'display_transform', 79 | type: 'boolean', 80 | }, 81 | { 82 | name: 'columns', 83 | edit_options: { 84 | type: 'select', 85 | source: [ 1, 2, 3, 4, 6, 12 ] 86 | } 87 | }, 88 | 'style' 89 | ]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/singlestat.ts: -------------------------------------------------------------------------------- 1 | import Presentation from './presentation' 2 | import { DashboardItemMetadata } from './item' 3 | import { extend } from '../../util' 4 | import { PropertyList } from '../../core/property' 5 | import Query from '../data/query' 6 | 7 | declare var $, d3 8 | 9 | export default class Singlestat extends Presentation { 10 | static meta: DashboardItemMetadata = { 11 | category: 'data-table', 12 | icon: 'fa fa-subscript', 13 | requires_data: true 14 | } 15 | 16 | units: string 17 | format: string = ',.3s' 18 | transform: string = 'mean' 19 | index: number 20 | 21 | constructor(data?: any) { 22 | super(data) 23 | if (data) { 24 | this.units = data.units 25 | this.format = data.format || this.format 26 | this.index = data.index 27 | this.transform = data.transform || this.transform 28 | } 29 | } 30 | 31 | toJSON() : any { 32 | return extend(super.toJSON(), { 33 | units: this.units, 34 | format: this.format, 35 | transform: this.transform, 36 | index: this.index 37 | }) 38 | } 39 | 40 | data_handler(query: Query) : void { 41 | if (!query.summation) 42 | return 43 | let element = $(`#${this.item_id} .value`) 44 | let value = query.summation[this.transform] 45 | if (this.index) { 46 | value = query.data[this.index].summation[this.transform] 47 | } 48 | element.text(d3.format(this.format)(value)) 49 | } 50 | 51 | interactive_properties(): PropertyList { 52 | return super.interactive_properties().concat([ 53 | 'units', 54 | 'format', 55 | 'title', 56 | { name: 'index', type: 'number' }, 57 | 'transform' 58 | ]) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/singlestat_grid.ts: -------------------------------------------------------------------------------- 1 | import Presentation from './presentation' 2 | import { DashboardItemMetadata } from './item' 3 | import { extend } from '../../util' 4 | import { PropertyList } from '../../core/property' 5 | import Query from '../data/query' 6 | 7 | declare var $, d3 8 | 9 | export default class SinglestatGrid extends Presentation { 10 | static meta: DashboardItemMetadata = { 11 | category: 'data-table', 12 | icon: 'fa fa-subscript', 13 | requires_data: true 14 | } 15 | 16 | units: string 17 | format: string = ',.3s' 18 | transform: string = 'mean' 19 | columns: number = 4 20 | hide_zero_series: boolean = false 21 | 22 | constructor(data?: any) { 23 | super(data) 24 | if (data) { 25 | this.units = data.units 26 | this.format = data.format || this.format 27 | this.transform = data.transform || this.transform 28 | this.columns = data.columns || this.columns 29 | if (typeof(data.hide_zero_series !== 'undefined')) { 30 | this.hide_zero_series = Boolean(data.hide_zero_series) 31 | } 32 | } 33 | } 34 | 35 | toJSON() : any { 36 | return extend(super.toJSON(), { 37 | units: this.units, 38 | format: this.format, 39 | transform: this.transform, 40 | columns: this.columns, 41 | hide_zero_series: this.hide_zero_series 42 | }) 43 | } 44 | 45 | data_handler(query: Query) : void { 46 | if (!query.data) 47 | return 48 | let span = 12 / this.columns 49 | let format = d3.format(this.format) 50 | let holder = $(`#${this.item_id} .ds-singlestat-grid-holder`) 51 | holder.empty() 52 | query.data.forEach((series, i) => { 53 | let value = series.summation[this.transform] 54 | if (!(series.summation.sum === 0 && this.hide_zero_series)) { 55 | holder.append(ts.templates.models.singlestat_grid_item({ 56 | item: this, 57 | index: i, 58 | colspan: span, 59 | value: format(value), 60 | label: series.target 61 | })) 62 | } 63 | }) 64 | } 65 | 66 | interactive_properties(): PropertyList { 67 | return super.interactive_properties().concat([ 68 | 'units', 69 | 'format', 70 | 'title', 71 | { name: 'index', type: 'number' }, 72 | { 73 | name: 'hide_zero_series', 74 | type: 'boolean' 75 | }, 76 | { 77 | name: 'columns', 78 | edit_options: { 79 | type: 'select', 80 | source: [ 1, 2, 3, 4, 6, 12 ] 81 | } 82 | }, 83 | 'transform', 84 | 'style' 85 | ]) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/table_presentation.ts: -------------------------------------------------------------------------------- 1 | import Presentation from './presentation' 2 | import { extend } from '../../util' 3 | import { PropertyList } from '../../core/property' 4 | import { DashboardItemMetadata } from './item' 5 | 6 | export default class TablePresentation extends Presentation { 7 | static meta: DashboardItemMetadata = { 8 | icon: 'fa fa-table', 9 | category: 'data-table', 10 | requires_data: true 11 | } 12 | 13 | striped: boolean = false 14 | sortable: boolean = false 15 | format: string = ',.3s' 16 | 17 | constructor(data?: any) { 18 | super(data) 19 | if (data) { 20 | this.striped = !!data.striped 21 | this.sortable = !!data.sortable 22 | this.format = data.format || this.format 23 | } 24 | } 25 | 26 | toJSON() : any { 27 | return extend(super.toJSON(), { 28 | striped: this.striped, 29 | sortable: this.sortable, 30 | format: this.format, 31 | }) 32 | } 33 | 34 | cleanup() : void { 35 | let table = $('#' + this.item_id + ' table') 36 | if ($.fn.dataTable.isDataTable(table)) { 37 | table.DataTable().destroy() 38 | } 39 | } 40 | 41 | interactive_properties() : PropertyList { 42 | return super.interactive_properties().concat([ 43 | { name: 'striped', type: 'boolean' }, 44 | { name: 'sortable', type: 'boolean' }, 45 | 'format' 46 | ]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/timeshift_jumbotron_singlestat.ts: -------------------------------------------------------------------------------- 1 | import TimeshiftSinglestat from './timeshift_singlestat' 2 | 3 | declare var ts 4 | 5 | export default class TimeshiftJumbotronSinglestat extends TimeshiftSinglestat { 6 | static meta = { 7 | template: ts.templates.models.jumbotron_singlestat 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/items/timeshift_singlestat.ts: -------------------------------------------------------------------------------- 1 | import { logger, extend } from '../../util' 2 | import { PropertyList } from '../../core' 3 | import * as app from '../../app' 4 | import Singlestat from './singlestat' 5 | import Query from '../data/query' 6 | 7 | declare var $, d3, ts 8 | 9 | const log = logger('models.timeshift_singlestat') 10 | const FORMAT_PERCENT = d3.format(',.1%') 11 | 12 | export default class TimeshiftSinglestat extends Singlestat { 13 | static meta = { 14 | template: ts.templates.models.singlestat 15 | } 16 | 17 | private _shift: string = '1d' 18 | percent : boolean = false 19 | 20 | constructor(data?: any) { 21 | super(data) 22 | if (data) { 23 | this._shift = data.shift || this._shift 24 | this.percent = !!data.percent 25 | } 26 | this._update_query() 27 | } 28 | 29 | _update_query() : void { 30 | if (this._query && this.dashboard) { 31 | let query = this.dashboard.definition.queries[this._query] 32 | this.query_override = query 33 | .join(query.shift(this.shift)) 34 | .set_name(this.item_id + '_shifted') 35 | this.query_override.render_templates(app.context().variables) 36 | } 37 | } 38 | 39 | get shift() : string { 40 | return this._shift 41 | } 42 | 43 | set shift(value: string) { 44 | this._shift = value 45 | } 46 | 47 | set_shift(value: string) : TimeshiftSinglestat { 48 | this.shift = value 49 | return this.updated() 50 | } 51 | 52 | toJSON() : any { 53 | return extend(super.toJSON(), { 54 | shift: this.shift, 55 | percent: this.percent 56 | }) 57 | } 58 | 59 | render() : string { 60 | this._update_query() 61 | return super.render() 62 | } 63 | 64 | data_handler(query: Query) : void { 65 | let value = query.data[0].summation[this.transform] 66 | let base = query.data[1].summation[this.transform] 67 | let diff = value - base 68 | let pct = (value / base) - 1 69 | let float_margin = 0.000001 70 | let diff_elt = $(`#${this.item_id} span.diff`) 71 | 72 | $(`#${this.item_id} span.value`).text(d3.format(this.format)(value)) 73 | 74 | if (diff > float_margin) 75 | diff_elt.addClass('ds-diff-plus') 76 | else if (diff < float_margin) 77 | diff_elt.addClass('ds-diff-minus') 78 | 79 | let diff_formatted = this.percent 80 | ? FORMAT_PERCENT(Math.abs(pct)) 81 | : d3.format(this.format)(Math.abs(diff)) 82 | $(`#${this.item_id} span.diff`).text(diff_formatted) 83 | 84 | } 85 | 86 | interactive_properties(): PropertyList { 87 | return super.interactive_properties().concat([ 88 | 'shift', 89 | { name: 'percent', type: 'boolean' } 90 | ]) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/model.ts: -------------------------------------------------------------------------------- 1 | import EventSource from '../util/event-source' 2 | 3 | /** 4 | * Base class for all API model classes 5 | */ 6 | export default class Model extends EventSource { 7 | 8 | constructor(data?: any) { 9 | super(data) 10 | } 11 | 12 | toJSON() : any { 13 | return {} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/preferences.ts: -------------------------------------------------------------------------------- 1 | import Model from './model' 2 | 3 | export default class Preferences extends Model { 4 | connected_lines: boolean = false 5 | default_from_time: string = '-3h' 6 | downsample: boolean = true 7 | graphite_auth: string 8 | graphite_url: string = 'http://localhost:8080' 9 | propsheet_autoclose_seconds: number = 3 10 | refresh: number = 60 11 | renderer: string = 'flot' 12 | theme: string = 'light' 13 | timezone: string = 'Etc/UTC' 14 | 15 | constructor(data?: any) { 16 | super(data) 17 | if (data) { 18 | if (typeof data.connected_lines != 'undefined') 19 | this.connected_lines = !!data.connected_lines 20 | this.default_from_time = data.default_from_time || this.default_from_time 21 | if (typeof data.downsample != 'undefined') 22 | this.downsample = !!data.downsample 23 | this.graphite_auth = data.graphite_auth 24 | this.graphite_url = data.graphite_url || this.graphite_url 25 | this.propsheet_autoclose_seconds = Number(data.propsheet_autoclose_seconds) || this.propsheet_autoclose_seconds 26 | this.refresh = Number(data.refresh) || this.refresh 27 | this.renderer = data.renderer || this.renderer 28 | this.theme = data.theme || this.theme 29 | this.timezone = data.timezone || this.timezone 30 | } 31 | } 32 | 33 | toJSON() : any { 34 | return { 35 | connected_lines: this.connected_lines, 36 | default_from_time: this.default_from_time, 37 | downsample: this.downsample, 38 | graphite_auth: this.graphite_auth, 39 | graphite_url: this.graphite_url, 40 | propsheet_autoclose_seconds: this.propsheet_autoclose_seconds, 41 | refresh: this.refresh, 42 | renderer: this.renderer, 43 | theme: this.theme, 44 | timezone: this.timezone 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/tag.ts: -------------------------------------------------------------------------------- 1 | import Model from './model' 2 | 3 | export default class Tag extends Model { 4 | id: string 5 | href: string 6 | name: string 7 | description: string 8 | color: string 9 | count: number 10 | 11 | constructor(data?: any) { 12 | super(data) 13 | if (data) { 14 | if (typeof data === 'string') { 15 | this.name = data 16 | } else { 17 | this.id = data.id 18 | this.href = data.href 19 | this.name = data.name 20 | this.description = data.description 21 | this.color = data.color 22 | this.count = data.count 23 | } 24 | } 25 | } 26 | 27 | toJSON() : any { 28 | return { 29 | id: this.id, 30 | href: this.href, 31 | name: this.name, 32 | description: this.description, 33 | color: this.color, 34 | count: this.count, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/thresholds.ts: -------------------------------------------------------------------------------- 1 | import Model from './model' 2 | 3 | export default class Thresholds extends Model { 4 | summation_type: string 5 | warning: number 6 | danger: number 7 | 8 | constructor(data?: any) { 9 | super(data) 10 | if (data) { 11 | this.summation_type = data.summation_type || 'mean' 12 | this.warning = data.warning 13 | this.danger = data.danger 14 | } 15 | } 16 | 17 | toJSON() : any { 18 | return { 19 | summation_type: this.summation_type, 20 | warning: this.warning, 21 | danger: this.danger 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/transform/HighlightAverages.ts: -------------------------------------------------------------------------------- 1 | import { transforms } from './transform' 2 | import { make } from '../items/factory' 3 | import { extend } from '../../util' 4 | import Query from '../data/query' 5 | import * as util from '../../charts/util' 6 | 7 | declare var Color, $ 8 | 9 | /** 10 | * Higlight the average, max average, and most deviant series. This 11 | * transform works best on line graphs with lots of metrics (for 12 | * example, CPU or disk usage across a cluster). 13 | * 14 | * TODO: enforcing that it not be used on stacked graphs would be 15 | * neat. Adding an `applicable_types` attribute or something could 16 | * control visibily of transforms, so they only appear for 17 | * presentations that they make sense for. 18 | */ 19 | transforms.register({ 20 | name: 'HighlightAverages', 21 | display_name: 'Highlight Averages', 22 | transform_type: 'presentation', 23 | 24 | transform: function(item: any) : any { 25 | let query = item.query 26 | let group = (query.targets.length > 1) ? 'group(' + query.targets.join(',') + ')' : query.targets[0] 27 | let bg = Color(window.getComputedStyle($('body')[0]).backgroundColor) 28 | let palette = util.get_low_contrast_palette() 29 | 30 | /* Set up the modified queries */ 31 | let query_averages = new Query({ 32 | name: query.name + '_averages', 33 | targets: [ 34 | group, 35 | 'alias(lineWidth(color(averageSeries(' + group + '), "' + (bg.isDark() 36 | ? bg.lighten(4.0) 37 | : bg.darken(1.0)).hex() + '"), 2), "Avg")', 38 | 'alias(lineWidth(color(highestAverage(' + group + ', 1), "red"), 2), "Max Avg")' 39 | ] 40 | }) 41 | let query_deviant = new Query({ 42 | name: query.name + '_deviant', 43 | targets: [ 44 | group, 45 | 'alias(lineWidth(color(mostDeviant(' + group + ', 1), "red"), 2), "Most Deviant")' 46 | ] 47 | }) 48 | 49 | /* Clone and modify the original */ 50 | let item_averages = make('standard_time_series') 51 | .set_height(6) 52 | .set_renderer('graphite') 53 | .set_title("Average & Max Average") 54 | .set_query_override(query_averages) 55 | item_averages.options.palette = palette 56 | item_averages.options.hideLegend = 'true' 57 | 58 | let item_deviant = make('standard_time_series') 59 | .set_height(4) 60 | .set_renderer('graphite') 61 | .set_title('Most Deviant') 62 | .set_query_override(query_deviant) 63 | item_deviant.options.palette = palette 64 | 65 | /* And put it all together */ 66 | return make('section') 67 | .add(make('row') 68 | .add(make('cell') 69 | .set_span(12) 70 | .set_style('well') 71 | .add(item_averages))) 72 | .add(make('row') 73 | .add(make('cell') 74 | .set_span(12) 75 | .add(item_deviant))) 76 | .add(make('separator')) 77 | .add(make('row') 78 | .add(make('cell') 79 | .set_span(12) 80 | .add(item.set_title('Original')))) 81 | 82 | } 83 | 84 | }) 85 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/transform/Isolate.ts: -------------------------------------------------------------------------------- 1 | import { transforms, render_query } from './transform' 2 | import { make } from '../items/factory' 3 | import Chart from '../items/chart' 4 | 5 | /** 6 | * Focus on a single presentation. 7 | */ 8 | transforms.register({ 9 | name: 'Isolate', 10 | display_name: 'Isolate', 11 | transform_type: 'presentation', 12 | 13 | transform: function(item: any) : any { 14 | let options = item.options || {} 15 | if (item instanceof Chart) { 16 | item.set_renderer('flot') 17 | } 18 | 19 | return make('section') 20 | .add(make('row') 21 | .add(make('cell') 22 | .set_span(12) 23 | .set_style('well') 24 | .add(item.set_height(6)))) 25 | .add(make('row') 26 | .add(make('cell') 27 | .set_span(12) 28 | .add(render_query(item.query)))) 29 | .add(make('row') 30 | .add(make('cell') 31 | .set_span(12) 32 | .add(make({item_type: 'summation_table', 33 | options: { 34 | palette: options.palette 35 | }, 36 | show_color: true, 37 | sortable: true, 38 | format: ',.3f', 39 | query: item.query})))) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/transform/SimpleGrid.ts: -------------------------------------------------------------------------------- 1 | import Transform, { transforms, TransformType } from './transform' 2 | import { make } from '../items/factory' 3 | import { extend } from '../../util' 4 | import Chart from '../items/chart' 5 | 6 | /** 7 | * A transform which simply takes all presentations and arranges them in 8 | * a regular grid. 9 | */ 10 | export default class SimpleGrid extends Transform { 11 | private _columns : number = 1 12 | span: number = 12 13 | section_type: string = 'fixed' 14 | charts_only: boolean = false 15 | 16 | constructor(data?: any) { 17 | super(extend({}, data, { 18 | display_name: 'Simple Grid', 19 | name: 'SimpleGrid', 20 | transform_type: TransformType.DASHBOARD 21 | })) 22 | 23 | if (data) { 24 | this.columns = data.columns || this.columns 25 | this.section_type = data.section_type || this.section_type 26 | this.charts_only = !!data.charts_only 27 | } 28 | this.span = 12 / this.columns 29 | } 30 | 31 | /** Setter for the columns property recalculates the column 32 | * span to resize items to (based ona 12-column grid) */ 33 | set columns(value: number) { 34 | this._columns = value 35 | this.span = 12 / value 36 | } 37 | 38 | get columns() : number { 39 | return this._columns 40 | } 41 | 42 | transform(item: any) : any { 43 | let items = item.flatten() 44 | let section = make('section').set_layout(this.section_type) 45 | let current_row = make('row') 46 | 47 | items.forEach( (item) => { 48 | if ( item.item_type === 'dashboard_definition' 49 | || item.item_type === 'cell' 50 | || item.item_type === 'row' 51 | || item.item_type === 'section' 52 | || (this.charts_only && !(item instanceof Chart))) { 53 | return 54 | } 55 | let cell = make('cell') 56 | .set_span(this.span) 57 | .add(item) 58 | 59 | if (current_row.add(cell).length == this.columns) { 60 | section.add(current_row) 61 | current_row = make('row') 62 | } 63 | } ) 64 | 65 | if (current_row.length > 0) { 66 | section.add(current_row) 67 | } 68 | 69 | return section 70 | } 71 | 72 | toJSON() : any { 73 | return { 74 | columns: this.columns, 75 | span: this.span, 76 | section_type: this.section_type, 77 | charts_only: this.charts_only, 78 | name: this.name 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/transform/TimeShift.ts: -------------------------------------------------------------------------------- 1 | import { transforms } from './transform' 2 | import { make } from '../items/factory' 3 | 4 | /** 5 | * Transform a view of a single graph into viewing multiple of that 6 | * graph at the same temporal resolution but shifted by one or more 7 | * time periods. 8 | */ 9 | transforms.register({ 10 | name: 'TimeShift', 11 | display_name: 'Time Shift', 12 | transform_type: 'presentation', 13 | icon: 'fa fa-clock-o', 14 | 15 | transform: function(item: any) : any { 16 | let query = item.query 17 | let shifts = [ 18 | { shift: '-1h', title: 'One Hour Ago' }, 19 | { shift: '-12h', title: '12 Hours Ago' }, 20 | { shift: '-1d', title: 'One Day Ago' }, 21 | { shift: '-2d', title: 'Two Days Ago' }, 22 | { shift: '-1w', title: 'One Week Ago' } 23 | ] 24 | let make_row = function(query, item, style?) { 25 | return make('row') 26 | .set_style(style) 27 | .add(make('cell') 28 | .set_span(10) 29 | .add(item)) 30 | .add(make('cell') 31 | .set_span(1) 32 | .set_align('right') 33 | .add(make('singlestat') 34 | .set_query(query) 35 | .set_transform('sum') 36 | .set_format(',.2s') 37 | .set_title('Total'))) 38 | .add(make('cell') 39 | .set_span(1) 40 | .set_align('left') 41 | .add(make('singlestat') 42 | .set_query(query) 43 | .set_transform('mean') 44 | .set_format(',.2s') 45 | .set_title('Average'))) 46 | } 47 | 48 | let section = make('section') 49 | .add(make({ item_type: 'heading', 50 | level: 2, text: item.title 51 | ? 'Time shift - ' + item.title 52 | : 'Time shift' })) 53 | .add(make('separator')) 54 | .add(make_row(item.query, item, 'well')) 55 | .add(make('separator')) 56 | 57 | for (let shift of shifts) { 58 | let modified_query = query.shift(shift.shift) 59 | let modified_item = make(item.toJSON()) 60 | .set_item_id(undefined) 61 | .set_query_override(modified_query) 62 | .set_title(shift.title) 63 | section.add(make_row(modified_query, modified_item)) 64 | } 65 | 66 | return section 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/transform/TimeSpans.ts: -------------------------------------------------------------------------------- 1 | import { transforms } from './transform' 2 | import { make } from '../items/factory' 3 | import { extend } from '../../util' 4 | import Query from '../data/query' 5 | 6 | /** 7 | * Transform a view of a single graph into viewing that graph over 8 | * multiple time spans. The item being transformed should be a single 9 | * presentation, not a composite (i.e. not a cell, row, section, or 10 | * dashboard). 11 | * 12 | * The time periods can be customized by passing an array of objects 13 | * with from/until/title properties 14 | * 15 | * Input => A single presentation 16 | * 17 | * Output => A section with a list of copies of the presentation with 18 | * immediate query objects that override the time period 19 | */ 20 | transforms.register({ 21 | name: 'TimeSpans', 22 | display_name: 'View across time spans', 23 | transform_type: 'presentation', 24 | icon: ' fa fa-clock-o', 25 | 26 | transform: function(item: any) : any { 27 | let spans : { from: string, until?: string, title: string }[] = [ 28 | { from: '-1h', title: 'Past Hour' }, 29 | { from: '-4h', title: 'Past 4 Hours' }, 30 | { from: '-1d', title: 'Past Day' }, 31 | { from: '-1w', title: 'Past Week' } 32 | ] 33 | let columns = 1 34 | let query = item.query 35 | let colspan = 12 / columns 36 | let section = make('section') 37 | .add(make('heading', { 38 | level: 2, text: item.title 39 | ? 'Time Spans - ' + item.title 40 | : 'Time Spans' })) 41 | .add(make('separator')) 42 | 43 | section.add(make('cell') 44 | .set_span(colspan) 45 | .set_style('well') 46 | .add(item.set_title('Original'))) 47 | .add(make('separator')) 48 | 49 | for (let span of spans) { 50 | let modified_query = new Query(query.toJSON()) 51 | .set_name(query.name + '/' + span.from + '/' + span.until) 52 | .set_options(extend({}, query.options, 53 | { 54 | from: span.from, 55 | until: span.until 56 | })) 57 | let modified_item = make(item.toJSON()) 58 | .set_item_id(undefined) 59 | .set_query_override(modified_query) 60 | .set_title(span.title) 61 | 62 | section.add(make('cell') 63 | .set_span(colspan) 64 | .add(modified_item)) 65 | } 66 | 67 | return section 68 | } 69 | }) 70 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/transform/transform.ts: -------------------------------------------------------------------------------- 1 | import { NamedObject, Registry } from '../../util' 2 | import { Action, actions } from '../../core' 3 | import manager from '../../app/manager' 4 | import * as app from '../../app/app' 5 | import Query from '../data/query' 6 | import Markdown from '../items/markdown' 7 | import { make } from '../items/factory' 8 | 9 | export const TransformType = { 10 | DASHBOARD: 'dashboard', 11 | PRESENTATION: 'presentation' 12 | } 13 | 14 | export default class Transform implements NamedObject { 15 | name: string 16 | display_name: string 17 | transform_type: string 18 | private _transform: (any) => any /* for now */ 19 | icon: string 20 | 21 | constructor(data?: any) { 22 | if (data) { 23 | this.name = data.name 24 | this.display_name = data.display_name 25 | this.transform_type = data.transform_type 26 | this._transform = data.transform 27 | this.icon = data.icon 28 | } 29 | } 30 | 31 | action() : Action { 32 | return new Action({ 33 | name: `${this.name}_action`, 34 | display: `${this.display_name}...`, 35 | icon: this.icon || 'fa fa-eye', 36 | hide: app.Mode.TRANSFORM, 37 | handler: (action, item) => { 38 | manager.apply_transform(this, item) 39 | } 40 | }) 41 | } 42 | 43 | transform(item: any) : any { 44 | return this._transform 45 | ? this._transform(item) 46 | : item 47 | } 48 | 49 | toJSON() : any { 50 | return { 51 | name: this.name 52 | } 53 | } 54 | } 55 | 56 | export const transforms = new Registry({ 57 | name: 'transforms', 58 | process: (input: any) : Transform => { 59 | let transform = input instanceof Transform 60 | ? input 61 | : new Transform(input) 62 | let action_cat = `${transform.transform_type}-transform-actions` 63 | actions.register(action_cat, transform.action()) 64 | return transform 65 | } 66 | }) 67 | 68 | export function render_query(query: Query) : Markdown { 69 | let markdown = ` 70 | #### Query: ${query.name} 71 | 72 | \`\`\` 73 | ${query.targets[0]} 74 | \`\`\` 75 | ` 76 | return make('markdown') 77 | .set_text(markdown) 78 | } 79 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/models/user.ts: -------------------------------------------------------------------------------- 1 | import { logger, extend } from '../util' 2 | import Model from './model' 3 | import Dashboard from './dashboard' 4 | 5 | declare var store 6 | const log = logger('user') 7 | const STORAGE_KEY = 'tessera.user' 8 | 9 | /** 10 | * A very rudimentary user class. Since tessera doesn't have any 11 | * authentication yet, this basically represents the anonymous, 12 | * cookie-based session in the current browser. Eventually this will 13 | * represent a named user with real server-side persistence. 14 | */ 15 | export default class User extends Model { 16 | favorites = new Map() 17 | 18 | constructor() { 19 | super() 20 | let data = store.get(STORAGE_KEY) 21 | if (data && data.favorites && data.favorites.map) { 22 | data.favorites = data.favorites.map((pair) => { 23 | let [key, dashboard_data] = pair 24 | return [key, new Dashboard(dashboard_data)] 25 | }) 26 | this.favorites = new Map(data.favorites) 27 | } 28 | } 29 | 30 | toJSON() : any { 31 | return extend(super.toJSON(), { 32 | favorites: this.favorites 33 | }) 34 | } 35 | 36 | add_favorite(d: Dashboard) : User { 37 | log.debug(`add_favorite(${d.href})`) 38 | if (!this.favorites.has(d.href)) { 39 | // Make a copy of the dashboard without the definition, to 40 | // minimize the amount of data stored locally. 41 | let dash = new Dashboard(d.toJSON()) 42 | .set_definition(null) 43 | this.favorites.set(dash.href, dash) 44 | this.store() 45 | } 46 | return this 47 | } 48 | 49 | remove_favorite(d: Dashboard) : User { 50 | log.debug(`remove_favorite(${d.href})`) 51 | if (this.favorites.has(d.href)) { 52 | this.favorites.delete(d.href) 53 | this.store() 54 | } 55 | return this.store() 56 | } 57 | 58 | toggle_favorite(d: Dashboard) : boolean { 59 | if (this.favorites.has(d.href)) { 60 | this.remove_favorite(d) 61 | return false 62 | } else { 63 | this.add_favorite(d) 64 | return true 65 | } 66 | } 67 | 68 | list_favorites() : Dashboard[] { 69 | return [...this.favorites.values()] 70 | } 71 | 72 | store() : User { 73 | store.set(STORAGE_KEY, this.toJSON()) 74 | return this 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/util/event-source.ts: -------------------------------------------------------------------------------- 1 | import events from './event' 2 | 3 | export default class EventSource { 4 | 5 | constructor(data?: any) { 6 | } 7 | 8 | on(event: string, handler) : void { 9 | events.on(this, event, handler) 10 | } 11 | 12 | off(event: string) : void { 13 | events.off(this, event) 14 | } 15 | 16 | fire(event: string, ...data: any[]) : EventSource { 17 | events.fire(this, event, ...data) 18 | return this 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/util/event.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './log' 2 | 3 | declare var bean 4 | const log = logger('events') 5 | 6 | export interface EventHandler { 7 | (...args:any[]) : void 8 | } 9 | 10 | export interface EventProvider { 11 | on(target: Object, event: string, handler: EventHandler) : void 12 | off(target: Object, event: string) : void 13 | fire(target: Object, event: string, ...data: any[]) : void 14 | } 15 | 16 | /** 17 | * A delegating event provider which just logs and forwards all calls 18 | * to another provider. 19 | */ 20 | class LoggingEventProvider implements EventProvider { 21 | private provider : EventProvider 22 | 23 | constructor(provider: EventProvider) { 24 | this.provider = provider 25 | } 26 | 27 | on(target: Object, event: string, handler: EventHandler) : void { 28 | log.debug(`on(): ${event}`) 29 | this.provider.on(target, event, handler) 30 | } 31 | 32 | off(target: Object, event: string) : void { 33 | log.debug(`off(): ${event}`) 34 | this.provider.off(target, event) 35 | } 36 | 37 | fire(target: Object, event: string, ...data: any[]) : void { 38 | log.debug(`fire(): ${event}`) 39 | this.provider.fire(target, event, ...data) 40 | } 41 | } 42 | 43 | /** 44 | * Event provider that uses bean.js as the underlying event registry 45 | * and dispatcher. 46 | * 47 | * @see https://github.com/fat/bean 48 | */ 49 | class BeanEventProvider implements EventProvider { 50 | on(target: Object, event: string, handler: EventHandler) : void { 51 | bean.on(target, event, handler) 52 | } 53 | 54 | off(target: Object, event: string) : void { 55 | bean.off(target, event) 56 | } 57 | 58 | fire(target: Object, event: string, ...data: any[]) : void { 59 | bean.fire(target, event, ...data) 60 | } 61 | } 62 | 63 | /** 64 | * Global event provider instance. 65 | */ 66 | var provider : EventProvider 67 | 68 | export function set_provider(value: EventProvider) : EventProvider { 69 | provider = new LoggingEventProvider(value) 70 | return provider 71 | } 72 | 73 | set_provider(new BeanEventProvider()) 74 | 75 | export default provider 76 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/util/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | json, extend 3 | } from './util' 4 | 5 | export { 6 | logger, Level as LogLevel, set_level as set_log_level 7 | } from './log' 8 | 9 | export { 10 | Template, TemplateFunction, compile_template, render_template 11 | } from './template' 12 | 13 | export { 14 | NamedObject, Registry 15 | } from './registry' 16 | 17 | export { default as events } from './event' 18 | export { default as EventSource } from './event-source' 19 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/util/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a very simple implementation of the age-old log4j logging 3 | * pattern, supporting only output to `console` for now. 4 | */ 5 | 6 | declare var moment 7 | 8 | export enum Level { 9 | OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE 10 | } 11 | 12 | const default_name : string = 'root' 13 | const default_time_format : string = 'YYYY-MM-DD hh:mm:ss A' 14 | const default_level : Level = Level.INFO 15 | var global_level : Level = default_level 16 | 17 | function timestamp() : string { 18 | return moment().format(default_time_format) 19 | } 20 | 21 | /** 22 | * Options for initializing a new logger. 23 | */ 24 | export interface LoggerOptions { 25 | name?: string 26 | level?: Level 27 | } 28 | 29 | /** 30 | * Loggers log logs. 31 | */ 32 | export class Logger { 33 | name: string 34 | level: Level = global_level 35 | 36 | constructor(init: string | LoggerOptions) { 37 | if (typeof init === 'string') { 38 | this.name = init 39 | } else { 40 | this.name = init.name || default_name 41 | this.level = init.level || default_level 42 | } 43 | } 44 | 45 | set_level(level: Level) : Logger { 46 | this.level = level 47 | return this 48 | } 49 | 50 | is_enabled(level: Level) : boolean { 51 | return level && level >= this.level 52 | } 53 | 54 | format(level: Level, msg: any) : string { 55 | var ts : string = timestamp() 56 | var level_name : string = Level[level] 57 | return `${ts} | ${level_name} | ${this.name} | ${msg}` 58 | } 59 | 60 | log(level: Level, msg: any) : Logger { 61 | if (this.level >= level) { 62 | let statement = this.format(level, msg) 63 | if (level <= Level.WARN) { 64 | console.error(statement) 65 | } else { 66 | console.log(statement) 67 | } 68 | } 69 | return this 70 | } 71 | 72 | fatal(msg: any) : Logger { 73 | return this.log(Level.FATAL, msg) 74 | } 75 | 76 | error(msg: any) : Logger { 77 | return this.log(Level.ERROR, msg) 78 | } 79 | 80 | warn(msg: any) : Logger { 81 | return this.log(Level.WARN, msg) 82 | } 83 | 84 | info(msg: any) : Logger { 85 | return this.log(Level.INFO, msg) 86 | } 87 | 88 | debug(msg: any) : Logger { 89 | return this.log(Level.DEBUG, msg) 90 | } 91 | 92 | trace(msg: any) : Logger { 93 | return this.log(Level.TRACE, msg) 94 | } 95 | } 96 | 97 | /** 98 | * Provide a typed map for caching loggers by name. 99 | */ 100 | const cache = new Map() 101 | 102 | /** 103 | * Cached factory function for loggers, which avoids 104 | * instantiating duplicates with the same name. 105 | */ 106 | export function logger(init: string | LoggerOptions) : Logger { 107 | let name = typeof init === 'string' 108 | ? init 109 | : init.name 110 | if (!cache.has(name)) { 111 | cache.set(name, new Logger(init)) 112 | } 113 | return cache.get(name) 114 | } 115 | 116 | /** 117 | * Set the default global level for new loggers, and change 118 | * the current logging level of all existing cached loggers. 119 | */ 120 | export function set_level(level: Level) : void { 121 | global_level = level 122 | for (let key of cache.keys()) { 123 | cache.get(key).level = level 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/util/template.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './log' 2 | declare var Handlebars 3 | 4 | const log = logger('template') 5 | 6 | export interface TemplateFunction { 7 | (ctx?: any) : string 8 | } 9 | 10 | export function compile_template(tmpl: string|TemplateFunction) : TemplateFunction { 11 | if (typeof tmpl === 'string') { 12 | return Handlebars.compile(tmpl) 13 | } else { 14 | return tmpl 15 | } 16 | } 17 | 18 | function _render_template(tmpl: string|TemplateFunction, context?: any) : string { 19 | if (tmpl == null) { 20 | return '' 21 | } 22 | if (typeof tmpl === 'string') { 23 | if (tmpl.indexOf('{{') == -1) { 24 | return tmpl 25 | } else { 26 | return Handlebars.compile(tmpl)(context) 27 | } 28 | } else { 29 | return tmpl(context) 30 | } 31 | } 32 | 33 | export function render_template(tmpl: string|TemplateFunction, context?: any) : string { 34 | try { 35 | return _render_template(tmpl, context) 36 | } catch (e) { 37 | log.error('render_template(): ' + e) 38 | if (typeof tmpl === 'string') { 39 | return tmpl 40 | } else { 41 | return '' 42 | } 43 | } 44 | } 45 | 46 | export class Template { 47 | tmpl: TemplateFunction 48 | 49 | constructor(tmpl: string|TemplateFunction) { 50 | this.tmpl = compile_template(tmpl) 51 | } 52 | 53 | render(context?: any) : string { 54 | return render_template(this.tmpl, context) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tessera-frontend/src/ts/util/util.ts: -------------------------------------------------------------------------------- 1 | declare var require 2 | 3 | export function json(thing: any) : any { 4 | if (thing.toJSON && typeof(thing.toJSON) === 'function') { 5 | return thing.toJSON() 6 | } else { 7 | return thing 8 | } 9 | } 10 | 11 | export const extend = require('extend') 12 | -------------------------------------------------------------------------------- /tessera-server/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | include tessera/tessera.db 4 | 5 | global-exclude *.pyc *.pyo ._* 6 | 7 | recursive-include tessera/templates * 8 | recursive-include tessera/static * 9 | -------------------------------------------------------------------------------- /tessera-server/dev-requirements.txt: -------------------------------------------------------------------------------- 1 | invoke==0.14.0 2 | invocations==0.13.0 3 | spec==0.11.1 4 | nose==1.3.0 5 | -------------------------------------------------------------------------------- /tessera-server/integration/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from spec import Spec, skip, eq_ 5 | from invoke import run 6 | 7 | 8 | class Integration(Spec): 9 | def setup(self): 10 | from tessera.application import db 11 | # Ensure we have a clean db target. 12 | self.dbpath = db.engine.url.database 13 | msg = "You seem to have a db in the default location ({0}) - please (re)move it before running tests to avoid collisions." 14 | assert not os.path.exists(self.dbpath), msg.format(self.dbpath) 15 | 16 | def teardown(self): 17 | from tessera.application import db 18 | # Teardown only runs if setup completed, so the below will not nuke 19 | # pre-existing dbs that cause setup's check to fail. 20 | if os.path.exists(self.dbpath): 21 | os.remove(self.dbpath) 22 | # Ensure no cached session crap 23 | db.session.close_all() 24 | 25 | 26 | def is_importable(self): 27 | import tessera 28 | assert tessera.app 29 | assert tessera.db 30 | 31 | def can_initdb(self): 32 | from tessera.application import db 33 | from tessera.database import DashboardRecord 34 | # Make sure we can create and look at the DB 35 | db.create_all() 36 | eq_(len(DashboardRecord.query.all()), 0) 37 | 38 | # def can_import_fixtures(self): 39 | # from tessera.application import db 40 | # from tessera.importer.json import JsonImporter 41 | # from tessera.database import DashboardRecord 42 | # db.create_all() 43 | # path = os.path.abspath(os.path.join( 44 | # os.path.dirname(__file__), '..', 'demo', 'demo-gallery.json' 45 | # )) 46 | # JsonImporter.import_file(path) 47 | # eq_(len(DashboardRecord.query.all()), 1) 48 | -------------------------------------------------------------------------------- /tessera-server/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /tessera-server/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /tessera-server/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from flask import current_app 19 | config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) 20 | target_metadata = current_app.extensions['migrate'].db.metadata 21 | 22 | # other values from the config, defined by the needs of env.py, 23 | # can be acquired: 24 | # my_important_option = config.get_main_option("my_important_option") 25 | # ... etc. 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | url = config.get_main_option("sqlalchemy.url") 40 | context.configure(url=url) 41 | 42 | with context.begin_transaction(): 43 | context.run_migrations() 44 | 45 | def run_migrations_online(): 46 | """Run migrations in 'online' mode. 47 | 48 | In this scenario we need to create an Engine 49 | and associate a connection with the context. 50 | 51 | """ 52 | engine = engine_from_config( 53 | config.get_section(config.config_ini_section), 54 | prefix='sqlalchemy.', 55 | poolclass=pool.NullPool) 56 | 57 | connection = engine.connect() 58 | context.configure( 59 | connection=connection, 60 | target_metadata=target_metadata 61 | ) 62 | 63 | try: 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | finally: 67 | connection.close() 68 | 69 | if context.is_offline_mode(): 70 | run_migrations_offline() 71 | else: 72 | run_migrations_online() 73 | 74 | -------------------------------------------------------------------------------- /tessera-server/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /tessera-server/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.12.4 2 | flask-sqlalchemy==2.1 3 | flask-migrate==2.0.2 4 | flask-cors==3.0.2 5 | sqlalchemy==1.3.0 6 | requests==2.20.0 7 | inflection==0.3.1 8 | six==1.10.0 9 | -------------------------------------------------------------------------------- /tessera-server/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | name = 'tessera' 4 | filename = '{0}/_version.py'.format(name) 5 | _locals = {} 6 | with open('tessera/_version.py') as src: 7 | exec(src.read(), None, _locals) 8 | version = _locals['__version__'] 9 | 10 | setup( 11 | name=name, 12 | version=version, 13 | description='A dashboard front end for Graphite', 14 | license='Apache', 15 | 16 | author='Adam Alpern', 17 | url='https://github.com/tessera-metrics/tessera', 18 | 19 | packages=find_packages(), 20 | include_package_data=True, # Ensure templates, etc get pulled into sdists 21 | install_requires=[ 22 | x.strip() 23 | for x in open('requirements.txt').readlines() 24 | if x and not x.startswith('#') 25 | ], 26 | 27 | entry_points = { 28 | 'console_scripts': [ 29 | 'tessera-init = tessera.main:init', 30 | 'tessera = tessera.main:run' 31 | ] 32 | }, 33 | 34 | classifiers=[ 35 | 'Development Status :: 3 - Alpha', 36 | 'Environment :: No Input/Output (Daemon)', 37 | 'Framework :: Flask', 38 | 'Intended Audience :: Developers', 39 | 'Intended Audience :: Information Technology', 40 | 'Intended Audience :: System Administrators', 41 | 'License :: OSI Approved :: Apache Software License', 42 | 'Operating System :: MacOS :: MacOS X', 43 | 'Operating System :: POSIX', 44 | 'Operating System :: Unix', 45 | 'Programming Language :: Python :: 2.7', 46 | 'Programming Language :: Python :: 3.5', 47 | 'Programming Language :: Python', 48 | 'Topic :: System :: Monitoring', 49 | 'Topic :: System :: Systems Administration', 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /tessera-server/tessera/__init__.py: -------------------------------------------------------------------------------- 1 | from .application import * 2 | -------------------------------------------------------------------------------- /tessera-server/tessera/_version.py: -------------------------------------------------------------------------------- 1 | __version_info__ = (0, 11, 0, 'beta') 2 | __version__ = '.'.join(map(str, __version_info__)) 3 | -------------------------------------------------------------------------------- /tessera-server/tessera/application.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python -*- 2 | 3 | import logging 4 | import os 5 | import inflection 6 | 7 | from flask import Flask 8 | from flask_sqlalchemy import SQLAlchemy 9 | from flask_migrate import Migrate 10 | from flask_cors import CORS 11 | from werkzeug.wsgi import DispatcherMiddleware 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | def configure(app): 16 | # Load internal defaults (inside application package) 17 | app.config.from_pyfile('config.py') 18 | # Allow overriding from an external file, pathwise (near project, not app, 19 | # root - is typically one level above app root.) 20 | local = os.path.join(os.getcwd(), 'etc', 'config.py') 21 | try: 22 | app.config.from_pyfile(local) 23 | except IOError as e: 24 | # It's fine to not have this file, just log for debugging. 25 | log.debug("Local override config {0!r} not found, skipping".format(local)) 26 | # Wholly configurable config file location via env var. 27 | # Only load if set, and then let it explode on its own if target not found. 28 | if os.environ.get('TESSERA_CONFIG', False): 29 | app.config.from_envvar('TESSERA_CONFIG') 30 | return app 31 | 32 | app = configure(Flask(__name__)) 33 | db = SQLAlchemy(app) 34 | migrate = Migrate(app, db) 35 | config = app.config 36 | 37 | from .views_api import api 38 | from .views_ui import ui 39 | 40 | app.register_blueprint(api, url_prefix='/api') 41 | app.register_blueprint(ui) 42 | 43 | app_root = app.config.get('APPLICATION_ROOT', None) 44 | if app_root: 45 | app.wsgi_app = DispatcherMiddleware(Flask('root'), { 46 | app_root : app 47 | }) 48 | 49 | if app.config.get('ENABLE_CORS', False): 50 | CORS(app, resources={ 51 | r'/api/*': { 52 | 'origins' : app.config.get('CORS_ORIGINS', '*') 53 | } 54 | }) 55 | 56 | @app.template_filter('titleize') 57 | def humanize(s): 58 | return inflection.titleize(s) 59 | -------------------------------------------------------------------------------- /tessera-server/tessera/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-server/tessera/client/__init__.py -------------------------------------------------------------------------------- /tessera-server/tessera/client/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-server/tessera/client/api/__init__.py -------------------------------------------------------------------------------- /tessera-server/tessera/importer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tessera-metrics/tessera/8e554f217220228fb8a0662fb5075cb839e9f1b1/tessera-server/tessera/importer/__init__.py -------------------------------------------------------------------------------- /tessera-server/tessera/importer/json_importer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import inflection 4 | from ..client.api.model import EntityEncoder 5 | from ..client.api.client import TesseraClient 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | class JsonImporter(object): 10 | def __init__(self, graphite_url, tessera_url): 11 | self.graphite_url = graphite_url 12 | self.client = TesseraClient(tessera_url) 13 | 14 | def import_files(self, files): 15 | for f in files: 16 | self.import_file(f) 17 | 18 | def import_file(self, filepath): 19 | log.info('Importing from file {0}'.format(filepath)) 20 | f = open(filepath, 'r') 21 | try: 22 | data = json.load(f) 23 | self.client.create_dashboard(data) 24 | log.info('Succesfully imported dashboard') 25 | finally: 26 | f.close() 27 | 28 | class JsonExporter(object): 29 | def __init__(self, graphite_url, tessera_url): 30 | self.graphite_url = graphite_url 31 | self.client = TesseraClient(tessera_url) 32 | 33 | def export(self, directory, tag=None): 34 | dashboards = self.client.list_dashboards(tag=tag, definition=True) 35 | log.info('Found {0} dashboards to export to {1}'.format(len(dashboards), directory)) 36 | for d in dashboards: 37 | self.export_dashboard(d, directory) 38 | 39 | def export_dashboard(self, dashboard, directory): 40 | filepath = '{0}/{1}.json'.format(directory, inflection.parameterize(dashboard.category + ' ' + dashboard.title)) 41 | log.info('Exporting to {0}'.format(filepath)) 42 | f = open(filepath, 'w') 43 | try: 44 | json.dump(dashboard, f, indent=2, cls=EntityEncoder) 45 | finally: 46 | f.close() 47 | -------------------------------------------------------------------------------- /tessera-server/tessera/main.py: -------------------------------------------------------------------------------- 1 | from tessera import app, db 2 | 3 | def init(): 4 | db.create_all() 5 | 6 | def run(): 7 | app.run(host='0.0.0.0') 8 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "snippets/site-header.html" %} 5 | 6 | 7 | 8 | {% block pageheader %} 9 | {% endblock %} 10 | 11 | {% block pagebody %} 12 | {% endblock %} 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/dashboard-embed.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block pagebody %} 5 | 11 | 12 |
    13 | 14 | 39 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/favorites.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "standard.html" %} 3 | 4 | {% block pagetitle %} 5 | {{config['DASHBOARD_APPNAME']}} {{title}} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |
    11 | 12 | {% include "snippets/breadcrumbs.html" %} 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    Dashboards

    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 30 | 35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "standard.html" %} 3 | 4 | {% block pagetitle %} 5 | {{config['DASHBOARD_APPNAME']}} Index 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |
    11 | 12 |
    13 | 15 |
    16 | 17 | 18 |
    19 | 20 |
    21 |
    22 |

    23 |

    Dashboards

    24 |
    25 |
    26 | 27 |
    28 |
    29 |

    30 |

    Favorites

    31 |
    32 |
    33 | 34 |
    35 |
    36 |

    37 |

    Import

    38 |
    39 |
    40 | 41 |
    42 | 46 |
    47 | 48 |
    49 | 50 | 51 | 57 | 58 |
    59 | 60 | 85 | 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if breadcrumbs is defined %} 4 | 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/dashboard-info-edit-panel.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/range-picker.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | 38 | 49 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/refresh-button.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 10 | 11 | 19 | 20 | 33 | 34 |
    35 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/rejigger-button.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 7 | 8 | 23 | 24 |
    25 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/site-footer.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | {{config.get('DASHBOARD_APPNAME')}} v{{ ctx.version }} 5 | 6 |
    7 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/site-header.html: -------------------------------------------------------------------------------- 1 | 2 | {{config.get('DASHBOARD_APPNAME')}}: {{title}} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 27 | 28 | 29 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/snippets/theme-button.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 7 | 8 | 15 | 16 |
    17 | -------------------------------------------------------------------------------- /tessera-server/tessera/templates/standard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block pageheader %} 4 |
    5 | 29 |
    30 | {% endblock %} 31 | 32 | {% block pagebody %} 33 | 34 | {% block layout %} 35 | {% block content %} 36 | {% endblock %} 37 | {% endblock %} 38 | 39 | {% block layoutfooter %} 40 | {% block footer %} 41 | {% endblock %} 42 | {% endblock %} 43 | 44 | 47 | 48 | 68 | 69 | {% endblock %} 70 | --------------------------------------------------------------------------------